From 174ed3c64b8a5dc290e1771122f4b68d7dd69e88 Mon Sep 17 00:00:00 2001 From: Clinton Date: Wed, 14 Feb 2018 14:49:59 +0000 Subject: [PATCH] CORDA-556: Added Cordapp Config and a sample (#2469) * Added per-cordapp configuration * Added new API for Cordformation cordapp declarations to support per-cordapp configuration * Added a cordapp configuration sample --- .ci/api-current.txt | 2 +- .idea/compiler.xml | 2 + constants.properties | 4 +- .../net/corda/core/cordapp/ConfigException.kt | 6 + .../net/corda/core/cordapp/CordappConfig.kt | 70 +++++++ .../net/corda/core/cordapp/CordappContext.kt | 10 +- .../net/corda/core/internal/InternalUtils.kt | 7 + .../kotlin/net/corda/core/node/ServiceHub.kt | 6 + docs/source/changelog.rst | 13 +- docs/source/cordapp-build-systems.rst | 17 ++ gradle-plugins/cordform-common/build.gradle | 7 +- .../corda/cordform/CordformDefinition.java | 14 +- .../net/corda/cordform/CordappDependency.kt | 10 + gradle-plugins/cordformation/build.gradle | 21 ++- .../main/kotlin/net/corda/plugins/Baseform.kt | 28 +-- .../main/kotlin/net/corda/plugins/Cordapp.kt | 26 +++ .../main/kotlin/net/corda/plugins/Cordform.kt | 21 ++- .../kotlin/net/corda/plugins/Cordformation.kt | 19 +- .../src/main/kotlin/net/corda/plugins/Node.kt | 176 ++++++++++++++---- .../kotlin/net/corda/plugins/NodeRunner.kt | 40 ++-- .../kotlin/net/corda/plugins/CordformTest.kt | 77 ++++++++ .../DeploySingleNodeWithCordapp.gradle | 33 ++++ .../DeploySingleNodeWithCordappConfig.gradle | 35 ++++ ...odeWithLocallyBuildCordappAndConfig.gradle | 39 ++++ ...tachmentsClassLoaderStaticContractTests.kt | 3 +- .../internal/AttachmentsClassLoaderTests.kt | 3 +- node/capsule/build.gradle | 5 - .../node/CordappConfigFileProviderTests.kt | 60 ++++++ .../node/services/AttachmentLoadingTests.kt | 3 +- .../net/corda/node/internal/AbstractNode.kt | 3 +- .../cordapp/CordappConfigFileProvider.kt | 36 ++++ .../internal/cordapp/CordappConfigProvider.kt | 7 + .../internal/cordapp/CordappProviderImpl.kt | 19 +- .../internal/cordapp/TypesafeCordappConfig.kt | 79 ++++++++ .../cordapp/CordappProviderImplTests.kt | 40 +++- .../cordapp/TypesafeCordappConfigTests.kt | 47 +++++ .../net/corda/bank/BankOfCordaCordformTest.kt | 4 +- .../net/corda/bank/BankOfCordaCordform.kt | 4 +- samples/cordapp-configuration/README.md | 23 +++ samples/cordapp-configuration/build.gradle | 54 ++++++ samples/cordapp-configuration/src/config.conf | 5 + .../corda/configsample/ConfigSampleFlow.kt | 10 + .../net/corda/notarydemo/BFTNotaryCordform.kt | 2 +- .../main/kotlin/net/corda/notarydemo/Clean.kt | 4 +- .../corda/notarydemo/CustomNotaryCordform.kt | 2 +- .../corda/notarydemo/RaftNotaryCordform.kt | 2 +- .../corda/notarydemo/SingleNotaryCordform.kt | 2 +- samples/simm-valuation-demo/build.gradle | 8 +- settings.gradle | 1 + .../internal/demorun/CordformNodeRunner.kt | 77 ++++++++ .../node/internal/demorun/DemoRunner.kt | 57 ------ .../testing/node/MockCordappConfigProvider.kt | 17 ++ .../testing/services/MockCordappProvider.kt | 9 +- 53 files changed, 1063 insertions(+), 206 deletions(-) create mode 100644 core/src/main/kotlin/net/corda/core/cordapp/ConfigException.kt create mode 100644 core/src/main/kotlin/net/corda/core/cordapp/CordappConfig.kt create mode 100644 gradle-plugins/cordform-common/src/main/kotlin/net/corda/cordform/CordappDependency.kt create mode 100644 gradle-plugins/cordformation/src/main/kotlin/net/corda/plugins/Cordapp.kt create mode 100644 gradle-plugins/cordformation/src/test/kotlin/net/corda/plugins/CordformTest.kt create mode 100644 gradle-plugins/cordformation/src/test/resources/net/corda/plugins/DeploySingleNodeWithCordapp.gradle create mode 100644 gradle-plugins/cordformation/src/test/resources/net/corda/plugins/DeploySingleNodeWithCordappConfig.gradle create mode 100644 gradle-plugins/cordformation/src/test/resources/net/corda/plugins/DeploySingleNodeWithLocallyBuildCordappAndConfig.gradle create mode 100644 node/src/integration-test/kotlin/net/corda/node/CordappConfigFileProviderTests.kt create mode 100644 node/src/main/kotlin/net/corda/node/internal/cordapp/CordappConfigFileProvider.kt create mode 100644 node/src/main/kotlin/net/corda/node/internal/cordapp/CordappConfigProvider.kt create mode 100644 node/src/main/kotlin/net/corda/node/internal/cordapp/TypesafeCordappConfig.kt create mode 100644 node/src/test/kotlin/net/corda/node/internal/cordapp/TypesafeCordappConfigTests.kt create mode 100644 samples/cordapp-configuration/README.md create mode 100644 samples/cordapp-configuration/build.gradle create mode 100644 samples/cordapp-configuration/src/config.conf create mode 100644 samples/cordapp-configuration/src/main/kotlin/net/corda/configsample/ConfigSampleFlow.kt create mode 100644 testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/demorun/CordformNodeRunner.kt delete mode 100644 testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/demorun/DemoRunner.kt create mode 100644 testing/test-utils/src/main/kotlin/net/corda/testing/node/MockCordappConfigProvider.kt diff --git a/.ci/api-current.txt b/.ci/api-current.txt index b5cf7b32e1..ddf0e06ee7 100644 --- a/.ci/api-current.txt +++ b/.ci/api-current.txt @@ -635,7 +635,7 @@ public static final class net.corda.core.contracts.UniqueIdentifier$Companion ex @org.jetbrains.annotations.NotNull public abstract List getServices() ## public final class net.corda.core.cordapp.CordappContext extends java.lang.Object - public (net.corda.core.cordapp.Cordapp, net.corda.core.crypto.SecureHash, ClassLoader) + public (net.corda.core.cordapp.Cordapp, net.corda.core.crypto.SecureHash, ClassLoader, net.corda.core.cordapp.CordappConfig) @org.jetbrains.annotations.Nullable public final net.corda.core.crypto.SecureHash getAttachmentId() @org.jetbrains.annotations.NotNull public final ClassLoader getClassLoader() @org.jetbrains.annotations.NotNull public final net.corda.core.cordapp.Cordapp getCordapp() diff --git a/.idea/compiler.xml b/.idea/compiler.xml index 9e259d9155..64293946a1 100644 --- a/.idea/compiler.xml +++ b/.idea/compiler.xml @@ -29,6 +29,8 @@ + + diff --git a/constants.properties b/constants.properties index 930a39acd2..1c0e8aabc6 100644 --- a/constants.properties +++ b/constants.properties @@ -1,4 +1,4 @@ -gradlePluginsVersion=3.0.5 +gradlePluginsVersion=3.0.6 kotlinVersion=1.2.20 platformVersion=2 guavaVersion=21.0 @@ -6,4 +6,4 @@ bouncycastleVersion=1.57 typesafeConfigVersion=1.3.1 jsr305Version=3.0.2 artifactoryPluginVersion=4.4.18 -snakeYamlVersion=1.19 \ No newline at end of file +snakeYamlVersion=1.19 diff --git a/core/src/main/kotlin/net/corda/core/cordapp/ConfigException.kt b/core/src/main/kotlin/net/corda/core/cordapp/ConfigException.kt new file mode 100644 index 0000000000..affbce62dd --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/cordapp/ConfigException.kt @@ -0,0 +1,6 @@ +package net.corda.core.cordapp + +/** + * Thrown if an exception occurs in accessing or parsing cordapp configuration + */ +class CordappConfigException(msg: String, e: Throwable) : Exception(msg, e) \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/cordapp/CordappConfig.kt b/core/src/main/kotlin/net/corda/core/cordapp/CordappConfig.kt new file mode 100644 index 0000000000..664e69fe80 --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/cordapp/CordappConfig.kt @@ -0,0 +1,70 @@ +package net.corda.core.cordapp + +import net.corda.core.DoNotImplement + +/** + * Provides access to cordapp configuration independent of the configuration provider. + */ +@DoNotImplement +interface CordappConfig { + /** + * Check if a config exists at path + */ + fun exists(path: String): Boolean + + /** + * Get the value of the configuration at "path". + * + * @throws CordappConfigException If the configuration fails to load, parse, or find a value. + */ + fun get(path: String): Any + + /** + * Get the int value of the configuration at "path". + * + * @throws CordappConfigException If the configuration fails to load, parse, or find a value. + */ + fun getInt(path: String): Int + + /** + * Get the long value of the configuration at "path". + * + * @throws CordappConfigException If the configuration fails to load, parse, or find a value. + */ + fun getLong(path: String): Long + + /** + * Get the float value of the configuration at "path". + * + * @throws CordappConfigException If the configuration fails to load, parse, or find a value. + */ + fun getFloat(path: String): Float + + /** + * Get the double value of the configuration at "path". + * + * @throws CordappConfigException If the configuration fails to load, parse, or find a value. + */ + fun getDouble(path: String): Double + + /** + * Get the number value of the configuration at "path". + * + * @throws CordappConfigException If the configuration fails to load, parse, or find a value. + */ + fun getNumber(path: String): Number + + /** + * Get the string value of the configuration at "path". + * + * @throws CordappConfigException If the configuration fails to load, parse, or find a value. + */ + fun getString(path: String): String + + /** + * Get the boolean value of the configuration at "path". + * + * @throws CordappConfigException If the configuration fails to load, parse, or find a value. + */ + fun getBoolean(path: String): Boolean +} \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/cordapp/CordappContext.kt b/core/src/main/kotlin/net/corda/core/cordapp/CordappContext.kt index 3c7be4a3e2..b91acec452 100644 --- a/core/src/main/kotlin/net/corda/core/cordapp/CordappContext.kt +++ b/core/src/main/kotlin/net/corda/core/cordapp/CordappContext.kt @@ -2,8 +2,6 @@ package net.corda.core.cordapp import net.corda.core.crypto.SecureHash -// TODO: Add per app config - /** * An app context provides information about where an app was loaded from, access to its classloader, * and (in the included [Cordapp] object) lists of annotated classes discovered via scanning the JAR. @@ -15,5 +13,11 @@ import net.corda.core.crypto.SecureHash * @property attachmentId For CorDapps containing [Contract] or [UpgradedContract] implementations this will be populated * with the attachment containing those class files * @property classLoader the classloader used to load this cordapp's classes + * @property config Configuration for this CorDapp */ -class CordappContext(val cordapp: Cordapp, val attachmentId: SecureHash?, val classLoader: ClassLoader) +class CordappContext internal constructor( + val cordapp: Cordapp, + val attachmentId: SecureHash?, + val classLoader: ClassLoader, + val config: CordappConfig +) diff --git a/core/src/main/kotlin/net/corda/core/internal/InternalUtils.kt b/core/src/main/kotlin/net/corda/core/internal/InternalUtils.kt index 18dcced593..f47e62e6d8 100644 --- a/core/src/main/kotlin/net/corda/core/internal/InternalUtils.kt +++ b/core/src/main/kotlin/net/corda/core/internal/InternalUtils.kt @@ -2,6 +2,9 @@ package net.corda.core.internal +import net.corda.core.cordapp.Cordapp +import net.corda.core.cordapp.CordappConfig +import net.corda.core.cordapp.CordappContext import net.corda.core.cordapp.CordappProvider import net.corda.core.crypto.* import net.corda.core.identity.CordaX500Name @@ -375,3 +378,7 @@ inline fun SerializedBytes.sign(keyPair: KeyPair): SignedData { } fun ByteBuffer.copyBytes() = ByteArray(remaining()).also { get(it) } + +fun createCordappContext(cordapp: Cordapp, attachmentId: SecureHash?, classLoader: ClassLoader, config: CordappConfig): CordappContext { + return CordappContext(cordapp, attachmentId, classLoader, config) +} \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/node/ServiceHub.kt b/core/src/main/kotlin/net/corda/core/node/ServiceHub.kt index 1832ac1cb5..2c31eb96f1 100644 --- a/core/src/main/kotlin/net/corda/core/node/ServiceHub.kt +++ b/core/src/main/kotlin/net/corda/core/node/ServiceHub.kt @@ -2,6 +2,7 @@ package net.corda.core.node import net.corda.core.DoNotImplement import net.corda.core.contracts.* +import net.corda.core.cordapp.CordappContext import net.corda.core.cordapp.CordappProvider import net.corda.core.crypto.Crypto import net.corda.core.crypto.SignableData @@ -369,4 +370,9 @@ interface ServiceHub : ServicesForResolution { * node starts. */ fun registerUnloadHandler(runOnStop: () -> Unit) + + /** + * See [CordappProvider.getAppContext] + */ + fun getAppContext(): CordappContext = cordappProvider.getAppContext() } diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index 8bac1b49d6..816bba7ec9 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -7,6 +7,10 @@ from the previous milestone release. UNRELEASED ---------- +* Per CorDapp configuration is now exposed. ``CordappContext`` now exposes a ``CordappConfig`` object that is populated +at CorDapp context creation time from a file source during runtime. + + * Introduced Flow Draining mode, in which a node continues executing existing flows, but does not start new. This is to support graceful node shutdown/restarts. In particular, when this mode is on, new flows through RPC will be rejected, scheduled flows will be ignored, and initial session messages will not be consumed. This will ensure that the number of checkpoints will strictly diminish with time, allowing for a clean shutdown. @@ -188,18 +192,9 @@ UNRELEASED * Marked ``stateMachine`` on ``FlowLogic`` as ``CordaInternal`` to make clear that is it not part of the public api and is only for internal use -* Provided experimental support for specifying your own webserver to be used instead of the default development - webserver in ``Cordform`` using the ``webserverJar`` argument - * Created new ``StartedMockNode`` and ``UnstartedMockNode`` classes which are wrappers around our MockNode implementation that expose relevant methods for testing without exposing internals, create these using a ``MockNetwork``. -* The test utils in ``Expect.kt``, ``SerializationTestHelpers.kt``, ``TestConstants.kt`` and ``TestUtils.kt`` have moved - from the ``net.corda.testing`` package to the ``net.corda.testing.core`` package, and ``FlowStackSnapshot.kt`` has moved to the - ``net.corda.testing.services`` package. Moving items out of the ``net.corda.testing.*`` package will help make it clearer which - parts of the api are stable. The bash script ``tools\scripts\update-test-packages.sh`` can be used to smooth the upgrade - process for existing projects. - .. _changelog_v1: Release 1.0 diff --git a/docs/source/cordapp-build-systems.rst b/docs/source/cordapp-build-systems.rst index 3900680d81..2de5cca3c7 100644 --- a/docs/source/cordapp-build-systems.rst +++ b/docs/source/cordapp-build-systems.rst @@ -159,3 +159,20 @@ Installing the CorDapp JAR At runtime, nodes will load any CorDapps present in their ``cordapps`` folder. Therefore in order to install a CorDapp on a node, the CorDapp JAR must be added to the ``/cordapps/`` folder, where ``node_dir`` is the folder in which the node's JAR and configuration files are stored. + +CorDapp configuration files +--------------------------- + +CorDapp configuration files should be placed in ``/cordapps/config``. The name of the file should match the +name of the JAR of the CorDapp (eg; if your CorDapp is called ``hello-0.1.jar`` the config should be ``config/hello-0.1.conf``). + +Config files are currently only available in the `Typesafe/Lightbend `_ config format. +These files are loaded when a CorDapp context is created and so can change during runtime. + +CorDapp configuration can be accessed from ``CordappContext::config`` whenever a ``CordappContext`` is available. + +There is an example project that demonstrates in ``samples` called ``cordapp-configuration`` and API documentation in +`_. + + + diff --git a/gradle-plugins/cordform-common/build.gradle b/gradle-plugins/cordform-common/build.gradle index be2fa0cf16..aca55ab2b4 100644 --- a/gradle-plugins/cordform-common/build.gradle +++ b/gradle-plugins/cordform-common/build.gradle @@ -1,4 +1,5 @@ apply plugin: 'java' +apply plugin: 'kotlin' apply plugin: 'maven-publish' apply plugin: 'net.corda.plugins.publish-utils' apply plugin: 'com.jfrog.artifactory' @@ -7,11 +8,9 @@ repositories { mavenCentral() } -// This tracks the gradle plugins version and not Corda -version gradle_plugins_version -group 'net.corda.plugins' - dependencies { + compile "org.jetbrains.kotlin:kotlin-stdlib-jre8:$kotlin_version" + // JSR 305: Nullability annotations compile "com.google.code.findbugs:jsr305:$jsr305_version" diff --git a/gradle-plugins/cordform-common/src/main/java/net/corda/cordform/CordformDefinition.java b/gradle-plugins/cordform-common/src/main/java/net/corda/cordform/CordformDefinition.java index fc62b1bbee..de4601d005 100644 --- a/gradle-plugins/cordform-common/src/main/java/net/corda/cordform/CordformDefinition.java +++ b/gradle-plugins/cordform-common/src/main/java/net/corda/cordform/CordformDefinition.java @@ -10,7 +10,12 @@ import java.util.function.Consumer; public abstract class CordformDefinition { private Path nodesDirectory = Paths.get("build", "nodes"); private final List> nodeConfigurers = new ArrayList<>(); - private final List cordappPackages = new ArrayList<>(); + /** + * A list of Cordapp maven coordinates and project name + * + * If maven coordinates are set project name is ignored + */ + private final List cordappDeps = new ArrayList<>(); public Path getNodesDirectory() { return nodesDirectory; @@ -28,8 +33,11 @@ public abstract class CordformDefinition { nodeConfigurers.add(configurer); } - public List getCordappPackages() { - return cordappPackages; + /** + * Cordapp maven coordinates or project names (ie; net.corda:finance:0.1 or ":finance") to scan for when resolving cordapp JARs + */ + public List getCordappDependencies() { + return cordappDeps; } /** diff --git a/gradle-plugins/cordform-common/src/main/kotlin/net/corda/cordform/CordappDependency.kt b/gradle-plugins/cordform-common/src/main/kotlin/net/corda/cordform/CordappDependency.kt new file mode 100644 index 0000000000..f677e2278c --- /dev/null +++ b/gradle-plugins/cordform-common/src/main/kotlin/net/corda/cordform/CordappDependency.kt @@ -0,0 +1,10 @@ +package net.corda.cordform + +data class CordappDependency( + val mavenCoordinates: String? = null, + val projectName: String? = null +) { + init { + require((mavenCoordinates != null) != (projectName != null), { "Only one of maven coordinates or project name must be set" }) + } +} \ No newline at end of file diff --git a/gradle-plugins/cordformation/build.gradle b/gradle-plugins/cordformation/build.gradle index 5ac1e33473..f68adb96ad 100644 --- a/gradle-plugins/cordformation/build.gradle +++ b/gradle-plugins/cordformation/build.gradle @@ -9,6 +9,7 @@ buildscript { } apply plugin: 'kotlin' +apply plugin: 'java-gradle-plugin' apply plugin: 'net.corda.plugins.publish-utils' apply plugin: 'com.jfrog.artifactory' @@ -33,18 +34,22 @@ sourceSets { } dependencies { - compile gradleApi() + gradleApi() + compile project(":cordapp") + compile project(':cordform-common') compile "org.jetbrains.kotlin:kotlin-stdlib-jre8:$kotlin_version" + compile "commons-io:commons-io:2.6" noderunner "org.jetbrains.kotlin:kotlin-stdlib-jre8:$kotlin_version" - compile project(':cordform-common') + testCompile "junit:junit:4.12" // TODO: Unify with core + testCompile "org.assertj:assertj-core:3.8.0" // Docker-compose file generation compile "org.yaml:snakeyaml:$snake_yaml_version" } -task createNodeRunner(type: Jar, dependsOn: [classes]) { +task createNodeRunner(type: Jar) { manifest { attributes('Main-Class': 'net.corda.plugins.NodeRunnerKt') } @@ -53,12 +58,12 @@ task createNodeRunner(type: Jar, dependsOn: [classes]) { from sourceSets.runnodes.output } -jar { +publish { + name project.name +} + +processResources { from(createNodeRunner) { rename { 'net/corda/plugins/runnodes.jar' } } } - -publish { - name project.name -} diff --git a/gradle-plugins/cordformation/src/main/kotlin/net/corda/plugins/Baseform.kt b/gradle-plugins/cordformation/src/main/kotlin/net/corda/plugins/Baseform.kt index ea528aaf08..010ee69e5c 100644 --- a/gradle-plugins/cordformation/src/main/kotlin/net/corda/plugins/Baseform.kt +++ b/gradle-plugins/cordformation/src/main/kotlin/net/corda/plugins/Baseform.kt @@ -2,11 +2,9 @@ package net.corda.plugins import groovy.lang.Closure import net.corda.cordform.CordformDefinition -import org.apache.tools.ant.filters.FixCrLfFilter import org.gradle.api.DefaultTask import org.gradle.api.plugins.JavaPluginConvention import org.gradle.api.tasks.SourceSet.MAIN_SOURCE_SET_NAME -import org.gradle.api.tasks.TaskAction import java.io.File import java.lang.reflect.InvocationTargetException import java.net.URLClassLoader @@ -111,20 +109,27 @@ open class Baseform : DefaultTask() { } } - protected fun initializeConfiguration() { + internal fun initializeConfiguration() { if (definitionClass != null) { val cd = loadCordformDefinition() // If the user has specified their own directory (even if it's the same default path) then let them know // it's not used and should just rely on the one in CordformDefinition require(directory === defaultDirectory) { + project.logger.info("User has used '$directory', default directory is '${defaultDirectory}'") "'directory' cannot be used when 'definitionClass' is specified. Use CordformDefinition.nodesDirectory instead." } directory = cd.nodesDirectory - val cordapps = cd.getMatchingCordapps() + val cordapps = cd.cordappDependencies cd.nodeConfigurers.forEach { val node = node { } it.accept(node) - node.additionalCordapps.addAll(cordapps) + cordapps.forEach { + if (it.mavenCoordinates != null) { + node.cordapp(project.project(it.mavenCoordinates!!)) + } else { + node.cordapp(it.projectName!!) + } + } node.rootDir(directory) } cd.setup { nodeName -> project.projectDir.toPath().resolve(getNodeByName(nodeName)!!.nodeDir.toPath()) } @@ -134,7 +139,6 @@ open class Baseform : DefaultTask() { } } } - protected fun bootstrapNetwork() { val networkBootstrapperClass = loadNetworkBootstrapperClass() val networkBootstrapper = networkBootstrapperClass.newInstance() @@ -148,18 +152,6 @@ open class Baseform : DefaultTask() { } } - private fun CordformDefinition.getMatchingCordapps(): List { - val cordappJars = project.configuration("cordapp").files - return cordappPackages.map { `package` -> - val cordappsWithPackage = cordappJars.filter { it.containsPackage(`package`) } - when (cordappsWithPackage.size) { - 0 -> throw IllegalArgumentException("There are no cordapp dependencies containing the package $`package`") - 1 -> cordappsWithPackage[0] - else -> throw IllegalArgumentException("More than one cordapp dependency contains the package $`package`: $cordappsWithPackage") - } - } - } - private fun File.containsPackage(`package`: String): Boolean { JarInputStream(inputStream()).use { while (true) { diff --git a/gradle-plugins/cordformation/src/main/kotlin/net/corda/plugins/Cordapp.kt b/gradle-plugins/cordformation/src/main/kotlin/net/corda/plugins/Cordapp.kt new file mode 100644 index 0000000000..a4cba09969 --- /dev/null +++ b/gradle-plugins/cordformation/src/main/kotlin/net/corda/plugins/Cordapp.kt @@ -0,0 +1,26 @@ +package net.corda.plugins + +import org.gradle.api.Project +import java.io.File + +open class Cordapp private constructor(val coordinates: String?, val project: Project?) { + constructor(coordinates: String) : this(coordinates, null) + constructor(cordappProject: Project) : this(null, cordappProject) + + // The configuration text that will be written + internal var config: String? = null + + /** + * Set the configuration text that will be written to the cordapp's configuration file + */ + fun config(config: String) { + this.config = config + } + + /** + * Reads config from the file and later writes it to the cordapp's configuration file + */ + fun config(configFile: File) { + this.config = configFile.readText() + } +} \ No newline at end of file diff --git a/gradle-plugins/cordformation/src/main/kotlin/net/corda/plugins/Cordform.kt b/gradle-plugins/cordformation/src/main/kotlin/net/corda/plugins/Cordform.kt index f207dc1818..74f9a02664 100644 --- a/gradle-plugins/cordformation/src/main/kotlin/net/corda/plugins/Cordform.kt +++ b/gradle-plugins/cordformation/src/main/kotlin/net/corda/plugins/Cordform.kt @@ -1,9 +1,6 @@ package net.corda.plugins import org.apache.tools.ant.filters.FixCrLfFilter -import org.gradle.api.DefaultTask -import org.gradle.api.plugins.JavaPluginConvention -import org.gradle.api.tasks.SourceSet.MAIN_SOURCE_SET_NAME import org.gradle.api.tasks.TaskAction import java.nio.file.Path import java.nio.file.Paths @@ -15,18 +12,25 @@ import java.nio.file.Paths */ @Suppress("unused") open class Cordform : Baseform() { - private companion object { + internal companion object { val nodeJarName = "corda.jar" - private val defaultDirectory: Path = Paths.get("build", "nodes") } + /** + * Returns a node by name. + * + * @param name The name of the node as specified in the node configuration DSL. + * @return A node instance. + */ + private fun getNodeByName(name: String): Node? = nodes.firstOrNull { it.name == name } + /** * Installs the run script into the nodes directory. */ private fun installRunScript() { project.copy { it.apply { - from(Cordformation.getPluginFile(project, "net/corda/plugins/runnodes.jar")) + from(Cordformation.getPluginFile(project, "runnodes.jar")) fileMode = Cordformation.executableFileMode into("$directory/") } @@ -34,7 +38,7 @@ open class Cordform : Baseform() { project.copy { it.apply { - from(Cordformation.getPluginFile(project, "net/corda/plugins/runnodes")) + from(Cordformation.getPluginFile(project, "runnodes")) // Replaces end of line with lf to avoid issues with the bash interpreter and Windows style line endings. filter(mapOf("eol" to FixCrLfFilter.CrLf.newInstance("lf")), FixCrLfFilter::class.java) fileMode = Cordformation.executableFileMode @@ -44,7 +48,7 @@ open class Cordform : Baseform() { project.copy { it.apply { - from(Cordformation.getPluginFile(project, "net/corda/plugins/runnodes.bat")) + from(Cordformation.getPluginFile(project, "runnodes.bat")) into("$directory/") } } @@ -63,4 +67,5 @@ open class Cordform : Baseform() { bootstrapNetwork() nodes.forEach(Node::build) } + } diff --git a/gradle-plugins/cordformation/src/main/kotlin/net/corda/plugins/Cordformation.kt b/gradle-plugins/cordformation/src/main/kotlin/net/corda/plugins/Cordformation.kt index 43444ca270..644116e35b 100644 --- a/gradle-plugins/cordformation/src/main/kotlin/net/corda/plugins/Cordformation.kt +++ b/gradle-plugins/cordformation/src/main/kotlin/net/corda/plugins/Cordformation.kt @@ -3,6 +3,7 @@ package net.corda.plugins import org.gradle.api.Plugin import org.gradle.api.Project import java.io.File +import java.io.InputStream /** * The Cordformation plugin deploys nodes to a directory in a state ready to be used by a developer for experimentation, @@ -13,19 +14,19 @@ class Cordformation : Plugin { const val CORDFORMATION_TYPE = "cordformationInternal" /** - * Gets a resource file from this plugin's JAR file. + * Gets a resource file from this plugin's JAR file by creating an intermediate tmp dir * - * @param project The project environment this plugin executes in. * @param filePathInJar The file in the JAR, relative to root, you wish to access. * @return A file handle to the file in the JAR. */ fun getPluginFile(project: Project, filePathInJar: String): File { - val archive = project.rootProject.buildscript.configurations - .single { it.name == "classpath" } - .first { it.name.contains("cordformation") } - return project.rootProject.resources.text - .fromArchiveEntry(archive, filePathInJar) - .asFile() + val tmpDir = File(project.buildDir, "tmp") + val outputFile = File(tmpDir, filePathInJar) + tmpDir.mkdir() + outputFile.outputStream().use { + Cordformation::class.java.getResourceAsStream(filePathInJar).copyTo(it) + } + return outputFile } /** @@ -38,7 +39,7 @@ class Cordformation : Plugin { fun verifyAndGetRuntimeJar(project: Project, jarName: String): File { val releaseVersion = project.rootProject.ext("corda_release_version") val maybeJar = project.configuration("runtime").filter { - "$jarName-$releaseVersion.jar" in it.toString() || "$jarName-enterprise-$releaseVersion.jar" in it.toString() + "$jarName-$releaseVersion.jar" in it.toString() || "$jarName-r3-$releaseVersion.jar" in it.toString() } if (maybeJar.isEmpty) { throw IllegalStateException("No $jarName JAR found. Have you deployed the Corda project to Maven? Looked for \"$jarName-$releaseVersion.jar\"") diff --git a/gradle-plugins/cordformation/src/main/kotlin/net/corda/plugins/Node.kt b/gradle-plugins/cordformation/src/main/kotlin/net/corda/plugins/Node.kt index 10c408cc71..0086854162 100644 --- a/gradle-plugins/cordformation/src/main/kotlin/net/corda/plugins/Node.kt +++ b/gradle-plugins/cordformation/src/main/kotlin/net/corda/plugins/Node.kt @@ -1,22 +1,28 @@ package net.corda.plugins import com.typesafe.config.ConfigFactory +import com.typesafe.config.ConfigObject import com.typesafe.config.ConfigRenderOptions import com.typesafe.config.ConfigValueFactory -import com.typesafe.config.ConfigObject import groovy.lang.Closure import net.corda.cordform.CordformNode import net.corda.cordform.RpcSettings +import org.apache.commons.io.FilenameUtils +import org.gradle.api.GradleException import org.gradle.api.Project +import org.gradle.api.artifacts.ProjectDependency import java.io.File import java.nio.charset.StandardCharsets import java.nio.file.Files import java.nio.file.Path +import javax.inject.Inject /** * Represents a node that will be installed. */ -class Node(private val project: Project) : CordformNode() { +open class Node @Inject constructor(private val project: Project) : CordformNode() { + private data class ResolvedCordapp(val jarFile: File, val config: String?) + companion object { @JvmStatic val webJarName = "corda-webserver.jar" @@ -30,8 +36,17 @@ class Node(private val project: Project) : CordformNode() { * @note Your app will be installed by default and does not need to be included here. * @note Type is any due to gradle's use of "GStrings" - each value will have "toString" called on it */ - var cordapps = mutableListOf() - internal var additionalCordapps = mutableListOf() + var cordapps: MutableList + get() = internalCordapps as MutableList + @Deprecated("Use cordapp instead - setter will be removed by Corda V4.0") + set(value) { + value.forEach { + cordapp(it.toString()) + } + } + + private val internalCordapps = mutableListOf() + private val builtCordapp = Cordapp(project) internal lateinit var nodeDir: File private set internal lateinit var rootDir: File @@ -76,8 +91,83 @@ class Node(private val project: Project) : CordformNode() { * * @param sshdPort The port for SSH server to listen on */ - fun sshdPort(sshdPort: Int?) { - config = config.withValue("sshd.port", ConfigValueFactory.fromAnyRef(sshdPort)) + fun sshdPort(sshdPort: Int) { + config = config.withValue("sshdAddress", + ConfigValueFactory.fromAnyRef("$DEFAULT_HOST:$sshdPort")) + } + + /** + * Install a cordapp to this node + * + * @param coordinates The coordinates of the [Cordapp] + * @param configureClosure A groovy closure to configure a [Cordapp] object + * @return The created and inserted [Cordapp] + */ + fun cordapp(coordinates: String, configureClosure: Closure): Cordapp { + val cordapp = project.configure(Cordapp(coordinates), configureClosure) as Cordapp + internalCordapps += cordapp + return cordapp + } + + /** + * Install a cordapp to this node + * + * @param cordappProject A project that produces a cordapp JAR + * @param configureClosure A groovy closure to configure a [Cordapp] object + * @return The created and inserted [Cordapp] + */ + fun cordapp(cordappProject: Project, configureClosure: Closure): Cordapp { + val cordapp = project.configure(Cordapp(cordappProject), configureClosure) as Cordapp + internalCordapps += cordapp + return cordapp + } + + /** + * Install a cordapp to this node + * + * @param cordappProject A project that produces a cordapp JAR + * @return The created and inserted [Cordapp] + */ + fun cordapp(cordappProject: Project): Cordapp { + return Cordapp(cordappProject).apply { + internalCordapps += this + } + } + + /** + * Install a cordapp to this node + * + * @param coordinates The coordinates of the [Cordapp] + * @return The created and inserted [Cordapp] + */ + fun cordapp(coordinates: String): Cordapp { + return Cordapp(coordinates).apply { + internalCordapps += this + } + } + + /** + * Install a cordapp to this node + * + * @param configureFunc A lambda to configure a [Cordapp] object + * @return The created and inserted [Cordapp] + */ + fun cordapp(coordinates: String, configureFunc: Cordapp.() -> Unit): Cordapp { + return Cordapp(coordinates).apply { + configureFunc() + internalCordapps += this + } + } + + /** + * Configures the default cordapp automatically added to this node from this project + * + * @param configureClosure A groovy closure to configure a [Cordapp] object + * @return The created and inserted [Cordapp] + */ + fun projectCordapp(configureClosure: Closure): Cordapp { + project.configure(builtCordapp, configureClosure) as Cordapp + return builtCordapp } /** @@ -96,8 +186,8 @@ class Node(private val project: Project) : CordformNode() { installWebserverJar() } installAgentJar() - installBuiltCordapp() installCordapps() + installConfig() } internal fun buildDocker() { @@ -109,7 +199,6 @@ class Node(private val project: Project) : CordformNode() { } } installAgentJar() - installBuiltCordapp() installCordapps() } @@ -160,19 +249,6 @@ class Node(private val project: Project) : CordformNode() { } } - /** - * Installs this project's cordapp to this directory. - */ - private fun installBuiltCordapp() { - val cordappsDir = File(nodeDir, "cordapps") - project.copy { - it.apply { - from(project.tasks.getByName("jar")) - into(cordappsDir) - } - } - } - /** * Installs the jolokia monitoring agent JAR to the node/drivers directory */ @@ -197,6 +273,14 @@ class Node(private val project: Project) : CordformNode() { } } + private fun installCordappConfigs(cordapps: Collection) { + val cordappsDir = project.file(File(nodeDir, "cordapps")) + cordappsDir.mkdirs() + cordapps.filter { it.config != null } + .map { Pair("${FilenameUtils.removeExtension(it.jarFile.name)}.conf", it.config!!) } + .forEach { project.file(File(cordappsDir, it.first)).writeText(it.second) } + } + private fun createTempConfigFile(configObject: ConfigObject): File { val options = ConfigRenderOptions .defaults() @@ -217,7 +301,7 @@ class Node(private val project: Project) : CordformNode() { /** * Installs the configuration file to the root directory and detokenises it. */ - internal fun installConfig() { + fun installConfig() { configureProperties() val tmpConfFile = createTempConfigFile(config.root()) appendOptionalConfig(tmpConfFile) @@ -269,31 +353,57 @@ class Node(private val project: Project) : CordformNode() { } } + /** - * Installs other cordapps to this node's cordapps directory. + * Installs the jolokia monitoring agent JAR to the node/drivers directory */ - internal fun installCordapps() { - additionalCordapps.addAll(getCordappList()) + private fun installCordapps() { + val cordapps = getCordappList() val cordappsDir = File(nodeDir, "cordapps") project.copy { it.apply { - from(additionalCordapps) - into(cordappsDir) + from(cordapps.map { it.jarFile }) + into(project.file(cordappsDir)) } } + + installCordappConfigs(cordapps) } + /** * Gets a list of cordapps based on what dependent cordapps were specified. * * @return List of this node's cordapps. */ - private fun getCordappList(): Collection { - // Cordapps can sometimes contain a GString instance which fails the equality test with the Java string - @Suppress("RemoveRedundantCallsOfConversionMethods") - val cordapps: List = cordapps.map { it.toString() } - return project.configuration("cordapp").files { - cordapps.contains(it.group + ":" + it.name + ":" + it.version) + private fun getCordappList(): Collection = + internalCordapps.map { cordapp -> resolveCordapp(cordapp) } + resolveBuiltCordapp() + + private fun resolveCordapp(cordapp: Cordapp): ResolvedCordapp { + val cordappConfiguration = project.configuration("cordapp") + val cordappName = if (cordapp.project != null) cordapp.project.name else cordapp.coordinates + val cordappFile = cordappConfiguration.files { + when { + (it is ProjectDependency) && (cordapp.project != null) -> it.dependencyProject == cordapp.project + cordapp.coordinates != null -> { + // Cordapps can sometimes contain a GString instance which fails the equality test with the Java string + @Suppress("RemoveRedundantCallsOfConversionMethods") + val coordinates = cordapp.coordinates.toString() + coordinates == (it.group + ":" + it.name + ":" + it.version) + } + else -> false + } + } + + return when { + cordappFile.size == 0 -> throw GradleException("Cordapp $cordappName not found in cordapps configuration.") + cordappFile.size > 1 -> throw GradleException("Multiple files found for $cordappName") + else -> ResolvedCordapp(cordappFile.single(), cordapp.config) } } + + private fun resolveBuiltCordapp(): ResolvedCordapp { + val projectCordappFile = project.tasks.getByName("jar").outputs.files.singleFile + return ResolvedCordapp(projectCordappFile, builtCordapp.config) + } } diff --git a/gradle-plugins/cordformation/src/noderunner/kotlin/net/corda/plugins/NodeRunner.kt b/gradle-plugins/cordformation/src/noderunner/kotlin/net/corda/plugins/NodeRunner.kt index bb308a8191..cffa06300f 100644 --- a/gradle-plugins/cordformation/src/noderunner/kotlin/net/corda/plugins/NodeRunner.kt +++ b/gradle-plugins/cordformation/src/noderunner/kotlin/net/corda/plugins/NodeRunner.kt @@ -109,35 +109,43 @@ private abstract class JavaCommand( private class HeadlessJavaCommand(jarName: String, dir: File, debugPort: Int?, monitoringPort: Int?, args: List, jvmArgs: List) : JavaCommand(jarName, dir, debugPort, monitoringPort, dir.name, { add("--no-local-shell") }, args, jvmArgs) { - override fun processBuilder() = ProcessBuilder(command).redirectError(File("error.$nodeName.log")).inheritIO() + override fun processBuilder(): ProcessBuilder { + println("Running command: ${command.joinToString(" ")}") + return ProcessBuilder(command).redirectError(File("error.$nodeName.log")).inheritIO() + } + override fun getJavaPath() = File(File(System.getProperty("java.home"), "bin"), "java").path } private class TerminalWindowJavaCommand(jarName: String, dir: File, debugPort: Int?, monitoringPort: Int?, args: List, jvmArgs: List) : JavaCommand(jarName, dir, debugPort, monitoringPort, "${dir.name}-$jarName", {}, args, jvmArgs) { - override fun processBuilder() = ProcessBuilder(when (os) { - OS.MACOS -> { - listOf("osascript", "-e", """tell app "Terminal" + override fun processBuilder(): ProcessBuilder { + val params = when (os) { + OS.MACOS -> { + listOf("osascript", "-e", """tell app "Terminal" activate delay 0.5 tell app "System Events" to tell process "Terminal" to keystroke "t" using command down delay 0.5 do script "bash -c 'cd \"$dir\" ; \"${command.joinToString("""\" \"""")}\" && exit'" in selected tab of the front window end tell""") - } - OS.WINDOWS -> { - listOf("cmd", "/C", "start ${command.joinToString(" ") { windowsSpaceEscape(it) }}") - } - OS.LINUX -> { - // Start shell to keep window open unless java terminated normally or due to SIGTERM: - val command = "${unixCommand()}; [ $? -eq 0 -o $? -eq 143 ] || sh" - if (isTmux()) { - listOf("tmux", "new-window", "-n", nodeName, command) - } else { - listOf("xterm", "-T", nodeName, "-e", command) + } + OS.WINDOWS -> { + listOf("cmd", "/C", "start ${command.joinToString(" ") { windowsSpaceEscape(it) }}") + } + OS.LINUX -> { + // Start shell to keep window open unless java terminated normally or due to SIGTERM: + val command = "${unixCommand()}; [ $? -eq 0 -o $? -eq 143 ] || sh" + if (isTmux()) { + listOf("tmux", "new-window", "-n", nodeName, command) + } else { + listOf("xterm", "-T", nodeName, "-e", command) + } } } - }) + println("Running command: ${params.joinToString(" ")}") + return ProcessBuilder(params) + } private fun unixCommand() = command.map(::quotedFormOf).joinToString(" ") override fun getJavaPath(): String = File(File(System.getProperty("java.home"), "bin"), "java").path diff --git a/gradle-plugins/cordformation/src/test/kotlin/net/corda/plugins/CordformTest.kt b/gradle-plugins/cordformation/src/test/kotlin/net/corda/plugins/CordformTest.kt new file mode 100644 index 0000000000..3f934191be --- /dev/null +++ b/gradle-plugins/cordformation/src/test/kotlin/net/corda/plugins/CordformTest.kt @@ -0,0 +1,77 @@ +package net.corda.plugins + +import org.apache.commons.io.FileUtils +import org.apache.commons.io.IOUtils +import org.assertj.core.api.Assertions.* +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TemporaryFolder +import java.io.File +import java.io.IOException +import java.nio.charset.StandardCharsets +import java.nio.file.Files +import org.gradle.testkit.runner.GradleRunner +import org.gradle.testkit.runner.TaskOutcome + +class CordformTest { + @Rule + @JvmField + val testProjectDir = TemporaryFolder() + private var buildFile: File? = null + + private companion object { + val cordaFinanceJarName = "corda-finance-3.0-SNAPSHOT" + val localCordappJarName = "locally-built-cordapp" + val notaryNodeName = "Notary Service" + } + + @Before + fun setup() { + buildFile = testProjectDir.newFile("build.gradle") + } + + @Test + fun `a node with cordapp dependency`() { + val runner = getStandardGradleRunnerFor("DeploySingleNodeWithCordapp.gradle") + + val result = runner.build() + + assertThat(result.task(":deployNodes")!!.outcome).isEqualTo(TaskOutcome.SUCCESS) + assertThat(getNodeCordappJar(notaryNodeName, cordaFinanceJarName)).exists() + } + + @Test + fun `deploy a node with cordapp config`() { + val runner = getStandardGradleRunnerFor("DeploySingleNodeWithCordappConfig.gradle") + + val result = runner.build() + + assertThat(result.task(":deployNodes")!!.outcome).isEqualTo(TaskOutcome.SUCCESS) + assertThat(getNodeCordappJar(notaryNodeName, cordaFinanceJarName)).exists() + assertThat(getNodeCordappConfig(notaryNodeName, cordaFinanceJarName)).exists() + } + + @Test + fun `deploy the locally built cordapp with cordapp config`() { + val runner = getStandardGradleRunnerFor("DeploySingleNodeWithLocallyBuildCordappAndConfig.gradle") + + val result = runner.build() + + assertThat(result.task(":deployNodes")!!.outcome).isEqualTo(TaskOutcome.SUCCESS) + assertThat(getNodeCordappJar(notaryNodeName, localCordappJarName)).exists() + assertThat(getNodeCordappConfig(notaryNodeName, localCordappJarName)).exists() + } + + private fun getStandardGradleRunnerFor(buildFileResourceName: String): GradleRunner { + createBuildFile(buildFileResourceName) + return GradleRunner.create() + .withProjectDir(testProjectDir.root) + .withArguments("deployNodes", "-s") + .withPluginClasspath() + } + + private fun createBuildFile(buildFileResourceName: String) = IOUtils.copy(javaClass.getResourceAsStream(buildFileResourceName), buildFile!!.outputStream()) + private fun getNodeCordappJar(nodeName: String, cordappJarName: String) = File(testProjectDir.root, "build/nodes/$nodeName/cordapps/$cordappJarName.jar") + private fun getNodeCordappConfig(nodeName: String, cordappJarName: String) = File(testProjectDir.root, "build/nodes/$nodeName/cordapps/$cordappJarName.conf") +} \ No newline at end of file diff --git a/gradle-plugins/cordformation/src/test/resources/net/corda/plugins/DeploySingleNodeWithCordapp.gradle b/gradle-plugins/cordformation/src/test/resources/net/corda/plugins/DeploySingleNodeWithCordapp.gradle new file mode 100644 index 0000000000..10eca84d66 --- /dev/null +++ b/gradle-plugins/cordformation/src/test/resources/net/corda/plugins/DeploySingleNodeWithCordapp.gradle @@ -0,0 +1,33 @@ +buildscript { + ext { + corda_group = 'net.corda' + corda_release_version = '3.0-SNAPSHOT' // TODO: Set to 3.0.0 when Corda 3 is released + jolokia_version = '1.3.7' + } +} + +plugins { + id 'java' + id 'net.corda.plugins.cordformation' +} + +repositories { + mavenCentral() + maven { url 'https://ci-artifactory.corda.r3cev.com/artifactory/corda-dev' } +} + +dependencies { + runtime "$corda_group:corda:$corda_release_version" + runtime "$corda_group:corda-node-api:$corda_release_version" + cordapp "$corda_group:corda-finance:$corda_release_version" +} + +task deployNodes(type: net.corda.plugins.Cordform) { + node { + name 'O=Notary Service,L=Zurich,C=CH' + notary = [validating : true] + p2pPort 10002 + rpcPort 10003 + cordapps = ["$corda_group:corda-finance:$corda_release_version"] + } +} \ No newline at end of file diff --git a/gradle-plugins/cordformation/src/test/resources/net/corda/plugins/DeploySingleNodeWithCordappConfig.gradle b/gradle-plugins/cordformation/src/test/resources/net/corda/plugins/DeploySingleNodeWithCordappConfig.gradle new file mode 100644 index 0000000000..4bb6642752 --- /dev/null +++ b/gradle-plugins/cordformation/src/test/resources/net/corda/plugins/DeploySingleNodeWithCordappConfig.gradle @@ -0,0 +1,35 @@ +buildscript { + ext { + corda_group = 'net.corda' + corda_release_version = '3.0-SNAPSHOT' // TODO: Set to 3.0.0 when Corda 3 is released + jolokia_version = '1.3.7' + } +} + +plugins { + id 'java' + id 'net.corda.plugins.cordformation' +} + +repositories { + mavenCentral() + maven { url 'https://ci-artifactory.corda.r3cev.com/artifactory/corda-dev' } +} + +dependencies { + runtime "$corda_group:corda:$corda_release_version" + runtime "$corda_group:corda-node-api:$corda_release_version" + cordapp "$corda_group:corda-finance:$corda_release_version" +} + +task deployNodes(type: net.corda.plugins.Cordform) { + node { + name 'O=Notary Service,L=Zurich,C=CH' + notary = [validating : true] + p2pPort 10002 + rpcPort 10003 + cordapp "$corda_group:corda-finance:$corda_release_version", { + config "a=b" + } + } +} \ No newline at end of file diff --git a/gradle-plugins/cordformation/src/test/resources/net/corda/plugins/DeploySingleNodeWithLocallyBuildCordappAndConfig.gradle b/gradle-plugins/cordformation/src/test/resources/net/corda/plugins/DeploySingleNodeWithLocallyBuildCordappAndConfig.gradle new file mode 100644 index 0000000000..6e0447764f --- /dev/null +++ b/gradle-plugins/cordformation/src/test/resources/net/corda/plugins/DeploySingleNodeWithLocallyBuildCordappAndConfig.gradle @@ -0,0 +1,39 @@ +buildscript { + ext { + corda_group = 'net.corda' + corda_release_version = '3.0-SNAPSHOT' // TODO: Set to 3.0.0 when Corda 3 is released + jolokia_version = '1.3.7' + } +} + +plugins { + id 'java' + id 'net.corda.plugins.cordformation' +} + +repositories { + mavenCentral() + maven { url 'https://ci-artifactory.corda.r3cev.com/artifactory/corda-dev' } +} + +dependencies { + runtime "$corda_group:corda:$corda_release_version" + runtime "$corda_group:corda-node-api:$corda_release_version" + cordapp "$corda_group:corda-finance:$corda_release_version" +} + +jar { + baseName 'locally-built-cordapp' +} + +task deployNodes(type: net.corda.plugins.Cordform, dependsOn: [jar]) { + node { + name 'O=Notary Service,L=Zurich,C=CH' + notary = [validating : true] + p2pPort 10002 + rpcPort 10003 + cordapp { + config "a=b" + } + } +} \ No newline at end of file diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/AttachmentsClassLoaderStaticContractTests.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/AttachmentsClassLoaderStaticContractTests.kt index 519c1632fc..12ec7230b8 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/AttachmentsClassLoaderStaticContractTests.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/AttachmentsClassLoaderStaticContractTests.kt @@ -17,6 +17,7 @@ import net.corda.testing.core.DUMMY_NOTARY_NAME import net.corda.testing.core.SerializationEnvironmentRule import net.corda.testing.core.TestIdentity import net.corda.testing.internal.rigorousMock +import net.corda.testing.node.MockCordappConfigProvider import net.corda.testing.services.MockAttachmentStorage import org.junit.Assert.* import org.junit.Rule @@ -58,7 +59,7 @@ class AttachmentsClassLoaderStaticContractTests { } private val serviceHub = rigorousMock().also { - doReturn(CordappProviderImpl(CordappLoader.createWithTestPackages(listOf("net.corda.nodeapi.internal")), MockAttachmentStorage())).whenever(it).cordappProvider + doReturn(CordappProviderImpl(CordappLoader.createWithTestPackages(listOf("net.corda.nodeapi.internal")), MockCordappConfigProvider(), MockAttachmentStorage())).whenever(it).cordappProvider } @Test diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/AttachmentsClassLoaderTests.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/AttachmentsClassLoaderTests.kt index af8e806874..77406d26a1 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/AttachmentsClassLoaderTests.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/AttachmentsClassLoaderTests.kt @@ -23,6 +23,7 @@ import net.corda.testing.core.SerializationEnvironmentRule import net.corda.testing.core.TestIdentity import net.corda.testing.internal.kryoSpecific import net.corda.testing.internal.rigorousMock +import net.corda.testing.node.MockCordappConfigProvider import net.corda.testing.services.MockAttachmentStorage import org.apache.commons.io.IOUtils import org.junit.Assert.* @@ -57,7 +58,7 @@ class AttachmentsClassLoaderTests { @JvmField val testSerialization = SerializationEnvironmentRule() private val attachments = MockAttachmentStorage() - private val cordappProvider = CordappProviderImpl(CordappLoader.createDevMode(listOf(ISOLATED_CONTRACTS_JAR_PATH)), attachments) + private val cordappProvider = CordappProviderImpl(CordappLoader.createDevMode(listOf(ISOLATED_CONTRACTS_JAR_PATH)), MockCordappConfigProvider(), attachments) private val cordapp get() = cordappProvider.cordapps.first() private val attachmentId get() = cordappProvider.getCordappAttachmentId(cordapp)!! private val appContext get() = cordappProvider.getAppContext(cordapp) diff --git a/node/capsule/build.gradle b/node/capsule/build.gradle index 3572e594fb..ca320e6b5d 100644 --- a/node/capsule/build.gradle +++ b/node/capsule/build.gradle @@ -59,11 +59,6 @@ task buildCordaJAR(type: FatCapsule, dependsOn: project(':node').compileJava) { // If you change these flags, please also update Driver.kt jvmArgs = ['-Xmx200m', '-XX:+UseG1GC'] } - - // Make the resulting JAR file directly executable on UNIX by prepending a shell script to it. - // This lets you run the file like so: ./corda.jar - // Other than being slightly less typing, this has one big advantage: Ctrl-C works properly in the terminal. - reallyExecutable { trampolining() } } artifacts { diff --git a/node/src/integration-test/kotlin/net/corda/node/CordappConfigFileProviderTests.kt b/node/src/integration-test/kotlin/net/corda/node/CordappConfigFileProviderTests.kt new file mode 100644 index 0000000000..ee2fd4674f --- /dev/null +++ b/node/src/integration-test/kotlin/net/corda/node/CordappConfigFileProviderTests.kt @@ -0,0 +1,60 @@ +package net.corda.node + +import com.typesafe.config.Config +import com.typesafe.config.ConfigException +import com.typesafe.config.ConfigFactory +import com.typesafe.config.ConfigRenderOptions +import net.corda.node.internal.cordapp.CordappConfigFileProvider +import org.assertj.core.api.Assertions.assertThat +import org.junit.Test +import java.io.File +import java.nio.file.Files +import java.nio.file.Path + +class CordappConfigFileProviderTests { + private companion object { + val cordappConfDir = File("build/tmp/cordapps/config") + val cordappName = "test" + val cordappConfFile = File(cordappConfDir, cordappName + ".conf").toPath() + + val validConfig = ConfigFactory.parseString("key=value") + val alternateValidConfig = ConfigFactory.parseString("key=alternateValue") + val invalidConfig = "Invalid" + } + + val provider = CordappConfigFileProvider(cordappConfDir) + + @Test + fun `test that config can be loaded`() { + writeConfig(validConfig, cordappConfFile) + assertThat(provider.getConfigByName(cordappName)).isEqualTo(validConfig) + } + + @Test + fun `config is idempotent if the underlying file is not changed`() { + writeConfig(validConfig, cordappConfFile) + assertThat(provider.getConfigByName(cordappName)).isEqualTo(validConfig) + assertThat(provider.getConfigByName(cordappName)).isEqualTo(validConfig) + } + + @Test + fun `config is not idempotent if the underlying file is changed`() { + writeConfig(validConfig, cordappConfFile) + assertThat(provider.getConfigByName(cordappName)).isEqualTo(validConfig) + + writeConfig(alternateValidConfig, cordappConfFile) + assertThat(provider.getConfigByName(cordappName)).isEqualTo(alternateValidConfig) + } + + @Test(expected = ConfigException.Parse::class) + fun `an invalid config throws an exception`() { + Files.write(cordappConfFile, invalidConfig.toByteArray()) + + provider.getConfigByName(cordappName) + } + + /** + * Writes the config to the path provided - will (and must) overwrite any existing config + */ + private fun writeConfig(config: Config, to: Path) = Files.write(cordappConfFile, config.root().render(ConfigRenderOptions.concise()).toByteArray()) +} \ No newline at end of file diff --git a/node/src/integration-test/kotlin/net/corda/node/services/AttachmentLoadingTests.kt b/node/src/integration-test/kotlin/net/corda/node/services/AttachmentLoadingTests.kt index 5fa206af81..65dd56c3f1 100644 --- a/node/src/integration-test/kotlin/net/corda/node/services/AttachmentLoadingTests.kt +++ b/node/src/integration-test/kotlin/net/corda/node/services/AttachmentLoadingTests.kt @@ -29,6 +29,7 @@ import net.corda.testing.driver.NodeHandle import net.corda.testing.driver.driver import net.corda.testing.internal.rigorousMock import net.corda.testing.internal.withoutTestSerialization +import net.corda.testing.node.MockCordappConfigProvider import net.corda.testing.services.MockAttachmentStorage import org.junit.Assert.assertEquals import org.junit.Rule @@ -42,7 +43,7 @@ class AttachmentLoadingTests { @JvmField val testSerialization = SerializationEnvironmentRule() private val attachments = MockAttachmentStorage() - private val provider = CordappProviderImpl(CordappLoader.createDevMode(listOf(isolatedJAR)), attachments) + private val provider = CordappProviderImpl(CordappLoader.createDevMode(listOf(isolatedJAR)), MockCordappConfigProvider(), attachments) private val cordapp get() = provider.cordapps.first() private val attachmentId get() = provider.getCordappAttachmentId(cordapp)!! private val appContext get() = provider.getAppContext(cordapp) diff --git a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt index a88bd73b5d..c00e1fc175 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt @@ -32,6 +32,7 @@ import net.corda.core.utilities.debug import net.corda.core.utilities.getOrThrow import net.corda.node.VersionInfo import net.corda.node.internal.classloading.requireAnnotation +import net.corda.node.internal.cordapp.CordappConfigFileProvider import net.corda.node.internal.cordapp.CordappLoader import net.corda.node.internal.cordapp.CordappProviderImpl import net.corda.node.internal.cordapp.CordappProviderInternal @@ -539,7 +540,7 @@ abstract class AbstractNode(val configuration: NodeConfiguration, checkpointStorage = DBCheckpointStorage() val metrics = MetricRegistry() attachments = NodeAttachmentService(metrics, configuration.attachmentContentCacheSizeBytes, configuration.attachmentCacheBound) - val cordappProvider = CordappProviderImpl(cordappLoader, attachments) + val cordappProvider = CordappProviderImpl(cordappLoader, CordappConfigFileProvider(), attachments) val keyManagementService = makeKeyManagementService(identityService, keyPairs) _services = ServiceHubInternalImpl( identityService, diff --git a/node/src/main/kotlin/net/corda/node/internal/cordapp/CordappConfigFileProvider.kt b/node/src/main/kotlin/net/corda/node/internal/cordapp/CordappConfigFileProvider.kt new file mode 100644 index 0000000000..ce30f5f0f6 --- /dev/null +++ b/node/src/main/kotlin/net/corda/node/internal/cordapp/CordappConfigFileProvider.kt @@ -0,0 +1,36 @@ +package net.corda.node.internal.cordapp + +import com.typesafe.config.Config +import com.typesafe.config.ConfigFactory +import net.corda.core.internal.cordapp.CordappConfigProvider +import net.corda.core.utilities.loggerFor +import sun.plugin.dom.exception.InvalidStateException +import java.io.File + +class CordappConfigFileProvider(val configDir: File = DEFAULT_CORDAPP_CONFIG_DIR) : CordappConfigProvider { + companion object { + val DEFAULT_CORDAPP_CONFIG_DIR = File("cordapps/config") + val CONFIG_EXT = ".conf" + val logger = loggerFor() + } + + init { + configDir.mkdirs() + } + + override fun getConfigByName(name: String): Config { + val configFile = File(configDir, name + CONFIG_EXT) + return if (configFile.exists()) { + if (configFile.isDirectory) { + throw InvalidStateException("ile at ${configFile.absolutePath} is a directory, expected a config file") + } else { + logger.info("Found config for cordapp $name in ${configFile.absolutePath}") + ConfigFactory.parseFile(configFile) + } + } else { + logger.info("No config found for cordapp $name in ${configFile.absolutePath}") + ConfigFactory.empty() + } + } + +} \ No newline at end of file diff --git a/node/src/main/kotlin/net/corda/node/internal/cordapp/CordappConfigProvider.kt b/node/src/main/kotlin/net/corda/node/internal/cordapp/CordappConfigProvider.kt new file mode 100644 index 0000000000..f632481d1c --- /dev/null +++ b/node/src/main/kotlin/net/corda/node/internal/cordapp/CordappConfigProvider.kt @@ -0,0 +1,7 @@ +package net.corda.core.internal.cordapp + +import com.typesafe.config.Config + +interface CordappConfigProvider { + fun getConfigByName(name: String): Config +} \ No newline at end of file diff --git a/node/src/main/kotlin/net/corda/node/internal/cordapp/CordappProviderImpl.kt b/node/src/main/kotlin/net/corda/node/internal/cordapp/CordappProviderImpl.kt index cfed6d0cd2..a25d872124 100644 --- a/node/src/main/kotlin/net/corda/node/internal/cordapp/CordappProviderImpl.kt +++ b/node/src/main/kotlin/net/corda/node/internal/cordapp/CordappProviderImpl.kt @@ -5,21 +5,27 @@ import net.corda.core.contracts.ContractClassName import net.corda.core.cordapp.Cordapp import net.corda.core.cordapp.CordappContext import net.corda.core.crypto.SecureHash +import net.corda.core.internal.cordapp.CordappConfigProvider +import net.corda.core.internal.createCordappContext import net.corda.core.node.services.AttachmentId import net.corda.core.node.services.AttachmentStorage import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.core.utilities.loggerFor import java.net.URL +import java.util.concurrent.ConcurrentHashMap /** * Cordapp provider and store. For querying CorDapps for their attachment and vice versa. */ -open class CordappProviderImpl(private val cordappLoader: CordappLoader, attachmentStorage: AttachmentStorage) : SingletonSerializeAsToken(), CordappProviderInternal { +open class CordappProviderImpl(private val cordappLoader: CordappLoader, private val cordappConfigProvider: CordappConfigProvider, attachmentStorage: AttachmentStorage) : SingletonSerializeAsToken(), CordappProviderInternal { companion object { private val log = loggerFor() } + private val contextCache = ConcurrentHashMap() + + override fun getAppContext(): CordappContext { // TODO: Use better supported APIs in Java 9 Exception().stackTrace.forEach { stackFrame -> @@ -51,7 +57,7 @@ open class CordappProviderImpl(private val cordappLoader: CordappLoader, attachm private fun loadContractsIntoAttachmentStore(attachmentStorage: AttachmentStorage): Map { val cordappsWithAttachments = cordapps.filter { !it.contractClassNames.isEmpty() }.map { it.jarPath } - val attachmentIds = cordappsWithAttachments.map { it.openStream().use { attachmentStorage.importOrGetAttachment(it) }} + val attachmentIds = cordappsWithAttachments.map { it.openStream().use { attachmentStorage.importOrGetAttachment(it) } } return attachmentIds.zip(cordappsWithAttachments).toMap() } @@ -62,7 +68,14 @@ open class CordappProviderImpl(private val cordappLoader: CordappLoader, attachm * @return A cordapp context for the given CorDapp */ fun getAppContext(cordapp: Cordapp): CordappContext { - return CordappContext(cordapp, getCordappAttachmentId(cordapp), cordappLoader.appClassLoader) + return contextCache.computeIfAbsent(cordapp, { + createCordappContext( + cordapp, + getCordappAttachmentId(cordapp), + cordappLoader.appClassLoader, + TypesafeCordappConfig(cordappConfigProvider.getConfigByName(cordapp.name)) + ) + }) } /** diff --git a/node/src/main/kotlin/net/corda/node/internal/cordapp/TypesafeCordappConfig.kt b/node/src/main/kotlin/net/corda/node/internal/cordapp/TypesafeCordappConfig.kt new file mode 100644 index 0000000000..73f5633350 --- /dev/null +++ b/node/src/main/kotlin/net/corda/node/internal/cordapp/TypesafeCordappConfig.kt @@ -0,0 +1,79 @@ +package net.corda.node.internal.cordapp + +import com.typesafe.config.Config +import com.typesafe.config.ConfigException +import net.corda.core.cordapp.CordappConfig +import net.corda.core.cordapp.CordappConfigException + +/** + * Provides configuration from a typesafe config source + */ +class TypesafeCordappConfig(private val cordappConfig: Config) : CordappConfig { + override fun exists(path: String): Boolean { + return cordappConfig.hasPath(path) + } + + override fun get(path: String): Any { + try { + return cordappConfig.getAnyRef(path) + } catch (e: ConfigException) { + throw CordappConfigException("Cordapp configuration is incorrect due to exception", e) + } + } + + override fun getInt(path: String): Int { + try { + return cordappConfig.getInt(path) + } catch (e: ConfigException) { + throw CordappConfigException("Cordapp configuration is incorrect due to exception", e) + } + } + + override fun getLong(path: String): Long { + try { + return cordappConfig.getLong(path) + } catch (e: ConfigException) { + throw CordappConfigException("Cordapp configuration is incorrect due to exception", e) + } + } + + override fun getFloat(path: String): Float { + try { + return cordappConfig.getDouble(path).toFloat() + } catch (e: ConfigException) { + throw CordappConfigException("Cordapp configuration is incorrect due to exception", e) + } + } + + override fun getDouble(path: String): Double { + try { + return cordappConfig.getDouble(path) + } catch (e: ConfigException) { + throw CordappConfigException("Cordapp configuration is incorrect due to exception", e) + } + } + + override fun getNumber(path: String): Number { + try { + return cordappConfig.getNumber(path) + } catch (e: ConfigException) { + throw CordappConfigException("Cordapp configuration is incorrect due to exception", e) + } + } + + override fun getString(path: String): String { + try { + return cordappConfig.getString(path) + } catch (e: ConfigException) { + throw CordappConfigException("Cordapp configuration is incorrect due to exception", e) + } + } + + override fun getBoolean(path: String): Boolean { + try { + return cordappConfig.getBoolean(path) + } catch (e: ConfigException) { + throw CordappConfigException("Cordapp configuration is incorrect due to exception", e) + } + } +} \ No newline at end of file diff --git a/node/src/test/kotlin/net/corda/node/internal/cordapp/CordappProviderImplTests.kt b/node/src/test/kotlin/net/corda/node/internal/cordapp/CordappProviderImplTests.kt index 8526b36bcd..934c906e5d 100644 --- a/node/src/test/kotlin/net/corda/node/internal/cordapp/CordappProviderImplTests.kt +++ b/node/src/test/kotlin/net/corda/node/internal/cordapp/CordappProviderImplTests.kt @@ -1,15 +1,27 @@ package net.corda.node.internal.cordapp +import com.typesafe.config.Config +import com.typesafe.config.ConfigFactory +import net.corda.core.internal.cordapp.CordappConfigProvider import net.corda.core.node.services.AttachmentStorage +import net.corda.testing.node.MockCordappConfigProvider import net.corda.testing.services.MockAttachmentStorage +import org.assertj.core.api.Assertions.assertThat import org.junit.Assert import org.junit.Before import org.junit.Test class CordappProviderImplTests { - companion object { - private val isolatedJAR = this::class.java.getResource("isolated.jar")!! - private val emptyJAR = this::class.java.getResource("empty.jar")!! + private companion object { + val isolatedJAR = this::class.java.getResource("isolated.jar")!! + // TODO: Cordapp name should differ from the JAR name + val isolatedCordappName = "isolated" + val emptyJAR = this::class.java.getResource("empty.jar")!! + val validConfig = ConfigFactory.parseString("key=value") + + val stubConfigProvider = object : CordappConfigProvider { + override fun getConfigByName(name: String): Config = ConfigFactory.empty() + } } private lateinit var attachmentStore: AttachmentStorage @@ -22,7 +34,7 @@ class CordappProviderImplTests { @Test fun `isolated jar is loaded into the attachment store`() { val loader = CordappLoader.createDevMode(listOf(isolatedJAR)) - val provider = CordappProviderImpl(loader, attachmentStore) + val provider = CordappProviderImpl(loader, stubConfigProvider, attachmentStore) val maybeAttachmentId = provider.getCordappAttachmentId(provider.cordapps.first()) Assert.assertNotNull(maybeAttachmentId) @@ -32,14 +44,14 @@ class CordappProviderImplTests { @Test fun `empty jar is not loaded into the attachment store`() { val loader = CordappLoader.createDevMode(listOf(emptyJAR)) - val provider = CordappProviderImpl(loader, attachmentStore) + val provider = CordappProviderImpl(loader, stubConfigProvider, attachmentStore) Assert.assertNull(provider.getCordappAttachmentId(provider.cordapps.first())) } @Test fun `test that we find a cordapp class that is loaded into the store`() { val loader = CordappLoader.createDevMode(listOf(isolatedJAR)) - val provider = CordappProviderImpl(loader, attachmentStore) + val provider = CordappProviderImpl(loader, stubConfigProvider, attachmentStore) val className = "net.corda.finance.contracts.isolated.AnotherDummyContract" val expected = provider.cordapps.first() @@ -50,9 +62,9 @@ class CordappProviderImplTests { } @Test - fun `test that we find an attachment for a cordapp contrat class`() { + fun `test that we find an attachment for a cordapp contract class`() { val loader = CordappLoader.createDevMode(listOf(isolatedJAR)) - val provider = CordappProviderImpl(loader, attachmentStore) + val provider = CordappProviderImpl(loader, stubConfigProvider, attachmentStore) val className = "net.corda.finance.contracts.isolated.AnotherDummyContract" val expected = provider.getAppContext(provider.cordapps.first()).attachmentId val actual = provider.getContractAttachmentID(className) @@ -60,4 +72,16 @@ class CordappProviderImplTests { Assert.assertNotNull(actual) Assert.assertEquals(actual!!, expected) } + + @Test + fun `test cordapp configuration`() { + val configProvider = MockCordappConfigProvider() + configProvider.cordappConfigs.put(isolatedCordappName, validConfig) + val loader = CordappLoader.createDevMode(listOf(isolatedJAR)) + val provider = CordappProviderImpl(loader, configProvider, attachmentStore) + + val expected = provider.getAppContext(provider.cordapps.first()).config + + assertThat(expected.getString("key")).isEqualTo("value") + } } diff --git a/node/src/test/kotlin/net/corda/node/internal/cordapp/TypesafeCordappConfigTests.kt b/node/src/test/kotlin/net/corda/node/internal/cordapp/TypesafeCordappConfigTests.kt new file mode 100644 index 0000000000..6a966ea4b0 --- /dev/null +++ b/node/src/test/kotlin/net/corda/node/internal/cordapp/TypesafeCordappConfigTests.kt @@ -0,0 +1,47 @@ +package net.corda.node.internal.cordapp + +import com.typesafe.config.ConfigFactory +import net.corda.core.cordapp.CordappConfigException +import org.junit.Test +import org.assertj.core.api.Assertions.assertThat + +class TypesafeCordappConfigTests { + @Test + fun `test that all value types can be retrieved`() { + val config = ConfigFactory.parseString("string=string\nint=1\nfloat=1.0\ndouble=1.0\nnumber=2\ndouble=1.01\nbool=false") + val cordappConf = TypesafeCordappConfig(config) + + assertThat(cordappConf.get("string")).isEqualTo("string") + assertThat(cordappConf.getString("string")).isEqualTo("string") + assertThat(cordappConf.getInt("int")).isEqualTo(1) + assertThat(cordappConf.getFloat("float")).isEqualTo(1.0F) + assertThat(cordappConf.getDouble("double")).isEqualTo(1.01) + assertThat(cordappConf.getNumber("number")).isEqualTo(2) + assertThat(cordappConf.getBoolean("bool")).isEqualTo(false) + } + + @Test + fun `test a nested path`() { + val config = ConfigFactory.parseString("outer: { inner: string }") + val cordappConf = TypesafeCordappConfig(config) + + assertThat(cordappConf.getString("outer.inner")).isEqualTo("string") + } + + @Test + fun `test exists determines existence and lack of existence correctly`() { + val config = ConfigFactory.parseString("exists=exists") + val cordappConf = TypesafeCordappConfig(config) + + assertThat(cordappConf.exists("exists")).isTrue() + assertThat(cordappConf.exists("notexists")).isFalse() + } + + @Test(expected = CordappConfigException::class) + fun `test that an exception is thrown when trying to access a non-extant field`() { + val config = ConfigFactory.empty() + val cordappConf = TypesafeCordappConfig(config) + + cordappConf.get("anything") + } +} \ No newline at end of file diff --git a/samples/bank-of-corda-demo/src/integration-test/kotlin/net/corda/bank/BankOfCordaCordformTest.kt b/samples/bank-of-corda-demo/src/integration-test/kotlin/net/corda/bank/BankOfCordaCordformTest.kt index 34432277a1..e5235553b6 100644 --- a/samples/bank-of-corda-demo/src/integration-test/kotlin/net/corda/bank/BankOfCordaCordformTest.kt +++ b/samples/bank-of-corda-demo/src/integration-test/kotlin/net/corda/bank/BankOfCordaCordformTest.kt @@ -2,13 +2,13 @@ package net.corda.bank import net.corda.finance.DOLLARS import net.corda.finance.POUNDS -import net.corda.testing.node.internal.demorun.deployNodesThen +import net.corda.testing.node.internal.demorun.nodeRunner import org.junit.Test class BankOfCordaCordformTest { @Test fun `run demo`() { - BankOfCordaCordform().deployNodesThen { + BankOfCordaCordform().nodeRunner().scanPackages(listOf("net.corda.finance")).deployAndRunNodesThen { IssueCash.requestWebIssue(30000.POUNDS) IssueCash.requestRpcIssue(20000.DOLLARS) } 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 260d37365f..e750504a8f 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 @@ -25,8 +25,8 @@ private const val BOC_RPC_ADMIN_PORT = 10015 private const val BOC_WEB_PORT = 10007 class BankOfCordaCordform : CordformDefinition() { + // TODO: Readd finance dependency - will fail without it init { - cordappPackages += "net.corda.finance" node { name(NOTARY_NAME) notary(NotaryConfig(validating = true)) @@ -65,7 +65,7 @@ class BankOfCordaCordform : CordformDefinition() { object DeployNodes { @JvmStatic fun main(args: Array) { - BankOfCordaCordform().deployNodes() + BankOfCordaCordform().nodeRunner().scanPackages(listOf("net.corda.finance")).deployAndRunNodes() } } diff --git a/samples/cordapp-configuration/README.md b/samples/cordapp-configuration/README.md new file mode 100644 index 0000000000..651e8debc9 --- /dev/null +++ b/samples/cordapp-configuration/README.md @@ -0,0 +1,23 @@ +# Cordapp Configuration Sample + +This sample shows a simple example of how to use per-cordapp configuration. It includes; + +* A configuration file +* Gradle build file to show how to install your Cordapp configuration +* A flow that consumes the Cordapp configuration + +## Usage + +To run the sample you must first build it from the project root with; + + ./gradlew deployNodes + +This will deploy the node with the configuration installed. +The relevant section is the ``deployNodes`` task. + +## Running + +* Windows: `build\nodes\runnodes` +* Mac/Linux: `./build/nodes/runnodes` + +Once the nodes have started up and show a prompt you can now run your flow. \ No newline at end of file diff --git a/samples/cordapp-configuration/build.gradle b/samples/cordapp-configuration/build.gradle new file mode 100644 index 0000000000..37edc09147 --- /dev/null +++ b/samples/cordapp-configuration/build.gradle @@ -0,0 +1,54 @@ +apply plugin: 'kotlin' +apply plugin: 'java' +apply plugin: 'net.corda.plugins.cordapp' +apply plugin: 'net.corda.plugins.cordformation' + +dependencies { + cordaCompile project(":core") + cordaCompile project(":node-api") + cordaCompile project(path: ":node:capsule", configuration: 'runtimeArtifacts') + cordaCompile project(path: ":webserver:webcapsule", configuration: 'runtimeArtifacts') +} + +task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { + ext.rpcUsers = [['username': "default", 'password': "default", 'permissions': [ 'ALL' ]]] + + directory "./build/nodes" + node { + name "O=Notary Service,L=Zurich,C=CH" + notary = [validating : true] + p2pPort 10002 + rpcSettings { + port 10003 + adminPort 10004 + } + } + node { + name "O=Bank A,L=London,C=GB" + p2pPort 10005 + cordapps = [] + rpcUsers = ext.rpcUsers + // This configures the default cordapp for this node + projectCordapp { + config "someStringValue=test" + } + rpcSettings { + port 10007 + adminPort 10008 + } + } + node { + name "O=Bank B,L=New York,C=US" + p2pPort 10009 + cordapps = [] + rpcUsers = ext.rpcUsers + // This configures the default cordapp for this node + projectCordapp { + config project.file("src/config.conf") + } + rpcSettings { + port 10011 + adminPort 10012 + } + } +} \ No newline at end of file diff --git a/samples/cordapp-configuration/src/config.conf b/samples/cordapp-configuration/src/config.conf new file mode 100644 index 0000000000..5e2d9fdcd6 --- /dev/null +++ b/samples/cordapp-configuration/src/config.conf @@ -0,0 +1,5 @@ +someStringValue=hello world +someIntValue=1 +nested: { + value: a string +} \ No newline at end of file diff --git a/samples/cordapp-configuration/src/main/kotlin/net/corda/configsample/ConfigSampleFlow.kt b/samples/cordapp-configuration/src/main/kotlin/net/corda/configsample/ConfigSampleFlow.kt new file mode 100644 index 0000000000..251830f538 --- /dev/null +++ b/samples/cordapp-configuration/src/main/kotlin/net/corda/configsample/ConfigSampleFlow.kt @@ -0,0 +1,10 @@ +package net.corda.configsample + +import net.corda.core.flows.FlowLogic + +class ConfigSampleFlow : FlowLogic() { + override fun call(): String { + val config = serviceHub.getAppContext().config + return config.getString("someStringValue") + } +} \ No newline at end of file diff --git a/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/BFTNotaryCordform.kt b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/BFTNotaryCordform.kt index 132636f05a..9f5c8802e4 100644 --- a/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/BFTNotaryCordform.kt +++ b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/BFTNotaryCordform.kt @@ -14,7 +14,7 @@ import net.corda.testing.core.ALICE_NAME import net.corda.testing.core.BOB_NAME import java.nio.file.Paths -fun main(args: Array) = BFTNotaryCordform().deployNodes() +fun main(args: Array) = BFTNotaryCordform().nodeRunner().deployAndRunNodes() private val clusterSize = 4 // Minimum size that tolerates a faulty replica. private val notaryNames = createNotaryNames(clusterSize) diff --git a/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/Clean.kt b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/Clean.kt index 91aa5ba967..2fbba77b26 100644 --- a/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/Clean.kt +++ b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/Clean.kt @@ -1,9 +1,9 @@ package net.corda.notarydemo -import net.corda.testing.node.internal.demorun.clean +import net.corda.testing.node.internal.demorun.nodeRunner fun main(args: Array) { - listOf(SingleNotaryCordform(), RaftNotaryCordform(), BFTNotaryCordform()).forEach { + listOf(SingleNotaryCordform(), RaftNotaryCordform(), BFTNotaryCordform()).map { it.nodeRunner() }.forEach { it.clean() } } diff --git a/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/CustomNotaryCordform.kt b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/CustomNotaryCordform.kt index e9a857b1cb..3aa6b38654 100644 --- a/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/CustomNotaryCordform.kt +++ b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/CustomNotaryCordform.kt @@ -9,7 +9,7 @@ import net.corda.testing.core.BOB_NAME import net.corda.testing.core.DUMMY_NOTARY_NAME import java.nio.file.Paths -fun main(args: Array) = CustomNotaryCordform().deployNodes() +fun main(args: Array) = CustomNotaryCordform().nodeRunner().deployAndRunNodes() class CustomNotaryCordform : CordformDefinition() { init { diff --git a/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/RaftNotaryCordform.kt b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/RaftNotaryCordform.kt index c4cbcd0d8e..2a2aec2582 100644 --- a/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/RaftNotaryCordform.kt +++ b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/RaftNotaryCordform.kt @@ -13,7 +13,7 @@ import net.corda.testing.core.ALICE_NAME import net.corda.testing.core.BOB_NAME import java.nio.file.Paths -fun main(args: Array) = RaftNotaryCordform().deployNodes() +fun main(args: Array) = RaftNotaryCordform().nodeRunner().deployAndRunNodes() internal fun createNotaryNames(clusterSize: Int) = (0 until clusterSize).map { CordaX500Name("Notary Service $it", "Zurich", "CH") } diff --git a/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/SingleNotaryCordform.kt b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/SingleNotaryCordform.kt index af449ce5cb..d5acdb135c 100644 --- a/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/SingleNotaryCordform.kt +++ b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/SingleNotaryCordform.kt @@ -11,7 +11,7 @@ import net.corda.testing.node.User import net.corda.testing.node.internal.demorun.* import java.nio.file.Paths -fun main(args: Array) = SingleNotaryCordform().deployNodes() +fun main(args: Array) = SingleNotaryCordform().nodeRunner().deployAndRunNodes() val notaryDemoUser = User("demou", "demop", setOf(all())) diff --git a/samples/simm-valuation-demo/build.gradle b/samples/simm-valuation-demo/build.gradle index b560d1aa4f..9edd4f2853 100644 --- a/samples/simm-valuation-demo/build.gradle +++ b/samples/simm-valuation-demo/build.gradle @@ -68,7 +68,7 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { name "O=Notary Service,L=Zurich,C=CH" notary = [validating : true] p2pPort 10002 - cordapps = ["$project.group:finance:$corda_release_version"] + cordapp project(':finance') extraConfig = [ jvmArgs : [ "-Xmx1g"] ] @@ -81,7 +81,7 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { address("localhost:10016") adminAddress("localhost:10017") } - cordapps = ["$project.group:finance:$corda_release_version"] + cordapp project(':finance') rpcUsers = ext.rpcUsers extraConfig = [ jvmArgs : [ "-Xmx1g"] @@ -95,7 +95,7 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { address("localhost:10026") adminAddress("localhost:10027") } - cordapps = ["$project.group:finance:$corda_release_version"] + cordapp project(':finance') rpcUsers = ext.rpcUsers extraConfig = [ jvmArgs : [ "-Xmx1g"] @@ -109,7 +109,7 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { address("localhost:10036") adminAddress("localhost:10037") } - cordapps = ["$project.group:finance:$corda_release_version"] + cordapp project(':finance') rpcUsers = ext.rpcUsers extraConfig = [ jvmArgs : [ "-Xmx1g"] diff --git a/settings.gradle b/settings.gradle index 4b0a92c815..1191e87414 100644 --- a/settings.gradle +++ b/settings.gradle @@ -46,3 +46,4 @@ include 'samples:network-visualiser' include 'samples:simm-valuation-demo' include 'samples:notary-demo' include 'samples:bank-of-corda-demo' +include 'samples:cordapp-configuration' diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/demorun/CordformNodeRunner.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/demorun/CordformNodeRunner.kt new file mode 100644 index 0000000000..bbaf5724a5 --- /dev/null +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/demorun/CordformNodeRunner.kt @@ -0,0 +1,77 @@ +package net.corda.testing.node.internal.demorun + +import net.corda.cordform.CordformDefinition +import net.corda.cordform.CordformNode +import net.corda.core.utilities.NetworkHostAndPort +import net.corda.core.utilities.getOrThrow +import net.corda.testing.driver.JmxPolicy +import net.corda.testing.driver.PortAllocation +import net.corda.testing.node.internal.internalDriver + +/** + * Creates a demo runner for this cordform definition + */ +fun CordformDefinition.nodeRunner() = CordformNodeRunner(this) + +/** + * A node runner creates and runs nodes for a given [[CordformDefinition]]. + */ +class CordformNodeRunner(val cordformDefinition: CordformDefinition) { + private var extraPackagesToScan = emptyList() + + /** + * Builder method to sets the extra cordapp scan packages + */ + fun scanPackages(packages: List): CordformNodeRunner { + extraPackagesToScan = packages + return this + } + + fun clean() { + System.err.println("Deleting: ${cordformDefinition.nodesDirectory}") + cordformDefinition.nodesDirectory.toFile().deleteRecursively() + } + + /** + * Deploy the nodes specified in the given [CordformDefinition]. This will block until all the nodes and webservers + * have terminated. + */ + fun deployAndRunNodes() { + runNodes(waitForAllNodesToFinish = true) { } + } + + /** + * Deploy the nodes specified in the given [CordformDefinition] and then execute the given [block] once all the nodes + * and webservers are up. After execution all these processes will be terminated. + */ + fun deployAndRunNodesThen(block: () -> Unit) { + runNodes(waitForAllNodesToFinish = false, block = block) + } + + private fun runNodes(waitForAllNodesToFinish: Boolean, block: () -> Unit) { + clean() + val nodes = cordformDefinition.nodeConfigurers.map { configurer -> CordformNode().also { configurer.accept(it) } } + val maxPort = nodes + .flatMap { listOf(it.p2pAddress, it.rpcAddress, it.webAddress) } + .mapNotNull { address -> address?.let { NetworkHostAndPort.parse(it).port } } + .max()!! + internalDriver( + isDebug = true, + jmxPolicy = JmxPolicy(true), + driverDirectory = cordformDefinition.nodesDirectory, + extraCordappPackagesToScan = extraPackagesToScan, + // Notaries are manually specified in Cordform so we don't want the driver automatically starting any + notarySpecs = emptyList(), + // Start from after the largest port used to prevent port clash + portAllocation = PortAllocation.Incremental(maxPort + 1), + waitForAllNodesToFinish = waitForAllNodesToFinish + ) { + cordformDefinition.setup(this) + startCordformNodes(nodes).getOrThrow() // Only proceed once everything is up and running + println("All nodes and webservers are ready...") + block() + } + } +} + + diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/demorun/DemoRunner.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/demorun/DemoRunner.kt deleted file mode 100644 index 185a851e99..0000000000 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/demorun/DemoRunner.kt +++ /dev/null @@ -1,57 +0,0 @@ -@file:JvmName("DemoRunner") - -package net.corda.testing.node.internal.demorun - -import net.corda.cordform.CordformDefinition -import net.corda.cordform.CordformNode -import net.corda.core.utilities.NetworkHostAndPort -import net.corda.core.utilities.getOrThrow -import net.corda.testing.driver.JmxPolicy -import net.corda.testing.driver.PortAllocation -import net.corda.testing.node.internal.internalDriver - -fun CordformDefinition.clean() { - System.err.println("Deleting: $nodesDirectory") - nodesDirectory.toFile().deleteRecursively() -} - -/** - * Deploy the nodes specified in the given [CordformDefinition]. This will block until all the nodes and webservers - * have terminated. - */ -fun CordformDefinition.deployNodes() { - runNodes(waitForAllNodesToFinish = true) { } -} - -/** - * Deploy the nodes specified in the given [CordformDefinition] and then execute the given [block] once all the nodes - * and webservers are up. After execution all these processes will be terminated. - */ -fun CordformDefinition.deployNodesThen(block: () -> Unit) { - runNodes(waitForAllNodesToFinish = false, block = block) -} - -private fun CordformDefinition.runNodes(waitForAllNodesToFinish: Boolean, block: () -> Unit) { - clean() - val nodes = nodeConfigurers.map { configurer -> CordformNode().also { configurer.accept(it) } } - val maxPort = nodes - .flatMap { listOf(it.p2pAddress, it.rpcAddress, it.webAddress) } - .mapNotNull { address -> address?.let { NetworkHostAndPort.parse(it).port } } - .max()!! - internalDriver( - isDebug = true, - jmxPolicy = JmxPolicy(true), - driverDirectory = nodesDirectory, - extraCordappPackagesToScan = cordappPackages, - // Notaries are manually specified in Cordform so we don't want the driver automatically starting any - notarySpecs = emptyList(), - // Start from after the largest port used to prevent port clash - portAllocation = PortAllocation.Incremental(maxPort + 1), - waitForAllNodesToFinish = waitForAllNodesToFinish - ) { - setup(this) - startCordformNodes(nodes).getOrThrow() // Only proceed once everything is up and running - println("All nodes and webservers are ready...") - block() - } -} diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/node/MockCordappConfigProvider.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/node/MockCordappConfigProvider.kt new file mode 100644 index 0000000000..d351154982 --- /dev/null +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/node/MockCordappConfigProvider.kt @@ -0,0 +1,17 @@ +package net.corda.testing.node + +import com.typesafe.config.Config +import com.typesafe.config.ConfigFactory +import net.corda.core.internal.cordapp.CordappConfigProvider + +class MockCordappConfigProvider : CordappConfigProvider { + val cordappConfigs = mutableMapOf () + + override fun getConfigByName(name: String): Config { + return if(cordappConfigs.containsKey(name)) { + cordappConfigs[name]!! + } else { + ConfigFactory.empty() + } + } +} \ No newline at end of file diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/services/MockCordappProvider.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/services/MockCordappProvider.kt index dba07b73be..82ca31d6f9 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/services/MockCordappProvider.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/services/MockCordappProvider.kt @@ -7,10 +7,17 @@ import net.corda.core.node.services.AttachmentId import net.corda.core.node.services.AttachmentStorage import net.corda.node.internal.cordapp.CordappLoader import net.corda.node.internal.cordapp.CordappProviderImpl +import net.corda.testing.node.MockCordappConfigProvider import java.nio.file.Paths import java.util.* -class MockCordappProvider(cordappLoader: CordappLoader, attachmentStorage: AttachmentStorage) : CordappProviderImpl(cordappLoader, attachmentStorage) { +class MockCordappProvider( + cordappLoader: CordappLoader, + attachmentStorage: AttachmentStorage, + val cordappConfigProvider: MockCordappConfigProvider = MockCordappConfigProvider() +) : CordappProviderImpl(cordappLoader, cordappConfigProvider, attachmentStorage) { + constructor(cordappLoader: CordappLoader, attachmentStorage: AttachmentStorage) : this(cordappLoader, attachmentStorage, MockCordappConfigProvider()) + val cordappRegistry = mutableListOf>() fun addMockCordapp(contractClassName: ContractClassName, attachments: MockAttachmentStorage) {