From c882b221a5a5b6028c6828dd912f9e1680a158d5 Mon Sep 17 00:00:00 2001 From: Stefan Iliev <46542846+StefanIliev545@users.noreply.github.com> Date: Mon, 21 Oct 2019 12:01:14 +0100 Subject: [PATCH] CORDA-3307 - add support for environment variables in linux (#5523) * Added a new way for environment variables to be loaded, which allows for underscore based separation. * Moved test to its own kotlin file. * Added case insensitivity support. * The corda. prefix is now case insensitive too. * Removed unused variable. * Added env variables support for driverDSL. Shadowing corda. properties raises an exception. * Driver api stability fix. * Changed type of cordapps param to reflect the real one, rather than what IntelliJ auto completed. * Some detekt issue fixes. Spread operator removed, baselined api stability constructors and buggy line. * Fixed misspelled variable. * Reverted unintentional changes. * Added suppress instead of changing baseline. * Reworked logic to handle previously defined CORDA_ starting properties and handle accordingly. Fixed a bug where wrong class was used for reflection walking. * Fix for detekt issues. * Changed message to a more understandable one. * Changelog + doc note, console error grammar. * Changes according to PR review. --- detekt-baseline.xml | 10 +-- docs/source/changelog.rst | 3 + docs/source/corda-configuration-file.rst | 7 ++ .../net/corda/node/NodeConfigParsingTests.kt | 87 +++++++++++++++++++ .../corda/node/NodeStartupPerformanceTests.kt | 2 +- .../node/services/config/ConfigUtilities.kt | 67 +++++++++++++- .../services/config/ShadowingException.kt | 7 ++ .../kotlin/net/corda/testing/driver/Driver.kt | 78 ++++++++++++++++- .../testing/node/internal/DriverDSLImpl.kt | 50 +++++++---- .../testing/node/internal/ProcessUtilities.kt | 20 ++++- .../corda/testing/node/internal/RPCDriver.kt | 5 +- 11 files changed, 304 insertions(+), 32 deletions(-) create mode 100644 node/src/integration-test/kotlin/net/corda/node/NodeConfigParsingTests.kt create mode 100644 node/src/main/kotlin/net/corda/node/services/config/ShadowingException.kt diff --git a/detekt-baseline.xml b/detekt-baseline.xml index 35e4f83c6c..50d853b9e4 100644 --- a/detekt-baseline.xml +++ b/detekt-baseline.xml @@ -691,8 +691,6 @@ LongParameterList:Driver.kt$DriverParameters$( isDebug: Boolean, driverDirectory: Path, portAllocation: PortAllocation, debugPortAllocation: PortAllocation, systemProperties: Map<String, String>, useTestClock: Boolean, startNodesInProcess: Boolean, waitForAllNodesToFinish: Boolean, notarySpecs: List<NotarySpec>, extraCordappPackagesToScan: List<String>, jmxPolicy: JmxPolicy, networkParameters: NetworkParameters, cordappsForAllNodes: Set<TestCordapp>? ) LongParameterList:DriverDSL.kt$DriverDSL$( defaultParameters: NodeParameters = NodeParameters(), providedName: CordaX500Name? = defaultParameters.providedName, rpcUsers: List<User> = defaultParameters.rpcUsers, verifierType: VerifierType = defaultParameters.verifierType, customOverrides: Map<String, Any?> = defaultParameters.customOverrides, startInSameProcess: Boolean? = defaultParameters.startInSameProcess, maximumHeapSize: String = defaultParameters.maximumHeapSize ) LongParameterList:DriverDSL.kt$DriverDSL$( defaultParameters: NodeParameters = NodeParameters(), providedName: CordaX500Name? = defaultParameters.providedName, rpcUsers: List<User> = defaultParameters.rpcUsers, verifierType: VerifierType = defaultParameters.verifierType, customOverrides: Map<String, Any?> = defaultParameters.customOverrides, startInSameProcess: Boolean? = defaultParameters.startInSameProcess, maximumHeapSize: String = defaultParameters.maximumHeapSize, logLevelOverride: String? = defaultParameters.logLevelOverride ) - LongParameterList:DriverDSLImpl.kt$( isDebug: Boolean = DriverParameters().isDebug, driverDirectory: Path = DriverParameters().driverDirectory, portAllocation: PortAllocation = DriverParameters().portAllocation, debugPortAllocation: PortAllocation = DriverParameters().debugPortAllocation, systemProperties: Map<String, String> = DriverParameters().systemProperties, useTestClock: Boolean = DriverParameters().useTestClock, startNodesInProcess: Boolean = DriverParameters().startNodesInProcess, extraCordappPackagesToScan: List<String> = @Suppress("DEPRECATION") DriverParameters().extraCordappPackagesToScan, waitForAllNodesToFinish: Boolean = DriverParameters().waitForAllNodesToFinish, notarySpecs: List<NotarySpec> = DriverParameters().notarySpecs, jmxPolicy: JmxPolicy = DriverParameters().jmxPolicy, networkParameters: NetworkParameters = DriverParameters().networkParameters, compatibilityZone: CompatibilityZoneParams? = null, notaryCustomOverrides: Map<String, Any?> = DriverParameters().notaryCustomOverrides, inMemoryDB: Boolean = DriverParameters().inMemoryDB, cordappsForAllNodes: Collection<TestCordappInternal>? = null, dsl: DriverDSLImpl.() -> A ) - LongParameterList:DriverDSLImpl.kt$DriverDSLImpl.Companion$( config: NodeConfig, quasarJarPath: String, debugPort: Int?, overriddenSystemProperties: Map<String, String>, maximumHeapSize: String, logLevelOverride: String?, vararg extraCmdLineFlag: String ) LongParameterList:DummyFungibleContract.kt$DummyFungibleContract$(inputs: List<State>, outputs: List<State>, tx: LedgerTransaction, issueCommand: CommandWithParties<Commands.Issue>, currency: Currency, issuer: PartyAndReference) LongParameterList:IRS.kt$FloatingRatePaymentEvent$(date: LocalDate = this.date, accrualStartDate: LocalDate = this.accrualStartDate, accrualEndDate: LocalDate = this.accrualEndDate, dayCountBasisDay: DayCountBasisDay = this.dayCountBasisDay, dayCountBasisYear: DayCountBasisYear = this.dayCountBasisYear, fixingDate: LocalDate = this.fixingDate, notional: Amount<Currency> = this.notional, rate: Rate = this.rate) LongParameterList:IRS.kt$InterestRateSwap$(floatingLeg: FloatingLeg, fixedLeg: FixedLeg, calculation: Calculation, common: Common, oracle: Party, notary: Party) @@ -729,8 +727,6 @@ LongParameterList:ParametersUtilities.kt$( notaries: List<NotaryInfo> = emptyList(), minimumPlatformVersion: Int = 1, modifiedTime: Instant = Instant.now(), maxMessageSize: Int = 10485760, // TODO: Make this configurable and consistence across driver, bootstrapper, demobench and NetworkMapServer maxTransactionSize: Int = maxMessageSize * 50, whitelistedContractImplementations: Map<String, List<AttachmentId>> = emptyMap(), epoch: Int = 1, eventHorizon: Duration = 30.days, packageOwnership: Map<String, PublicKey> = emptyMap() ) LongParameterList:PersistentUniquenessProvider.kt$PersistentUniquenessProvider$( states: List<StateRef>, txId: SecureHash, callerIdentity: Party, requestSignature: NotarisationRequestSignature, timeWindow: TimeWindow?, references: List<StateRef> ) LongParameterList:PhysicalLocationStructures.kt$WorldCoordinate$(screenWidth: Double, screenHeight: Double, topLatitude: Double, bottomLatitude: Double, leftLongitude: Double, rightLongitude: Double) - LongParameterList:ProcessUtilities.kt$ProcessUtilities$( arguments: List<String>, classPath: List<String> = defaultClassPath, workingDirectory: Path? = null, jdwpPort: Int? = null, extraJvmArguments: List<String> = emptyList(), maximumHeapSize: String? = null ) - LongParameterList:ProcessUtilities.kt$ProcessUtilities$( className: String, arguments: List<String>, classPath: List<String> = defaultClassPath, workingDirectory: Path? = null, jdwpPort: Int? = null, extraJvmArguments: List<String> = emptyList(), maximumHeapSize: String? = null ) LongParameterList:QueryCriteria.kt$QueryCriteria.FungibleAssetQueryCriteria$( participants: List<AbstractParty>? = this.participants, owner: List<AbstractParty>? = this.owner, quantity: ColumnPredicate<Long>? = this.quantity, issuer: List<AbstractParty>? = this.issuer, issuerRef: List<OpaqueBytes>? = this.issuerRef, status: Vault.StateStatus = this.status, contractStateTypes: Set<Class<out ContractState>>? = this.contractStateTypes ) LongParameterList:QueryCriteria.kt$QueryCriteria.FungibleAssetQueryCriteria$( participants: List<AbstractParty>? = this.participants, owner: List<AbstractParty>? = this.owner, quantity: ColumnPredicate<Long>? = this.quantity, issuer: List<AbstractParty>? = this.issuer, issuerRef: List<OpaqueBytes>? = this.issuerRef, status: Vault.StateStatus = this.status, contractStateTypes: Set<Class<out ContractState>>? = this.contractStateTypes, relevancyStatus: Vault.RelevancyStatus = this.relevancyStatus ) LongParameterList:QueryCriteria.kt$QueryCriteria.LinearStateQueryCriteria$( participants: List<AbstractParty>? = this.participants, uuid: List<UUID>? = this.uuid, externalId: List<String>? = this.externalId, status: Vault.StateStatus = this.status, contractStateTypes: Set<Class<out ContractState>>? = this.contractStateTypes, relevancyStatus: Vault.RelevancyStatus = this.relevancyStatus ) @@ -738,7 +734,6 @@ LongParameterList:QueryCriteria.kt$QueryCriteria.VaultQueryCriteria$( status: Vault.StateStatus = Vault.StateStatus.UNCONSUMED, contractStateTypes: Set<Class<out ContractState>>? = null, stateRefs: List<StateRef>? = null, notary: List<AbstractParty>? = null, softLockingCondition: SoftLockingCondition? = null, timeCondition: TimeCondition? = null, relevancyStatus: Vault.RelevancyStatus = Vault.RelevancyStatus.ALL, constraintTypes: Set<Vault.ConstraintInfo.Type> = emptySet(), constraints: Set<Vault.ConstraintInfo> = emptySet(), participants: List<AbstractParty>? = null, externalIds: List<UUID> = emptyList() ) LongParameterList:QueryCriteria.kt$QueryCriteria.VaultQueryCriteria$( status: Vault.StateStatus = this.status, contractStateTypes: Set<Class<out ContractState>>? = this.contractStateTypes, stateRefs: List<StateRef>? = this.stateRefs, notary: List<AbstractParty>? = this.notary, softLockingCondition: SoftLockingCondition? = this.softLockingCondition, timeCondition: TimeCondition? = this.timeCondition ) LongParameterList:RPCClient.kt$RPCClient$( rpcOpsClass: Class<I>, username: String, password: String, externalTrace: Trace? = null, impersonatedActor: Actor? = null, targetLegalIdentity: CordaX500Name? = null ) - LongParameterList:RPCDriver.kt$( isDebug: Boolean = false, driverDirectory: Path = Paths.get("build") / "rpc-driver" / getTimestampAsDirectoryName(), portAllocation: PortAllocation = globalPortAllocation, debugPortAllocation: PortAllocation = globalDebugPortAllocation, systemProperties: Map<String, String> = emptyMap(), useTestClock: Boolean = false, startNodesInProcess: Boolean = false, waitForNodesToFinish: Boolean = false, extraCordappPackagesToScan: List<String> = emptyList(), notarySpecs: List<NotarySpec> = emptyList(), externalTrace: Trace? = null, @Suppress("DEPRECATION") jmxPolicy: JmxPolicy = JmxPolicy(), networkParameters: NetworkParameters = testNetworkParameters(), notaryCustomOverrides: Map<String, Any?> = emptyMap(), inMemoryDB: Boolean = true, cordappsForAllNodes: Collection<TestCordappInternal>? = null, dsl: RPCDriverDSL.() -> A ) LongParameterList:RPCDriver.kt$RPCDriverDSL$( rpcUser: User = rpcTestUser, nodeLegalName: CordaX500Name = fakeNodeLegalName, configuration: RPCServerConfiguration = RPCServerConfiguration.DEFAULT, listOps: List<I>, brokerHandle: RpcBrokerHandle, queueDrainTimeout: Duration = 5.seconds ) LongParameterList:RPCDriver.kt$RPCDriverDSL$( rpcUser: User = rpcTestUser, nodeLegalName: CordaX500Name = fakeNodeLegalName, configuration: RPCServerConfiguration = RPCServerConfiguration.DEFAULT, ops: I, brokerHandle: RpcBrokerHandle, queueDrainTimeout: Duration = 5.seconds ) LongParameterList:RPCDriver.kt$RPCDriverDSL$( rpcUser: User = rpcTestUser, nodeLegalName: CordaX500Name = fakeNodeLegalName, maxFileSize: Int = MAX_MESSAGE_SIZE, maxBufferedBytesPerClient: Long = 10L * MAX_MESSAGE_SIZE, configuration: RPCServerConfiguration = RPCServerConfiguration.DEFAULT, ops: I, queueDrainTimeout: Duration = 5.seconds ) @@ -1769,7 +1764,6 @@ MaxLineLength:ConfigUtilities.kt$ // TODO Move this to KeyStoreConfigHelpers. fun NodeConfiguration.configureWithDevSSLCertificate(cryptoService: CryptoService? = null) MaxLineLength:ConfigUtilities.kt$// Problems: // - Forces you to have a primary constructor with all fields of name and type matching the configuration file structure. // - Encourages weak bean-like types. // - Cannot support a many-to-one relationship between configuration file structures and configuration domain type. This is essential for versioning of the configuration files. // - It's complicated and based on reflection, meaning problems with it are typically found at runtime. // - It doesn't support validation errors in a structured way. If something goes wrong, it throws exceptions, which doesn't support good usability practices like displaying all the errors at once. fun <T : Any> Config.parseAs(clazz: KClass<T>, onUnknownKeys: ((Set<String>, logger: Logger) -> Unit) = UnknownConfigKeysPolicy.FAIL::handle, nestedPath: String? = null, baseDirectory: Path? = null): T MaxLineLength:ConfigUtilities.kt$// TODO Move this to KeyStoreConfigHelpers. fun MutualSslConfiguration.configureDevKeyAndTrustStores(myLegalName: CordaX500Name, signingCertificateStore: FileBasedCertificateStoreSupplier, certificatesDirectory: Path, cryptoService: CryptoService? = null) - MaxLineLength:ConfigUtilities.kt$ConfigHelper$return ConfigFactory.parseMap(toProperties().filterKeys { (it as String).startsWith(CORDA_PROPERTY_PREFIX) }.mapKeys { (it.key as String).removePrefix(CORDA_PROPERTY_PREFIX) }) MaxLineLength:ConfigUtilities.kt$ConfigHelper$val smartDevMode = CordaSystemUtils.isOsMac() || (CordaSystemUtils.isOsWindows() && !CordaSystemUtils.getOsName().toLowerCase().contains("server")) MaxLineLength:ConfigUtilities.kt$fun Any?.toConfigValue(): ConfigValue MaxLineLength:ConfigUtilities.kt$inline fun <reified T : Any> Config.parseAs(noinline onUnknownKeys: ((Set<String>, logger: Logger) -> Unit) = UnknownConfigKeysPolicy.FAIL::handle): T @@ -2088,9 +2082,8 @@ MaxLineLength:DriverDSLImpl.kt$DriverDSLImpl$private MaxLineLength:DriverDSLImpl.kt$DriverDSLImpl$val flowOverrideConfig = FlowOverrideConfig(parameters.flowOverrides.map { FlowOverride(it.key.canonicalName, it.value.canonicalName) }) MaxLineLength:DriverDSLImpl.kt$DriverDSLImpl$val jdbcUrl = "jdbc:h2:mem:persistence${inMemoryCounter.getAndIncrement()};DB_CLOSE_ON_EXIT=FALSE;LOCK_TIMEOUT=10000;WRITE_DELAY=100" - MaxLineLength:DriverDSLImpl.kt$DriverDSLImpl$val process = startOutOfProcessNode(config, quasarJarPath, debugPort, systemProperties, parameters.maximumHeapSize, parameters.logLevelOverride) MaxLineLength:DriverDSLImpl.kt$DriverDSLImpl.Companion$private operator fun Config.plus(property: Pair<String, Any>) - MaxLineLength:DriverDSLImpl.kt$DriverDSLImpl.Companion${ log.info("Starting out-of-process Node ${config.corda.myLegalName.organisation}, debug port is " + (debugPort ?: "not enabled")) // Write node.conf writeConfig(config.corda.baseDirectory, "node.conf", config.typesafe.toNodeOnly()) val systemProperties = mutableMapOf( "name" to config.corda.myLegalName, "visualvm.display.name" to "corda-${config.corda.myLegalName}" ) debugPort?.let { systemProperties += "log4j2.level" to "debug" systemProperties += "log4j2.debug" to "true" } systemProperties += inheritFromParentProcess() systemProperties += overriddenSystemProperties // See experimental/quasar-hook/README.md for how to generate. val excludePattern = "x(antlr**;bftsmart**;ch**;co.paralleluniverse**;com.codahale**;com.esotericsoftware**;" + "com.fasterxml**;com.google**;com.ibm**;com.intellij**;com.jcabi**;com.nhaarman**;com.opengamma**;" + "com.typesafe**;com.zaxxer**;de.javakaffee**;groovy**;groovyjarjarantlr**;groovyjarjarasm**;io.atomix**;" + "io.github**;io.netty**;jdk**;joptsimple**;junit**;kotlin**;net.bytebuddy**;net.i2p**;org.apache**;" + "org.assertj**;org.bouncycastle**;org.codehaus**;org.crsh**;org.dom4j**;org.fusesource**;org.h2**;" + "org.hamcrest**;org.hibernate**;org.jboss**;org.jcp**;org.joda**;org.junit**;org.mockito**;org.objectweb**;" + "org.objenesis**;org.slf4j**;org.w3c**;org.xml**;org.yaml**;reflectasm**;rx**;org.jolokia**;" + "com.lmax**;picocli**;liquibase**;com.github.benmanes**;org.json**;org.postgresql**;nonapi.io.github.classgraph**;)" val extraJvmArguments = systemProperties.removeResolvedClasspath().map { "-D${it.key}=${it.value}" } + "-javaagent:$quasarJarPath=$excludePattern" val loggingLevel = when { logLevelOverride != null -> logLevelOverride debugPort == null -> "INFO" else -> "DEBUG" } val arguments = mutableListOf( "--base-directory=${config.corda.baseDirectory}", "--logging-level=$loggingLevel", "--no-local-shell").also { it += extraCmdLineFlag }.toList() // The following dependencies are excluded from the classpath of the created JVM, so that the environment resembles a real one as close as possible. // These are either classes that will be added as attachments to the node (i.e. samples, finance, opengamma etc.) or irrelevant testing libraries (test, corda-mock etc.). // TODO: There is pending work to fix this issue without custom blacklisting. See: https://r3-cev.atlassian.net/browse/CORDA-2164. val exclude = listOf("samples", "finance", "integrationTest", "test", "corda-mock", "com.opengamma.strata") val cp = ProcessUtilities.defaultClassPath.filterNot { cpEntry -> exclude.any { token -> cpEntry.contains("${File.separatorChar}$token") } || cpEntry.endsWith("-tests.jar") } return ProcessUtilities.startJavaProcess( className = "net.corda.node.Corda", // cannot directly get class for this, so just use string arguments = arguments, jdwpPort = debugPort, extraJvmArguments = extraJvmArguments, workingDirectory = config.corda.baseDirectory, maximumHeapSize = maximumHeapSize, classPath = cp ) } + MaxLineLength:DriverDSLImpl.kt$DriverDSLImpl.Companion${ log.info("Starting out-of-process Node ${config.corda.myLegalName.organisation}, " + "debug port is " + (debugPort ?: "not enabled")) // Write node.conf writeConfig(config.corda.baseDirectory, "node.conf", config.typesafe.toNodeOnly()) val systemProperties = mutableMapOf( "name" to config.corda.myLegalName, "visualvm.display.name" to "corda-${config.corda.myLegalName}" ) debugPort?.let { systemProperties += "log4j2.level" to "debug" systemProperties += "log4j2.debug" to "true" } systemProperties += inheritFromParentProcess() systemProperties += overriddenSystemProperties // See experimental/quasar-hook/README.md for how to generate. val excludePattern = "x(antlr**;bftsmart**;ch**;co.paralleluniverse**;com.codahale**;com.esotericsoftware**;" + "com.fasterxml**;com.google**;com.ibm**;com.intellij**;com.jcabi**;com.nhaarman**;com.opengamma**;" + "com.typesafe**;com.zaxxer**;de.javakaffee**;groovy**;groovyjarjarantlr**;groovyjarjarasm**;io.atomix**;" + "io.github**;io.netty**;jdk**;joptsimple**;junit**;kotlin**;net.bytebuddy**;net.i2p**;org.apache**;" + "org.assertj**;org.bouncycastle**;org.codehaus**;org.crsh**;org.dom4j**;org.fusesource**;org.h2**;" + "org.hamcrest**;org.hibernate**;org.jboss**;org.jcp**;org.joda**;org.junit**;org.mockito**;org.objectweb**;" + "org.objenesis**;org.slf4j**;org.w3c**;org.xml**;org.yaml**;reflectasm**;rx**;org.jolokia**;" + "com.lmax**;picocli**;liquibase**;com.github.benmanes**;org.json**;org.postgresql**;nonapi.io.github.classgraph**;)" val extraJvmArguments = systemProperties.removeResolvedClasspath().map { "-D${it.key}=${it.value}" } + "-javaagent:$quasarJarPath=$excludePattern" val loggingLevel = when { logLevelOverride != null -> logLevelOverride debugPort == null -> "INFO" else -> "DEBUG" } val arguments = mutableListOf( "--base-directory=${config.corda.baseDirectory}", "--logging-level=$loggingLevel", "--no-local-shell").also { it.addAll(extraCmdLineFlag) }.toList() // The following dependencies are excluded from the classpath of the created JVM, so that the environment resembles a real one as close as possible. // These are either classes that will be added as attachments to the node (i.e. samples, finance, opengamma etc.) or irrelevant testing libraries (test, corda-mock etc.). // TODO: There is pending work to fix this issue without custom blacklisting. See: https://r3-cev.atlassian.net/browse/CORDA-2164. val exclude = listOf("samples", "finance", "integrationTest", "test", "corda-mock", "com.opengamma.strata") val cp = ProcessUtilities.defaultClassPath.filterNot { cpEntry -> exclude.any { token -> cpEntry.contains("${File.separatorChar}$token") } || cpEntry.endsWith("-tests.jar") } return ProcessUtilities.startJavaProcess( className = "net.corda.node.Corda", // cannot directly get class for this, so just use string arguments = arguments, jdwpPort = debugPort, extraJvmArguments = extraJvmArguments, workingDirectory = config.corda.baseDirectory, maximumHeapSize = maximumHeapSize, classPath = cp, environmentVariables = environmentVariables ) } MaxLineLength:DriverDSLImpl.kt$InternalDriverDSL$ fun <A> pollUntilNonNull(pollName: String, pollInterval: Duration = DEFAULT_POLL_INTERVAL, warnCount: Int = DEFAULT_WARN_COUNT, check: () -> A?): CordaFuture<A> MaxLineLength:DriverDSLImpl.kt$InternalDriverDSL$ fun pollUntilTrue(pollName: String, pollInterval: Duration = DEFAULT_POLL_INTERVAL, warnCount: Int = DEFAULT_WARN_COUNT, check: () -> Boolean): CordaFuture<Unit> MaxLineLength:DriverDSLImpl.kt$fun DriverDSL.startNode(providedName: CordaX500Name, devMode: Boolean, parameters: NodeParameters = NodeParameters()): CordaFuture<NodeHandle> @@ -3869,7 +3862,6 @@ SpreadOperator:DemoBench.kt$DemoBench.Companion$(DemoBench::class.java, *args) SpreadOperator:DevCertificatesTest.kt$DevCertificatesTest$(*oldX509Certificates) SpreadOperator:DockerInstantiator.kt$DockerInstantiator$(*it.toTypedArray()) - SpreadOperator:DriverDSLImpl.kt$DriverDSLImpl$( config, quasarJarPath, debugPort, systemProperties, "512m", null, *extraCmdLineFlag ) SpreadOperator:DummyContract.kt$DummyContract.Companion$( /* INPUTS */ *priors.toTypedArray(), /* COMMAND */ Command(cmd, priorState.owner.owningKey), /* OUTPUT */ StateAndContract(state, PROGRAM_ID) ) SpreadOperator:DummyContract.kt$DummyContract.Companion$(*items) SpreadOperator:DummyContractV2.kt$DummyContractV2.Companion$( /* INPUTS */ *priors.toTypedArray(), /* COMMAND */ Command(cmd, priorState.owners.map { it.owningKey }), /* OUTPUT */ StateAndContract(state, DummyContractV2.PROGRAM_ID) ) diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index e7ff69555c..d0d079bbed 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -80,6 +80,9 @@ Unreleased Note that it's a responsibility of a client application to handle RPC reconnection in case this happens. See :ref:`setting_jvm_args` and :ref:`memory_usage_and_tuning` for further details. +* Environment variables and system properties can now be provided with underscore separators instead of dots. Neither are case sensitive. + See :ref:`overriding config values ` for more information. + .. _changelog_v4.1: Version 4.1 diff --git a/docs/source/corda-configuration-file.rst b/docs/source/corda-configuration-file.rst index 6cbbc141d9..e44b92b10c 100644 --- a/docs/source/corda-configuration-file.rst +++ b/docs/source/corda-configuration-file.rst @@ -39,6 +39,8 @@ To alter this behaviour, the ``on-unknown-config-keys`` command-line argument ca Overriding values from node.conf -------------------------------- +.. _corda_configuration_file_overriding_config: + Environment variables For example: ``${NODE_TRUST_STORE_PASSWORD}`` would be replaced by the contents of environment variable ``NODE_TRUST_STORE_PASSWORD`` (see: :ref:`hiding-sensitive-data` section). @@ -54,6 +56,11 @@ JVM options .. note:: If the same field is overriden by both an environment variable and system property, the system property takes precedence. +.. note:: Underscores can be used in instead of dots. For example overriding the ``p2pAddress`` with an environment variable can be done + by specifying ``CORDA_P2PADDRESS=host:port``. Variables and properties are not case sensitive. Corda will warn you if a variable + prefixed with ``CORDA`` cannot be mapped to a valid property. Shadowing occurs when two properties + of the same type with the same key are defined. For example having ``CORDA_P2PADDRESS=host:port`` and ``corda_p2paddress=host1:port1`` + will raise an exception on startup. This is to prevent hard to spot mistakes. Configuration file fields ------------------------- diff --git a/node/src/integration-test/kotlin/net/corda/node/NodeConfigParsingTests.kt b/node/src/integration-test/kotlin/net/corda/node/NodeConfigParsingTests.kt new file mode 100644 index 0000000000..42a9dc95fe --- /dev/null +++ b/node/src/integration-test/kotlin/net/corda/node/NodeConfigParsingTests.kt @@ -0,0 +1,87 @@ +package net.corda.node + +import net.corda.core.utilities.getOrThrow +import net.corda.node.logging.logFile +import net.corda.testing.driver.DriverParameters +import net.corda.testing.driver.NodeParameters +import net.corda.testing.driver.driver +import net.corda.testing.driver.internal.incrementalPortAllocation +import org.assertj.core.api.Assertions.assertThatThrownBy +import org.junit.Test +import org.junit.jupiter.api.assertThrows + +class NodeConfigParsingTests { + + @Test + fun `config is overriden by underscore variable`() { + val portAllocator = incrementalPortAllocation() + val sshPort = portAllocator.nextPort() + + driver(DriverParameters( + environmentVariables = mapOf("corda_sshd_port" to sshPort.toString()), + startNodesInProcess = false, + portAllocation = portAllocator)) { + val hasSsh = startNode().get() + .logFile() + .readLines() + .filter { it.contains("SSH server listening on port") } + .any { it.contains(sshPort.toString()) } + assert(hasSsh) + } + } + + @Test + fun `config is overriden by case insensitive underscore variable`() { + val portAllocator = incrementalPortAllocation() + val sshPort = portAllocator.nextPort() + + driver(DriverParameters( + environmentVariables = mapOf("cORDa_sShD_pOrt" to sshPort.toString()), + startNodesInProcess = false, + portAllocation = portAllocator)) { + val hasSsh = startNode().get() + .logFile() + .readLines() + .filter { it.contains("SSH server listening on port") } + .any { it.contains(sshPort.toString()) } + assert(hasSsh) + } + } + + @Test + fun `config is overriden by case insensitive dot variable`() { + val portAllocator = incrementalPortAllocation() + val sshPort = portAllocator.nextPort() + + driver(DriverParameters( + environmentVariables = mapOf("cOrda.sShD.pOrt" to sshPort.toString()), + startNodesInProcess = false, + portAllocation = portAllocator)) { + val hasSsh = startNode(NodeParameters()).get() + .logFile() + .readLines() + .filter { it.contains("SSH server listening on port") } + .any { it.contains(sshPort.toString()) } + assert(hasSsh) + } + } + + @Test + fun `shadowing is forbidden`() { + val portAllocator = incrementalPortAllocation() + val sshPort = portAllocator.nextPort() + + driver(DriverParameters( + environmentVariables = mapOf( + "cOrda_sShD_POrt" to sshPort.toString(), + "cOrda.sShD.pOrt" to sshPort.toString()), + startNodesInProcess = false, + portAllocation = portAllocator, + notarySpecs = emptyList())) { + + assertThatThrownBy { + startNode().getOrThrow() + } + } + } +} diff --git a/node/src/integration-test/kotlin/net/corda/node/NodeStartupPerformanceTests.kt b/node/src/integration-test/kotlin/net/corda/node/NodeStartupPerformanceTests.kt index a9f2586013..12c434a69d 100644 --- a/node/src/integration-test/kotlin/net/corda/node/NodeStartupPerformanceTests.kt +++ b/node/src/integration-test/kotlin/net/corda/node/NodeStartupPerformanceTests.kt @@ -24,4 +24,4 @@ class NodeStartupPerformanceTests { println(times.map { it / 1_000_000.0 }) } } -} +} \ No newline at end of file diff --git a/node/src/main/kotlin/net/corda/node/services/config/ConfigUtilities.kt b/node/src/main/kotlin/net/corda/node/services/config/ConfigUtilities.kt index 1f914c5c01..7715d64153 100644 --- a/node/src/main/kotlin/net/corda/node/services/config/ConfigUtilities.kt +++ b/node/src/main/kotlin/net/corda/node/services/config/ConfigUtilities.kt @@ -1,6 +1,7 @@ package net.corda.node.services.config import com.typesafe.config.Config +import com.typesafe.config.ConfigException import com.typesafe.config.ConfigFactory import com.typesafe.config.ConfigParseOptions import net.corda.cliutils.CordaSystemUtils @@ -8,6 +9,7 @@ import net.corda.core.identity.CordaX500Name import net.corda.core.internal.createDirectories import net.corda.core.internal.div import net.corda.core.internal.exists +import net.corda.node.internal.Node import net.corda.nodeapi.internal.DEV_CA_KEY_STORE_PASS import net.corda.nodeapi.internal.config.FileBasedCertificateStoreSupplier import net.corda.nodeapi.internal.config.MutualSslConfiguration @@ -19,7 +21,10 @@ import net.corda.nodeapi.internal.installDevNodeCaCertPath import net.corda.nodeapi.internal.loadDevCaTrustStore import net.corda.nodeapi.internal.registerDevP2pCertificates import org.slf4j.LoggerFactory +import java.lang.IllegalStateException import java.nio.file.Path +import kotlin.reflect.KClass +import kotlin.reflect.full.memberProperties fun configOf(vararg pairs: Pair): Config = ConfigFactory.parseMap(mapOf(*pairs)) operator fun Config.plus(overrides: Map): Config = ConfigFactory.parseMap(overrides).withFallback(this) @@ -62,9 +67,69 @@ object ConfigHelper { return finalConfig } + private fun getCaseSensitivePropertyPath(target : KClass?, path : List) : String { + + require(path.isNotEmpty()) { "Path to config property cannot be empty." } + + val lookFor = path.first() + target?.memberProperties?.forEach { + if (it.name.toLowerCase() == lookFor.toLowerCase()) { + return if (path.size > 1) + "${it.name}." + + getCaseSensitivePropertyPath( + (it.getter.returnType.classifier as KClass<*>), + path.subList(1, path.size)) + else + it.name + } + } + + return "" + } + + /* + * Gets + */ private fun Config.cordaEntriesOnly(): Config { - return ConfigFactory.parseMap(toProperties().filterKeys { (it as String).startsWith(CORDA_PROPERTY_PREFIX) }.mapKeys { (it.key as String).removePrefix(CORDA_PROPERTY_PREFIX) }) + val cordaPropOccurrences = mutableSetOf() + val badKeyConversions = mutableSetOf() + + return ConfigFactory.parseMap( + toProperties() + .mapKeys { + val newKey = (it.key as String) + .replace("_", ".") + .toLowerCase() + + if (newKey.contains(CORDA_PROPERTY_PREFIX) && cordaPropOccurrences.contains(newKey)) { + throw ShadowingException(it.key.toString(), newKey) + } + + cordaPropOccurrences.add(newKey) + newKey.let { key -> + if (!key.contains(CORDA_PROPERTY_PREFIX)) + return@let key + + val nodeConfKey = key.removePrefix(CORDA_PROPERTY_PREFIX) + val configPath = getCaseSensitivePropertyPath( + NodeConfigurationImpl::class, + nodeConfKey.split(".") + ) + + if (nodeConfKey.length != configPath.length) { + Node.printWarning( + "${it.key} (property or environment variable) cannot be mapped to an existing Corda" + + " config property and thus won't be used as a config override!" + + " It won't be passed as a config override! If that was the intention " + + " double check the spelling and ensure there is such config key.") + badKeyConversions.add(configPath) + } + CORDA_PROPERTY_PREFIX + configPath + } + }.filterKeys { it.startsWith(CORDA_PROPERTY_PREFIX) } + .mapKeys { it.key.removePrefix(CORDA_PROPERTY_PREFIX) } + .filterKeys { !badKeyConversions.contains(it) }) } } diff --git a/node/src/main/kotlin/net/corda/node/services/config/ShadowingException.kt b/node/src/main/kotlin/net/corda/node/services/config/ShadowingException.kt new file mode 100644 index 0000000000..4778fe4284 --- /dev/null +++ b/node/src/main/kotlin/net/corda/node/services/config/ShadowingException.kt @@ -0,0 +1,7 @@ +package net.corda.node.services.config + +import com.typesafe.config.ConfigException + +class ShadowingException(definedProperty : String, convertedProperty : String) + : ConfigException( + "Environment variable $definedProperty is shadowing another property transformed to $convertedProperty") \ No newline at end of file diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/driver/Driver.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/driver/Driver.kt index a5085085d4..79ba6b4e8d 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/driver/Driver.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/driver/Driver.kt @@ -199,7 +199,8 @@ fun driver(defaultParameters: DriverParameters = DriverParameters(), dsl: Dr networkParameters = defaultParameters.networkParameters, notaryCustomOverrides = defaultParameters.notaryCustomOverrides, inMemoryDB = defaultParameters.inMemoryDB, - cordappsForAllNodes = uncheckedCast(defaultParameters.cordappsForAllNodes) + cordappsForAllNodes = uncheckedCast(defaultParameters.cordappsForAllNodes), + environmentVariables = defaultParameters.environmentVariables ), coerce = { it }, dsl = dsl @@ -255,10 +256,46 @@ data class DriverParameters( val networkParameters: NetworkParameters = testNetworkParameters(notaries = emptyList()), val notaryCustomOverrides: Map = emptyMap(), val inMemoryDB: Boolean = true, - val cordappsForAllNodes: Collection? = null + val cordappsForAllNodes: Collection? = null, + val environmentVariables : Map = emptyMap() ) { constructor(cordappsForAllNodes: Collection) : this(isDebug = false, cordappsForAllNodes = cordappsForAllNodes) + constructor( + isDebug: Boolean = false, + driverDirectory: Path = Paths.get("build") / "node-driver" / getTimestampAsDirectoryName(), + portAllocation: PortAllocation = incrementalPortAllocation(), + debugPortAllocation: PortAllocation = incrementalPortAllocation(), + systemProperties: Map = emptyMap(), + useTestClock: Boolean = false, + startNodesInProcess: Boolean = false, + waitForAllNodesToFinish: Boolean = false, + notarySpecs: List = listOf(NotarySpec(DUMMY_NOTARY_NAME)), + extraCordappPackagesToScan: List = emptyList(), + @Suppress("DEPRECATION") jmxPolicy: JmxPolicy = JmxPolicy(), + networkParameters: NetworkParameters = testNetworkParameters(notaries = emptyList()), + notaryCustomOverrides: Map = emptyMap(), + inMemoryDB: Boolean = true, + cordappsForAllNodes: Collection? = null + ) : this( + isDebug, + driverDirectory, + portAllocation, + debugPortAllocation, + systemProperties, + useTestClock, + startNodesInProcess, + waitForAllNodesToFinish, + notarySpecs, + extraCordappPackagesToScan, + jmxPolicy, + networkParameters, + notaryCustomOverrides, + inMemoryDB, + cordappsForAllNodes, + environmentVariables = emptyMap() + ) + constructor( isDebug: Boolean = false, driverDirectory: Path = Paths.get("build") / "node-driver" / getTimestampAsDirectoryName(), @@ -373,6 +410,7 @@ data class DriverParameters( fun withNotaryCustomOverrides(notaryCustomOverrides: Map): DriverParameters = copy(notaryCustomOverrides = notaryCustomOverrides) fun withInMemoryDB(inMemoryDB: Boolean): DriverParameters = copy(inMemoryDB = inMemoryDB) fun withCordappsForAllNodes(cordappsForAllNodes: Collection?): DriverParameters = copy(cordappsForAllNodes = cordappsForAllNodes) + fun withEnvironmentVariables(variables : Map): DriverParameters = copy(environmentVariables = variables) fun copy( isDebug: Boolean, @@ -433,4 +471,40 @@ data class DriverParameters( notaryCustomOverrides = emptyMap(), cordappsForAllNodes = cordappsForAllNodes ) + + @Suppress("LongParameterList") + fun copy( + isDebug: Boolean, + driverDirectory: Path, + portAllocation: PortAllocation, + debugPortAllocation: PortAllocation, + systemProperties: Map, + useTestClock: Boolean, + startNodesInProcess: Boolean, + waitForAllNodesToFinish: Boolean, + notarySpecs: List, + extraCordappPackagesToScan: List, + jmxPolicy: JmxPolicy, + networkParameters: NetworkParameters, + notaryCustomOverrides: Map, + inMemoryDB: Boolean, + cordappsForAllNodes: Collection? + ) = this.copy( + isDebug = isDebug, + driverDirectory = driverDirectory, + portAllocation = portAllocation, + debugPortAllocation = debugPortAllocation, + systemProperties = systemProperties, + useTestClock = useTestClock, + startNodesInProcess = startNodesInProcess, + waitForAllNodesToFinish = waitForAllNodesToFinish, + notarySpecs = notarySpecs, + extraCordappPackagesToScan = extraCordappPackagesToScan, + jmxPolicy = jmxPolicy, + networkParameters = networkParameters, + notaryCustomOverrides = notaryCustomOverrides, + inMemoryDB = inMemoryDB, + cordappsForAllNodes = cordappsForAllNodes, + environmentVariables = emptyMap() + ) } \ No newline at end of file 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 6d3fa042ea..c34d07b2c4 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 @@ -91,7 +91,8 @@ class DriverDSLImpl( val networkParameters: NetworkParameters, val notaryCustomOverrides: Map, val inMemoryDB: Boolean, - val cordappsForAllNodes: Collection? + val cordappsForAllNodes: Collection?, + val environmentVariables : Map ) : InternalDriverDSL { private var _executorService: ScheduledExecutorService? = null @@ -288,9 +289,11 @@ class DriverDSLImpl( } else { startOutOfProcessMiniNode( config, - "initial-registration", - "--network-root-truststore=${rootTruststorePath.toAbsolutePath()}", - "--network-root-truststore-password=$rootTruststorePassword" + arrayOf( + "initial-registration", + "--network-root-truststore=${rootTruststorePath.toAbsolutePath()}", + "--network-root-truststore-password=$rootTruststorePassword" + ) ).map { config } } } @@ -450,7 +453,7 @@ class DriverDSLImpl( } else { // TODO The config we use here is uses a hardocded p2p port which changes when the node is run proper // This causes two node info files to be generated. - startOutOfProcessMiniNode(config, "generate-node-info").map { + startOutOfProcessMiniNode(config, arrayOf("generate-node-info")).map { // Once done we have to read the signed node info file that's been generated val nodeInfoFile = config.corda.baseDirectory.list { paths -> paths.filter { it.fileName.toString().startsWith(NodeInfoFilesCopier.NODE_INFO_FILE_NAME_PREFIX) }.findFirst().get() @@ -536,7 +539,7 @@ class DriverDSLImpl( * Start the node with the given flag which is expected to start the node for some function, which once complete will * terminate the node. */ - private fun startOutOfProcessMiniNode(config: NodeConfig, vararg extraCmdLineFlag: String): CordaFuture { + private fun startOutOfProcessMiniNode(config: NodeConfig, extraCmdLineFlag: Array = emptyArray()): CordaFuture { val debugPort = if (isDebug) debugPortAllocation.nextPort() else null val process = startOutOfProcessNode( config, @@ -545,7 +548,8 @@ class DriverDSLImpl( systemProperties, "512m", null, - *extraCmdLineFlag + environmentVariables, + extraCmdLineFlag ) return poll(executorService, "$extraCmdLineFlag (${config.corda.myLegalName})") { @@ -602,7 +606,15 @@ class DriverDSLImpl( nodeFuture } else { val debugPort = if (isDebug) debugPortAllocation.nextPort() else null - val process = startOutOfProcessNode(config, quasarJarPath, debugPort, systemProperties, parameters.maximumHeapSize, parameters.logLevelOverride) + val process = startOutOfProcessNode( + config, + quasarJarPath, + debugPort, + systemProperties, + parameters.maximumHeapSize, + parameters.logLevelOverride, + environmentVariables + ) // Destroy the child process when the parent exits.This is needed even when `waitForAllNodesToFinish` is // true because we don't want orphaned processes in the case that the parent process is terminated by the @@ -726,6 +738,7 @@ class DriverDSLImpl( } } + @Suppress("LongParameterList") private fun startOutOfProcessNode( config: NodeConfig, quasarJarPath: String, @@ -733,9 +746,11 @@ class DriverDSLImpl( overriddenSystemProperties: Map, maximumHeapSize: String, logLevelOverride: String?, - vararg extraCmdLineFlag: String - ): Process { - log.info("Starting out-of-process Node ${config.corda.myLegalName.organisation}, debug port is " + (debugPort ?: "not enabled")) + environmentVariables : Map, + extraCmdLineFlag: Array = emptyArray() + ): Process { + log.info("Starting out-of-process Node ${config.corda.myLegalName.organisation}, " + + "debug port is " + (debugPort ?: "not enabled")) // Write node.conf writeConfig(config.corda.baseDirectory, "node.conf", config.typesafe.toNodeOnly()) @@ -774,7 +789,7 @@ class DriverDSLImpl( "--base-directory=${config.corda.baseDirectory}", "--logging-level=$loggingLevel", "--no-local-shell").also { - it += extraCmdLineFlag + it.addAll(extraCmdLineFlag) }.toList() // The following dependencies are excluded from the classpath of the created JVM, so that the environment resembles a real one as close as possible. @@ -792,7 +807,8 @@ class DriverDSLImpl( extraJvmArguments = extraJvmArguments, workingDirectory = config.corda.baseDirectory, maximumHeapSize = maximumHeapSize, - classPath = cp + classPath = cp, + environmentVariables = environmentVariables ) } @@ -1013,7 +1029,8 @@ fun genericDriver( networkParameters = defaultParameters.networkParameters, notaryCustomOverrides = defaultParameters.notaryCustomOverrides, inMemoryDB = defaultParameters.inMemoryDB, - cordappsForAllNodes = uncheckedCast(defaultParameters.cordappsForAllNodes) + cordappsForAllNodes = uncheckedCast(defaultParameters.cordappsForAllNodes), + environmentVariables = defaultParameters.environmentVariables ) ) val shutdownHook = addShutdownHook(driverDsl::shutdown) @@ -1090,6 +1107,7 @@ class SplitCompatibilityZoneParams( override fun config() : NetworkServicesConfig = config } +@Suppress("LongParameterList") fun internalDriver( isDebug: Boolean = DriverParameters().isDebug, driverDirectory: Path = DriverParameters().driverDirectory, @@ -1107,6 +1125,7 @@ fun internalDriver( notaryCustomOverrides: Map = DriverParameters().notaryCustomOverrides, inMemoryDB: Boolean = DriverParameters().inMemoryDB, cordappsForAllNodes: Collection? = null, + environmentVariables: Map = emptyMap(), dsl: DriverDSLImpl.() -> A ): A { return genericDriver( @@ -1126,7 +1145,8 @@ fun internalDriver( networkParameters = networkParameters, notaryCustomOverrides = notaryCustomOverrides, inMemoryDB = inMemoryDB, - cordappsForAllNodes = cordappsForAllNodes + cordappsForAllNodes = cordappsForAllNodes, + environmentVariables = environmentVariables ), coerce = { it }, dsl = dsl diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/ProcessUtilities.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/ProcessUtilities.kt index 224d525ca4..cb87b756ff 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/ProcessUtilities.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/ProcessUtilities.kt @@ -7,17 +7,29 @@ import java.time.ZonedDateTime import java.time.format.DateTimeFormatter object ProcessUtilities { + @Suppress("LongParameterList") inline fun startJavaProcess( arguments: List, classPath: List = defaultClassPath, workingDirectory: Path? = null, jdwpPort: Int? = null, extraJvmArguments: List = emptyList(), - maximumHeapSize: String? = null + maximumHeapSize: String? = null, + environmentVariables: Map = emptyMap() ): Process { - return startJavaProcess(C::class.java.name, arguments, classPath, workingDirectory, jdwpPort, extraJvmArguments, maximumHeapSize) + return startJavaProcess( + C::class.java.name, + arguments, + classPath, + workingDirectory, + jdwpPort, + extraJvmArguments, + maximumHeapSize, + environmentVariables + ) } + @Suppress("LongParameterList") fun startJavaProcess( className: String, arguments: List, @@ -25,7 +37,8 @@ object ProcessUtilities { workingDirectory: Path? = null, jdwpPort: Int? = null, extraJvmArguments: List = emptyList(), - maximumHeapSize: String? = null + maximumHeapSize: String? = null, + environmentVariables: Map = emptyMap() ): Process { val command = mutableListOf().apply { add(javaPath) @@ -38,6 +51,7 @@ object ProcessUtilities { } return ProcessBuilder(command).apply { inheritIO() + environment().putAll(environmentVariables) environment()["CLASSPATH"] = classPath.joinToString(File.pathSeparator) if (workingDirectory != null) { // Timestamp may be handy if the same process started, killed and then re-started. Without timestamp diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/RPCDriver.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/RPCDriver.kt index c1caf61551..b8d0bda519 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/RPCDriver.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/RPCDriver.kt @@ -104,6 +104,7 @@ val fakeNodeLegalName = CordaX500Name(organisation = "Not:a:real:name", locality private val globalPortAllocation = incrementalPortAllocation() private val globalDebugPortAllocation = incrementalPortAllocation() +@Suppress("LongParameterList") fun rpcDriver( isDebug: Boolean = false, driverDirectory: Path = Paths.get("build") / "rpc-driver" / getTimestampAsDirectoryName(), @@ -121,6 +122,7 @@ fun rpcDriver( notaryCustomOverrides: Map = emptyMap(), inMemoryDB: Boolean = true, cordappsForAllNodes: Collection? = null, + environmentVariables: Map = emptyMap(), dsl: RPCDriverDSL.() -> A ): A { return genericDriver( @@ -141,7 +143,8 @@ fun rpcDriver( networkParameters = networkParameters, notaryCustomOverrides = notaryCustomOverrides, inMemoryDB = inMemoryDB, - cordappsForAllNodes = cordappsForAllNodes + cordappsForAllNodes = cordappsForAllNodes, + environmentVariables = environmentVariables ), externalTrace ), coerce = { it },