diff --git a/constants.properties b/constants.properties index c134a442b1..afaa2b79c8 100644 --- a/constants.properties +++ b/constants.properties @@ -1,4 +1,4 @@ -gradlePluginsVersion=4.0.2 +gradlePluginsVersion=4.0.3 kotlinVersion=1.2.20 platformVersion=2 guavaVersion=21.0 diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index f7a7d7bff4..8638c8c397 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -7,6 +7,12 @@ from the previous milestone release. 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 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. diff --git a/docs/source/corda-configuration-file.rst b/docs/source/corda-configuration-file.rst index 9f954d3bb5..a4deaac21e 100644 --- a/docs/source/corda-configuration-file.rst +++ b/docs/source/corda-configuration-file.rst @@ -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 :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 cluster then specify ``raft`` or ``bftSMaRt`` respectively as described below. If a single node notary then omit both. @@ -180,15 +170,16 @@ 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 metadata, the content is cached separately and can be loaded lazily. Defaults to 1024. + Examples -------- General node configuration file for hosting the IRSDemo services: .. literalinclude:: example-code/src/main/resources/example-node.conf - :language: javascript +:language: javascript -Simple notary configuration file: + Simple notary configuration file: .. parsed-literal:: @@ -202,9 +193,72 @@ Simple notary configuration file: address : "my-corda-node:10003" adminAddress : "my-corda-node:10004" } - webAddress : "localhost:12347" notary : { validating : false } devMode : true - compatibilityZoneURL : "https://cz.corda.net" \ No newline at end of file + 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 (``/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 (``/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. \ No newline at end of file diff --git a/docs/source/example-code/src/main/resources/example-network-map-node.conf b/docs/source/example-code/src/main/resources/example-network-map-node.conf index 980f5f7031..d36b5b222c 100644 --- a/docs/source/example-code/src/main/resources/example-network-map-node.conf +++ b/docs/source/example-code/src/main/resources/example-network-map-node.conf @@ -1,6 +1,4 @@ myLegalName : "O=Notary Service,OU=corda,L=London,C=GB" keyStorePassword : "cordacadevpass" trustStorePassword : "trustpass" -p2pAddress : "my-network-map:10000" -webAddress : "localhost:10001" -sshdAddress : "localhost:10002" +p2pAddress : "my-network-map:10000" \ No newline at end of file diff --git a/docs/source/example-code/src/main/resources/example-node.conf b/docs/source/example-code/src/main/resources/example-node.conf index 538f118df5..06e9fae078 100644 --- a/docs/source/example-code/src/main/resources/example-node.conf +++ b/docs/source/example-code/src/main/resources/example-node.conf @@ -14,7 +14,6 @@ rpcSettings = { address : "my-corda-node:10003" adminAddress : "my-corda-node:10004" } -webAddress : "localhost:10004" rpcUsers : [ { username=user1, password=letmein, permissions=[ StartFlow.net.corda.protocols.CashProtocol ] } ] diff --git a/docs/source/example-code/src/main/resources/example-out-of-process-verifier-node.conf b/docs/source/example-code/src/main/resources/example-out-of-process-verifier-node.conf index 27bf43a1ab..88fde57379 100644 --- a/docs/source/example-code/src/main/resources/example-out-of-process-verifier-node.conf +++ b/docs/source/example-code/src/main/resources/example-out-of-process-verifier-node.conf @@ -1,4 +1,3 @@ myLegalName : "O=Bank A,L=London,C=GB" p2pAddress : "my-corda-node:10002" -webAddress : "localhost:10003" verifierType: "OutOfProcess" diff --git a/finance/src/integration-test/kotlin/net/corda/finance/flows/CashConfigDataFlowTest.kt b/finance/src/integration-test/kotlin/net/corda/finance/flows/CashConfigDataFlowTest.kt index 49351f1e13..70bf28e8d3 100644 --- a/finance/src/integration-test/kotlin/net/corda/finance/flows/CashConfigDataFlowTest.kt +++ b/finance/src/integration-test/kotlin/net/corda/finance/flows/CashConfigDataFlowTest.kt @@ -12,7 +12,7 @@ class CashConfigDataFlowTest { @Test fun `issuable currencies are read in from node config`() { 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() assertThat(config.issuableCurrencies).containsExactly(EUR, USD) } diff --git a/finance/src/main/kotlin/net/corda/finance/flows/CashConfigDataFlow.kt b/finance/src/main/kotlin/net/corda/finance/flows/CashConfigDataFlow.kt index 24735d8505..e1978905fa 100644 --- a/finance/src/main/kotlin/net/corda/finance/flows/CashConfigDataFlow.kt +++ b/finance/src/main/kotlin/net/corda/finance/flows/CashConfigDataFlow.kt @@ -16,6 +16,7 @@ import net.corda.finance.EUR import net.corda.finance.GBP import net.corda.finance.USD import net.corda.finance.flows.ConfigHolder.Companion.supportedCurrencies +import java.io.IOException import java.nio.file.Path 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("getBaseDirectory").apply { isAccessible = true }.invoke(it)} .let { it.javaClass.getMethod("toString").apply { isAccessible = true }.invoke(it) as String } - val config = (baseDirectory / "node.conf").read { ConfigFactory.parseReader(it.reader()) } - if (config.hasPath("issuableCurrencies")) { - issuableCurrencies = config.getStringList("issuableCurrencies").map { Currency.getInstance(it) } - require(supportedCurrencies.containsAll(issuableCurrencies)) - } else { - issuableCurrencies = emptyList() + + var issuableCurrenciesValue: List + try { + val config = (baseDirectory / "node.conf").read { ConfigFactory.parseReader(it.reader()) } + if (config.hasPath("custom.issuableCurrencies")) { + issuableCurrenciesValue = config.getStringList("custom.issuableCurrencies").map { Currency.getInstance(it) } + require(supportedCurrencies.containsAll(issuableCurrenciesValue)) + } else { + issuableCurrenciesValue = emptyList() + } + } catch (e: IOException) { + issuableCurrenciesValue = emptyList() } + issuableCurrencies = issuableCurrenciesValue } } diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/config/ConfigUtilities.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/config/ConfigUtilities.kt index 544f1c1f7e..dbf880905b 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/config/ConfigUtilities.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/config/ConfigUtilities.kt @@ -29,6 +29,8 @@ import kotlin.reflect.jvm.jvmErasure @Target(AnnotationTarget.PROPERTY) annotation class OldConfig(val value: String) +const val CUSTOM_NODE_PROPERTIES_ROOT = "custom" + // TODO Move other config parsing to use parseAs and remove this operator fun Config.getValue(receiver: Any, metadata: KProperty<*>): T { return getValueInternal(metadata.name, metadata.returnType) @@ -37,9 +39,24 @@ operator fun Config.getValue(receiver: Any, metadata: KProperty<*>): T fun Config.parseAs(clazz: KClass): T { require(clazz.isData) { "Only Kotlin data classes can be parsed. Offending: ${clazz.qualifiedName}" } val constructor = clazz.primaryConstructor!! - val args = constructor.parameters - .filterNot { it.isOptional && !hasPath(it.name!!) } - .associateBy({ it }) { param -> + val parameters = constructor.parameters + val parameterNames = parameters.flatMap { param -> + mutableSetOf().apply { + param.name?.let(this::add) + clazz.memberProperties.singleOrNull { it.name == param.name }?.let { matchingProperty -> + matchingProperty.annotations.filterIsInstance().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 val property = clazz.memberProperties.first { it.name == param.name } val path = defaultToOldPath(property) @@ -48,6 +65,20 @@ fun Config.parseAs(clazz: KClass): T { return constructor.callBy(args) } +class UnknownConfigurationKeysException private constructor(val unknownKeys: Set) : IllegalArgumentException(message(unknownKeys)) { + + init { + require(unknownKeys.isNotEmpty()) { "Absence of unknown keys should not raise UnknownConfigurationKeysException." } + } + + companion object { + + fun of(offendingKeys: Set): UnknownConfigurationKeysException = UnknownConfigurationKeysException(offendingKeys) + + private fun message(offendingKeys: Set) = "Unknown configuration keys: ${offendingKeys.joinToString(", ", "[", "]")}." + } +} + inline fun Config.parseAs(): T = parseAs(T::class) fun Config.toProperties(): Properties { diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/network/NetworkBootstrapper.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/network/NetworkBootstrapper.kt index cfd038a208..cc4a91f4f8 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/network/NetworkBootstrapper.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/network/NetworkBootstrapper.kt @@ -90,15 +90,17 @@ class NetworkBootstrapper { } 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 println("Node config files found in the root directory - generating node directories") val cordaJar = extractCordaJarTo(directory) for (confFile in confFiles) { - val nodeName = confFile.fileName.toString().removeSuffix(".conf") + val nodeName = confFile.fileName.toString().removeSuffix("_node.conf") println("Generating directory for $nodeName") val nodeDir = (directory / nodeName).createDirectories() 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.delete(cordaJar) diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/config/ConfigParsingTest.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/config/ConfigParsingTest.kt index 7fbc481321..265de97530 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/config/ConfigParsingTest.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/config/ConfigParsingTest.kt @@ -200,6 +200,21 @@ class ConfigParsingTest { 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() }.isInstanceOfSatisfying(UnknownConfigurationKeysException::class.java) { exception -> + + assertThat(exception.unknownKeys).contains(unknownKey) + assertThat(exception.unknownKeys).doesNotContain(knownKey) + } + } + private inline fun , reified L : ListData, V : Any> testPropertyType( value1: V, value2: V, @@ -243,6 +258,7 @@ class ConfigParsingTest { val values: List } + data class TypedConfiguration(private val mandatory: String, private val optional: String = "optional") data class StringData(override val value: String) : SingleData data class StringListData(override val values: List) : ListData data class StringSetData(val values: Set) diff --git a/node/src/integration-test/kotlin/net/corda/node/AuthDBTests.kt b/node/src/integration-test/kotlin/net/corda/node/AuthDBTests.kt index 0485a5e70f..73c9aa026f 100644 --- a/node/src/integration-test/kotlin/net/corda/node/AuthDBTests.kt +++ b/node/src/integration-test/kotlin/net/corda/node/AuthDBTests.kt @@ -82,14 +82,14 @@ class AuthDBTests : NodeBasedTest() { "password" to "", "driverClassName" to "org.h2.Driver" ) + ), + "options" to mapOf( + "cache" to mapOf( + "expireAfterSecs" to cacheExpireAfterSecs, + "maxEntries" to 50 + ) ) - ), - "options" to mapOf( - "cache" to mapOf( - "expireAfterSecs" to cacheExpireAfterSecs, - "maxEntries" to 50 - ) - ) + ) ) ) diff --git a/node/src/main/java/CordaCaplet.java b/node/src/main/java/CordaCaplet.java index e42e5acd80..ac81f04b1c 100644 --- a/node/src/main/java/CordaCaplet.java +++ b/node/src/main/java/CordaCaplet.java @@ -100,7 +100,7 @@ public class CordaCaplet extends Capsule { // Read JVM args from the config if specified, else leave alone. List jvmArgs = new ArrayList<>((List) super.attribute(attr)); try { - List configJvmArgs = nodeConfig.getStringList("jvmArgs"); + List configJvmArgs = nodeConfig.getStringList("custom.jvmArgs"); jvmArgs.clear(); jvmArgs.addAll(configJvmArgs); log(LOG_VERBOSE, "Configured JVM args = " + jvmArgs); diff --git a/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt b/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt index 17e980ee87..af04a57afd 100644 --- a/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt +++ b/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt @@ -17,6 +17,7 @@ import net.corda.node.shell.InteractiveShell import net.corda.node.utilities.registration.HTTPNetworkRegistrationService import net.corda.node.utilities.registration.NetworkRegistrationHelper import net.corda.nodeapi.internal.addShutdownHook +import net.corda.nodeapi.internal.config.UnknownConfigurationKeysException import org.fusesource.jansi.Ansi import org.fusesource.jansi.AnsiConsole import org.slf4j.bridge.SLF4JBridgeHandler @@ -86,6 +87,9 @@ open class NodeStartup(val args: Array) { } else { conf0 } + } catch (e: UnknownConfigurationKeysException) { + logger.error(e.message) + return false } catch (e: Exception) { logger.error("Exception during node configuration", e) return false diff --git a/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt b/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt index 02a5489d6b..c105cb2e4c 100644 --- a/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt +++ b/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt @@ -150,7 +150,9 @@ data class NodeConfigurationImpl( override val database: DatabaseConfig = DatabaseConfig(initialiseSchema = devMode, exportHibernateJMXStatistics = devMode), private val transactionCacheSizeMegaBytes: 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 { companion object { private val logger = loggerFor() diff --git a/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/BankOfCordaCordform.kt b/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/BankOfCordaCordform.kt index e750504a8f..c37d1722e2 100644 --- a/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/BankOfCordaCordform.kt +++ b/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/BankOfCordaCordform.kt @@ -38,7 +38,7 @@ class BankOfCordaCordform : CordformDefinition() { } node { name(BOC_NAME) - extraConfig = mapOf("issuableCurrencies" to listOf("USD")) + extraConfig = mapOf("custom" to mapOf("issuableCurrencies" to listOf("USD"))) p2pPort(10005) rpcSettings { address("localhost:$BOC_RPC_PORT") diff --git a/samples/irs-demo/cordapp/build.gradle b/samples/irs-demo/cordapp/build.gradle index 5ee75f4ee2..5e735c8a72 100644 --- a/samples/irs-demo/cordapp/build.gradle +++ b/samples/irs-demo/cordapp/build.gradle @@ -64,8 +64,8 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { notary = [validating : true] p2pPort 10002 rpcSettings { - port 10003 - adminPort 10023 + address("localhost:10003") + adminAddress("localhost:10023") } cordapps = ["${project(":finance").group}:finance:$corda_release_version"] rpcUsers = rpcUsersList @@ -75,8 +75,8 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { name "O=Bank A,L=London,C=GB" p2pPort 10005 rpcSettings { - port 10006 - adminPort 10026 + address("localhost:10006") + adminAddress("localhost:10026") } cordapps = ["${project(":finance").group}:finance:$corda_release_version"] rpcUsers = rpcUsersList @@ -86,8 +86,8 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { name "O=Bank B,L=New York,C=US" p2pPort 10008 rpcSettings { - port 10009 - adminPort 10029 + address("localhost:10009") + adminAddress("localhost:10029") } cordapps = ["${project.group}:finance:$corda_release_version"] rpcUsers = rpcUsersList @@ -97,8 +97,8 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { name "O=Regulator,L=Moscow,C=RU" p2pPort 10011 rpcSettings { - port 10012 - adminPort 10032 + address("localhost:10012") + adminAddress("localhost:10032") } cordapps = ["${project.group}:finance:$corda_release_version"] cordapps = ["${project(":finance").group}:finance:$corda_release_version"] diff --git a/samples/simm-valuation-demo/build.gradle b/samples/simm-valuation-demo/build.gradle index 9edd4f2853..590948ad66 100644 --- a/samples/simm-valuation-demo/build.gradle +++ b/samples/simm-valuation-demo/build.gradle @@ -70,7 +70,9 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { p2pPort 10002 cordapp project(':finance') extraConfig = [ - jvmArgs : [ "-Xmx1g"] + custom: [ + jvmArgs: ["-Xmx1g"] + ] ] } node { @@ -84,7 +86,9 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { cordapp project(':finance') rpcUsers = ext.rpcUsers extraConfig = [ - jvmArgs : [ "-Xmx1g"] + custom: [ + jvmArgs: ["-Xmx1g"] + ] ] } node { @@ -98,7 +102,9 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { cordapp project(':finance') rpcUsers = ext.rpcUsers extraConfig = [ - jvmArgs : [ "-Xmx1g"] + custom: [ + jvmArgs: ["-Xmx1g"] + ] ] } node { @@ -112,7 +118,9 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { cordapp project(':finance') rpcUsers = ext.rpcUsers extraConfig = [ - jvmArgs : [ "-Xmx1g"] + custom: [ + jvmArgs: ["-Xmx1g"] + ] ] } } diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt index ecf37c5a1b..942159efa8 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt @@ -5,6 +5,7 @@ import com.google.common.util.concurrent.ThreadFactoryBuilder import com.typesafe.config.Config import com.typesafe.config.ConfigFactory import com.typesafe.config.ConfigRenderOptions +import com.typesafe.config.ConfigValueFactory import net.corda.client.rpc.CordaRPCClient import net.corda.client.rpc.internal.createCordaRPCClientWithSsl import net.corda.cordform.CordformContext @@ -229,7 +230,6 @@ class DriverDSLImpl( "p2pAddress" to p2pAddress.toString(), "rpcSettings.address" to rpcAddress.toString(), "rpcSettings.adminAddress" to rpcAdminAddress.toString(), - "webAddress" to webAddress.toString(), "useTestClock" to useTestClock, "rpcUsers" to if (users.isEmpty()) defaultRpcUserList else users.map { it.toConfig().root().unwrapped() }, "verifierType" to verifierType.name @@ -355,13 +355,17 @@ class DriverDSLImpl( val webAddress = cordform.webAddress?.let { NetworkHostAndPort.parse(it) } ?: portAllocation.nextHostAndPort() val notary = if (cordform.notary != null) mapOf("notary" to cordform.notary) else emptyMap() val rpcUsers = cordform.rpcUsers - val config = NodeConfig(ConfigHelper.loadConfig( + + val rawConfig = cordform.config + rpcAddress + notary + mapOf( + "rpcUsers" to if (rpcUsers.isEmpty()) defaultRpcUserList else rpcUsers + ) + val typesafe = ConfigHelper.loadConfig( baseDirectory = baseDirectory(name), allowMissingConfig = true, - configOverrides = cordform.config + rpcAddress + notary + mapOf( - "rpcUsers" to if (rpcUsers.isEmpty()) defaultRpcUserList else rpcUsers - ) - )) + configOverrides = rawConfig.toNodeOnly() + ) + val cordaConfig = typesafe.parseAsNodeConfiguration() + val config = NodeConfig(rawConfig, cordaConfig) return startNodeInternal(config, webAddress, null, "200m", localNetworkMap) } @@ -384,9 +388,9 @@ class DriverDSLImpl( override fun startWebserver(handle: NodeHandle, maximumHeapSize: String): CordaFuture { 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) - val webReadyFuture = addressMustBeBoundFuture(executorService, (handle as NodeHandleInternal).webAddress, process) + val webReadyFuture = addressMustBeBoundFuture(executorService, handle.webAddress, 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]. * Keeping [Config] around is needed as the user may specify extra config options not specified in [NodeConfiguration]. */ - private class NodeConfig(val typesafe: Config) { - val corda: NodeConfiguration = typesafe.parseAsNodeConfiguration().also { nodeConfiguration -> - val errors = nodeConfiguration.validate() - if (errors.isNotEmpty()) { - throw IllegalStateException("Invalid node configuration. Errors where:${System.lineSeparator()}${errors.joinToString(System.lineSeparator())}") - } + private class NodeConfig(val typesafe: Config, val corda: NodeConfiguration = typesafe.parseAsNodeConfiguration().also { nodeConfiguration -> + val errors = nodeConfiguration.validate() + if (errors.isNotEmpty()) { + throw IllegalStateException("Invalid node configuration. Errors where:${System.lineSeparator()}${errors.joinToString(System.lineSeparator())}") } - } + }) companion object { 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") } // 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? val node = InProcessNode(config.corda, MOCK_VERSION_INFO, cordappPackages).start() val nodeThread = thread(name = config.corda.myLegalName.organisation) { @@ -798,7 +800,7 @@ class DriverDSLImpl( "debug port is " + (debugPort ?: "not enabled") + ", " + "jolokia monitoring port is " + (monitorPort ?: "not enabled")) // Write node.conf - writeConfig(config.corda.baseDirectory, "node.conf", config.typesafe) + writeConfig(config.corda.baseDirectory, "node.conf", config.typesafe.toNodeOnly()) val systemProperties = mutableMapOf( "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" + writeConfig(handle.baseDirectory, "web-server.conf", handle.toWebServerConfig()) return ProcessUtilities.startCordaProcess( className = className, // cannot directly get class for this, so just use string 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) = 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. * 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()) 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) = withValue(property.first, ConfigValueFactory.fromAnyRef(property.second)) \ No newline at end of file diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeConfig.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeConfig.kt index 5012c99908..d8553818e3 100644 --- a/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeConfig.kt +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeConfig.kt @@ -46,17 +46,49 @@ data class NodeConfig( @Suppress("unused") 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() .withValue("address", ConfigValueFactory.fromAnyRef(rpcAddress.toString())) .withValue("adminAddress", ConfigValueFactory.fromAnyRef(rpcAdminAddress.toString())) .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 = 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 +) { + + fun asConfig() = toConfig() } /** diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeController.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeController.kt index e96c4b847d..bd1226374a 100644 --- a/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeController.kt +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeController.kt @@ -121,7 +121,11 @@ class NodeController(check: atRuntime = ::checkExists) : Controller() { // Write this node's configuration file into its working directory. 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 val cordaEnv = System.getenv().toMutableMap().apply { diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/profile/ProfileController.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/profile/ProfileController.kt index 9f8a380b38..45b2a94a5e 100644 --- a/tools/demobench/src/main/kotlin/net/corda/demobench/profile/ProfileController.kt +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/profile/ProfileController.kt @@ -58,7 +58,7 @@ class ProfileController : Controller() { configs.forEach { config -> // Write the configuration file. 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") // Write all of the non-built-in cordapps. diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/web/WebServerController.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/web/WebServerController.kt index a73f652cac..6760547adc 100644 --- a/tools/demobench/src/main/kotlin/net/corda/demobench/web/WebServerController.kt +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/web/WebServerController.kt @@ -11,7 +11,7 @@ class WebServerController : Controller() { 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) } diff --git a/tools/demobench/src/test/kotlin/net/corda/demobench/model/NodeConfigTest.kt b/tools/demobench/src/test/kotlin/net/corda/demobench/model/NodeConfigTest.kt index cbe6f54a18..1e9a874119 100644 --- a/tools/demobench/src/test/kotlin/net/corda/demobench/model/NodeConfigTest.kt +++ b/tools/demobench/src/test/kotlin/net/corda/demobench/model/NodeConfigTest.kt @@ -6,7 +6,6 @@ import net.corda.core.identity.CordaX500Name import net.corda.core.utilities.NetworkHostAndPort import net.corda.node.services.config.parseAsNodeConfiguration import net.corda.nodeapi.internal.config.User -import net.corda.nodeapi.internal.config.toConfig import net.corda.webserver.WebServerConfig import org.assertj.core.api.Assertions.assertThat import org.junit.Test @@ -28,14 +27,14 @@ class NodeConfigTest { legalName = myLegalName, p2pPort = 10001, rpcPort = 40002, - rpcAdminPort = 40003, + rpcAdminPort = 40005, webPort = 20001, h2port = 30001, notary = NotaryService(validating = false), users = listOf(user("jenny")) ) - val nodeConfig = config.toConfig() + val nodeConfig = config.nodeConf() .withValue("baseDirectory", ConfigValueFactory.fromAnyRef(baseDir.toString())) .withFallback(ConfigFactory.parseResources("reference.conf")) .resolve() @@ -63,7 +62,7 @@ class NodeConfigTest { users = listOf(user("jenny")) ) - val nodeConfig = config.toConfig() + val nodeConfig = config.webServerConf() .withValue("baseDirectory", ConfigValueFactory.fromAnyRef(baseDir.toString())) .withFallback(ConfigFactory.parseResources("web-reference.conf")) .resolve() diff --git a/tools/explorer/src/main/kotlin/net/corda/explorer/ExplorerSimulation.kt b/tools/explorer/src/main/kotlin/net/corda/explorer/ExplorerSimulation.kt index 0eb1f05423..e3b014a02c 100644 --- a/tools/explorer/src/main/kotlin/net/corda/explorer/ExplorerSimulation.kt +++ b/tools/explorer/src/main/kotlin/net/corda/explorer/ExplorerSimulation.kt @@ -70,9 +70,9 @@ class ExplorerSimulation(private val options: OptionSet) { val ukBankName = CordaX500Name(organisation = "UK Bank Plc", locality = "London", country = "GB") val usaBankName = CordaX500Name(organisation = "USA Bank Corp", locality = "New York", country = "US") 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), - customOverrides = mapOf("issuableCurrencies" to listOf("USD"))) + customOverrides = mapOf("custom" to mapOf("issuableCurrencies" to listOf("USD")))) notaryNode = defaultNotaryNode.get() aliceNode = alice.get() diff --git a/webserver/src/main/kotlin/net/corda/webserver/WebArgsParser.kt b/webserver/src/main/kotlin/net/corda/webserver/WebArgsParser.kt index 868ee24d18..a627e46d5c 100644 --- a/webserver/src/main/kotlin/net/corda/webserver/WebArgsParser.kt +++ b/webserver/src/main/kotlin/net/corda/webserver/WebArgsParser.kt @@ -25,7 +25,7 @@ class ArgsParser { private val configFileArg = optionParser .accepts("config-file", "The path to the config file") .withRequiredArg() - .defaultsTo("node.conf") + .defaultsTo("web-server.conf") private val loggerLevel = optionParser .accepts("logging-level", "Enable logging at this level and higher") .withRequiredArg()