Merge pull request #12 from corda/chrisbarratt/CORE-9302-switch-to-Corda-beta1-release-build

CORE-9302 Switch to corda beta1 release build
This commit is contained in:
Chris Barratt 2023-01-11 14:09:20 +00:00 committed by GitHub
commit 1eb51b7fcc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 1540 additions and 802 deletions

2
.ci/Jenkinsfile vendored
View File

@ -1,4 +1,4 @@
@Library('corda-shared-build-pipeline-steps@chrisbarratt/CORE-8075-create-pipeline-for-running-the-autotester') _
@Library('corda-shared-build-pipeline-steps@5.0') _
cordaPipeline(
nexusAppId: 'com.corda.CSDE-Java.5.0',

View File

@ -1,5 +1,8 @@
# CSDE-cordapp-template-java
## Note: This cut of CSDE is work in progress and has not been released yet, hence may not function as expected.
To help make the process of prototyping Cordapps on Developer Preview 2 more straight forward we have developed the Cordapp Standard Development Environment (CSDE).
The CSDE is obtained by cloning this CSDE-Cordapp-Template-java to your local machine. The CSDE provides:

View File

@ -3,11 +3,6 @@ import static org.gradle.api.JavaVersion.VERSION_11
plugins {
id 'org.jetbrains.kotlin.jvm'
// Include the cordapp-cpb plugin. This automatically includes the cordapp-cpk plugin as well.
// These extend existing build environment so that CPB and CPK files can be built.
// This includes a CorDapp DSL that allows the developer to supply metadata for the CorDapp
// required by Corda.
id 'net.corda.plugins.cordapp-cpb2'
id 'net.corda.cordapp.cordapp-configuration'
id 'org.jetbrains.kotlin.plugin.jpa'
@ -18,36 +13,14 @@ plugins {
id 'csde'
}
allprojects {
group 'com.r3.hellocorda'
version '1.0-SNAPSHOT'
def javaVersion = VERSION_11
// The CordApp section.
// This is part of the DSL provided by the corda plugins to define metadata for our CorDapp.
// Each component of the CorDapp would get its own CorDapp section in the build.gradle file for the components
// subproject.
// This is required by the corda plugins to build the CorDapp.
cordapp {
// "targetPlatformVersion" and "minimumPlatformVersion" are intended to specify the preferred
// and earliest versions of the Corda platform that the CorDapp will run on respectively.
// Enforced versioning has not implemented yet so we need to pass in a dummy value for now.
// The platform version will correspond to and be roughly equivalent to the Corda API version.
targetPlatformVersion platformVersion.toInteger()
minimumPlatformVersion platformVersion.toInteger()
// The cordapp section contains either a workflow or contract subsection depending on the type of component.
// Declares the type and metadata of the CPK (this CPB has one CPK).
workflow {
name "ModuleNameHere"
versionId 1
vendor "VendorNameHere"
}
}
// Declare the set of Kotlin compiler options we need to build a CorDapp.
// Declare the set of Java compiler options we need to build a CorDapp.
tasks.withType(JavaCompile) {
// -parameters - Needed for reflection and serialization to work correctly.
options.compilerArgs += [
"-parameters"
@ -57,65 +30,14 @@ tasks.withType(JavaCompile) {
repositories {
// All dependencies are held in Maven Central
mavenCentral()
maven {
url = "$artifactoryContextUrl/"
}
}
// Declare dependencies for the modules we will use.
// A cordaProvided declaration is required for anything that we use that the Corda API provides.
// This is required to allow us to build CorDapp modules as OSGi bundles that CPI and CPB files are built on.
dependencies {
// We need a version of kotlin-stdlib-jdk8 built as an OSGi bundle, this is "kotlin-stdlib-jdk8-osgi".
// R3 builds kotlin-stdlib-jdk8-osgi from Kotlin's kotlin-stdlib-jdk8.
// NB:
// Kotlin's kotlin-osgi-bundle does not provide all of the Kotlin API that is required,
// There is no kotlin-stdlib-jdk11, but one is not needed even though we are targetting Java 11.
cordaProvided 'net.corda.kotlin:kotlin-stdlib-jdk8-osgi'
// Declare a "platform" so that we use the correct set of dependency versions for the version of the
// Corda API specified.
cordaProvided platform("net.corda:corda-api:$cordaApiVersion")
// If using transistive dependencies this will provide most of Corda-API:
// cordaProvided 'net.corda:corda-application'
// Alternatively we can explicitly specify all our Corda-API dependencies:
cordaProvided 'net.corda:corda-base'
cordaProvided 'net.corda:corda-application'
cordaProvided 'net.corda:corda-crypto'
cordaProvided 'net.corda:corda-membership'
// cordaProvided 'net.corda:corda-persistence'
cordaProvided 'net.corda:corda-serialization'
// Not yet fully implemented:
// cordaProvided 'net.corda:corda-ledger'
// The CorDapp uses the slf4j logging framework. Corda-API provides this so we need a 'cordaProvided' declaration.
cordaProvided 'org.slf4j:slf4j-api'
// Dependencies Required By Test Tooling
testImplementation "net.corda:corda-simulator-api:$simulatorVersion"
testRuntimeOnly "net.corda:corda-simulator-runtime:$simulatorVersion"
// 3rd party libraries
// Required
testImplementation "org.slf4j:slf4j-simple:2.0.0"
testImplementation "org.junit.jupiter:junit-jupiter:$junitVersion"
testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$junitVersion"
// Optional but used by exmaple tests.
testImplementation "org.mockito:mockito-core:$mockitoVersion"
testImplementation "org.mockito.kotlin:mockito-kotlin:$mockitoKotlinVersion"
testImplementation "org.hamcrest:hamcrest-library:$hamcrestVersion"
}
test {
tasks.withType(Test).configureEach {
useJUnitPlatform()
}
}
publishing {
publications {
maven(MavenPublication) {
@ -123,5 +45,7 @@ publishing {
groupId project.group
artifact jar
}
}
}

View File

@ -6,9 +6,6 @@ plugins {
repositories {
mavenCentral()
maven {
url = "$artifactoryContextUrl/"
}
}
dependencies {

View File

@ -1,6 +1,4 @@
jacksonVersion = 2.13.3
jacksonVersion = 2.13.4
unirestVersion=3.13.10
cordaApiVersion=5.0.0.505-Beta1.0-HC00
# R3 internal repository
artifactoryContextUrl=https://staging.download.corda.net/maven/20ede3c6-29c0-11ed-966d-b7c36748b9f6-Beta1.0-HC00
cordaApiVersion=5.0.0.523-Fox1.0

View File

@ -1,4 +1,24 @@
import com.r3.csde.CsdeRpcInterface
// Note, IntelliJ does not recognise the imported Java Classes, hence they are
// highlighted in Red. However, they are recognised in the gradle compilation.
// todo: look at the declaration of the script variables, can they be combined with the declaration of the Project Context
// todo: investigate adding corda-cli to the class path then executing it directly - might not work as gradle has to set up the jar file, so its not their when you start.
// Todo: write a test flow runner helper function??
// todo: rename deployCPIsHelper
// todo: add proper logging, rather than reading Stdout
// todo: add test corda running/live task
// todo: add a test to check docker is running and display error if not + halt start corda
// todo: add a clean corda task.
import com.r3.csde.CordaLifeCycleHelper
import com.r3.csde.ProjectContext
import com.r3.csde.DeployCPIsHelper
import com.r3.csde.BuildCPIsHelper
import com.r3.csde.ProjectUtils
import com.r3.csde.CordaStatusQueries
import com.r3.csde.CreateAndRegisterVNodesHelper
plugins {
id 'java-library'
@ -18,21 +38,37 @@ configurations {
canBeResolved = true
}
notaryServerCPB {
canBeConsumed = false
canBeResolved = true
}
}
// Dependencies for supporting tools
dependencies {
combinedWorker "net.corda:corda-combined-worker:$combinedWorkerVersion"
myPostgresJDBC 'org.postgresql:postgresql:42.4.1'
notaryServerCPB("com.r3.corda.notary.plugin.nonvalidating:notary-plugin-non-validating-server:$cordaNotaryPluginsVersion") {
artifact {
classifier = 'package'
extension = 'cpb'
}
}
implementation "org.codehaus.groovy:groovy-json:3.0.9"
}
// task groupings
def cordaGroup = 'csde-corda' // corda lifecycle tasks
def cordappGroup = 'csde-cordapp' // tasks to build and deploy corDapps
def queriesGroup = 'csde-queries' // tasks which query corda status
def supportingGroup = 'supporting' // tasks which should be hidden from the csde user
def pluginGroupName = "CSDE"
def pluginImplGroupName = "other"
def cordaBinDir= System.getProperty('user.home') + "/.corda/corda5"
def cordaCliBinDir = System.getProperty('user.home') + "/.corda/cli"
def cordaBinDir = System.getenv("CSDE_CORDA_BIN") ?: System.getProperty('user.home') + "/.corda/corda5"
def cordaCliBinDir = System.getenv("CSDE_CORDA_CLI") ?:System.getProperty('user.home') + "/.corda/cli"
def cordaJDBCDir = cordaBinDir + "/jdbcDrivers"
def cordaNotaryServerDir = cordaBinDir + "/notaryserver"
def signingCertAlias="gradle-plugin-default-key"
// Get error if this is not a autotyped object
// def signingCertFName = "$rootDir/config/gradle-plugin-default-key.pem"
@ -41,25 +77,75 @@ def keystoreAlias = "my-signing-key"
def keystoreFName = devEnvWorkspace + "/signingkeys.pfx"
def keystoreCertFName = devEnvWorkspace + "/signingkey1.pem"
def combiWorkerPidCacheFile = devEnvWorkspace + "/CordaPID.dat"
// todo: can we rely on the build directory always being /workflow/build? aslo, is the
// workflow directory the correct place to build the cpb to. shoudl it be the main build directory.
def workflowBuildDir = rootDir.toString() + "/workflows/build"
// Need to read things from cordapp plugin
def cpiName = 'cpi name'
// todo: Need to read things from cordapp plugin - the cordapp names will be changed by the user
def appCpiName = 'cpi name'
def notaryCpiName = 'CSDE Notary Server CPI'
def csdeHelper = new CsdeRpcInterface(project,
// todo: there should be a better way to set up these project context variables.
def projectContext = new ProjectContext(project,
cordaClusterURL.toString(),
cordaRpcUser,
cordaRpcPasswd,
devEnvWorkspace,
// todo: why is this not obtained in the groovy def's abouve - its inconsistent.
new String("${System.getProperty("java.home")}/bin"),
dbContainerName,
cordaJDBCDir,
combiWorkerPidCacheFile
combiWorkerPidCacheFile,
signingCertAlias,
signingCertFName,
keystoreAlias,
keystoreFName,
keystoreCertFName,
appCpiName,
notaryCpiName,
devEnvWorkspace,
cordaCliBinDir,
cordaNotaryServerDir,
workflowBuildDir,
cordaNotaryPluginsVersion
)
def utils = new ProjectUtils()
// Initiate workspace folder
tasks.register('projInit') {
group = supportingGroup
doLast {
mkdir devEnvWorkspace
}
}
// CordaLifeCycle tasks
def cordaLifeCycle = new CordaLifeCycleHelper(projectContext)
tasks.register("startCorda") {
group = cordaGroup
dependsOn('getDevCordaLite', 'getPostgresJDBC')
doLast {
mkdir devEnvWorkspace
cordaLifeCycle.startCorda()
}
}
tasks.register("stopCorda") {
group = cordaGroup
doLast {
cordaLifeCycle.stopCorda()
}
}
tasks.register("getPostgresJDBC") {
group = pluginImplGroupName
group = supportingGroup
doLast {
copy {
from configurations.myPostgresJDBC
@ -68,183 +154,99 @@ tasks.register("getPostgresJDBC") {
}
}
tasks.register('projInit') {
group = pluginImplGroupName
doLast {
mkdir devEnvWorkspace
}
}
tasks.register("createGroupPolicy") {
group = pluginImplGroupName
dependsOn('projInit')
doLast {
def groupPolicyFName = new String("${devEnvWorkspace}/GroupPolicy.json")
def devnetFName = new String("$rootDir/config/dev-net.json")
File groupPolicyFile = new File(groupPolicyFName)
File devnetFile = new File(devnetFName)
if (!groupPolicyFile.exists() || groupPolicyFile.lastModified() < devnetFile.lastModified()) {
def configX500Ids = csdeHelper.getConfigX500Ids()
println("createGroupPolicy: Creating a GroupPolicy")
javaexec {
classpath = files("$cordaCliBinDir/corda-cli.jar")
jvmArgs = ["-Dpf4j.pluginsDir=$cordaCliBinDir/plugins/"]
standardOutput = new FileOutputStream(groupPolicyFName)
LinkedList<String> myArgs = new LinkedList<String>()
myArgs.add("mgm")
myArgs.add("groupPolicy")
configX500Ids.forEach {
myArgs.add("--name")
myArgs.add("$it")
}
myArgs.add("--endpoint-protocol=1")
myArgs.add("--endpoint=http://localhost:1080")
args = myArgs
}
} else {
println("createPolicyTask: everything up to date; nothing to do.")
}
}
}
tasks.register("getDevCordaLite", Copy) {
group = pluginImplGroupName
group = supportingGroup
from configurations.combinedWorker
into cordaBinDir
}
tasks.register('createKeystore') {
group = pluginImplGroupName
dependsOn('projInit')
doLast {
File keystoreFile = new File(keystoreFName)
if(!keystoreFile.exists()) {
println('createKeystore: Create a keystore')
exec {
commandLine "${System.getProperty("java.home")}/bin/keytool", "-genkeypair",
"-alias", keystoreAlias,
"-keystore", keystoreFName,
"-storepass", "keystore password",
"-dname", "CN=CPI Example - My Signing Key, O=CorpOrgCorp, L=London, C=GB",
"-keyalg", "RSA",
"-storetype", "pkcs12",
"-validity", "4000"
}
// Need to add the default signing key to the keystore
exec {
commandLine "${System.getProperty("java.home")}/bin/keytool", "-importcert",
"-keystore", keystoreFName,
"-storepass", "keystore password",
"-noprompt",
"-alias", signingCertAlias,
"-file", signingCertFName
}
// keytool -exportcert -rfc -alias "signing key 1" -keystore signingkeys.pfx -storepass "keystore password" -file signingkey1.pem
exec {
commandLine "${System.getProperty("java.home")}/bin/keytool",
"-exportcert", "-rfc", "-alias", keystoreAlias,
"-keystore", keystoreFName,
"-storepass", "keystore password",
"-file", keystoreCertFName
}
}
else {
println('createKeystore: keystore already created; nothing to do.')
}
}
}
// Corda status queries
def cordaStatusQueries = new CordaStatusQueries(projectContext)
tasks.register('buildCPI') {
group = pluginGroupName
dependsOn('build', 'createGroupPolicy', 'createKeystore')
doLast{
def cpiFile= buildDir.toString() + "/" + project.archivesBaseName + "-" + project.version + ".cpi"
delete { delete cpiFile }
File srcDir
srcDir = file('build/libs')
// Create a file collection using a closure
def collection = layout.files { srcDir.listFiles() }
def cpbs = collection.filter { it.getName().endsWith(".cpb") }
javaexec {
classpath = files("$cordaCliBinDir/corda-cli.jar")
jvmArgs = ["-Dpf4j.pluginsDir=$cordaCliBinDir/plugins/"]
args = ['package', 'create-cpi',
'--cpb', cpbs.singleFile.absolutePath,
'--group-policy', "${devEnvWorkspace}/GroupPolicy.json",
'--cpi-name', cpiName,
'--cpi-version', project.version,
'--file', cpiFile,
'--keystore', "${devEnvWorkspace}/signingkeys.pfx",
'--storepass', 'keystore password',
'--key', 'my-signing-key' ]
}
}
}
tasks.register("deployCPI") {
group = pluginImplGroupName
dependsOn('buildCPI')
doLast {
csdeHelper.uploadCertificate(signingCertAlias, signingCertFName)
csdeHelper.uploadCertificate(keystoreAlias, keystoreCertFName)
csdeHelper.deployCPI("${buildDir}/${project.archivesBaseName}-${project.version}.cpi", cpiName, project.version)
}
}
tasks.register("createAndRegVNodes") {
group = pluginImplGroupName
dependsOn('deployCPI')
doLast {
csdeHelper.createAndRegVNodes()
}
}
tasks.register('listVNodes') {
group = pluginGroupName
group = queriesGroup
doLast {
csdeHelper.listVNodes()
cordaStatusQueries.listVNodes()
}
}
tasks.register('listCPIs') {
group = pluginImplGroupName
group = queriesGroup
doLast {
csdeHelper.listCPIs()
cordaStatusQueries.listCPIs()
}
}
// Build CPI tasks
def buildCPIsHelper = new BuildCPIsHelper(projectContext)
tasks.register("1-createGroupPolicy") {
group = cordappGroup
dependsOn('projInit')
doLast {
buildCPIsHelper.createGroupPolicy()
}
}
tasks.register("getNotaryServerCPB", Copy) {
group = supportingGroup
from configurations.notaryServerCPB
into cordaNotaryServerDir
}
tasks.register('2-createKeystore') {
group = cordappGroup
dependsOn('projInit')
doLast {
buildCPIsHelper.createKeyStore()
}
}
tasks.register('3-buildCPIs') {
group = cordappGroup
def dependsOnTasks = subprojects.collect {it.tasks.findByName("build") }
dependsOnTasks.add('1-createGroupPolicy')
dependsOnTasks.add('2-createKeystore')
dependsOnTasks.add('getNotaryServerCPB')
dependsOn dependsOnTasks
doLast{
buildCPIsHelper.buildCPIs()
}
}
// deploy CPI tasks
def deployCPIsHelper = new DeployCPIsHelper(projectContext)
tasks.register("4-deployCPIs") {
group = cordappGroup
dependsOn('3-buildCPIs')
doLast {
deployCPIsHelper.deployCPIs()
}
}
// create and register Vnodes Tasks
def createAndRegisterVNodesHelper = new CreateAndRegisterVNodesHelper(projectContext)
tasks.register("5-createAndRegVNodes") {
group = cordappGroup
dependsOn('4-deployCPIs')
doLast {
createAndRegisterVNodesHelper.createAndRegVNodes()
}
}
// Empty task, just acts as the Task user entry point task.
tasks.register('deployCordapp') {
group = pluginGroupName
dependsOn("createAndRegVNodes")
}
tasks.register("startCorda") {
group = pluginGroupName
dependsOn('getDevCordaLite', 'getPostgresJDBC')
doLast {
mkdir devEnvWorkspace
csdeHelper.startCorda()
}
}
tasks.register("stopCorda") {
group = pluginGroupName
doLast {
csdeHelper.stopCorda()
}
tasks.register('quickDeployCordapp') {
group = cordappGroup
dependsOn("5-createAndRegVNodes")
}

View File

@ -0,0 +1,270 @@
package com.r3.csde;
import java.io.*;
import java.util.LinkedList;
public class BuildCPIsHelper {
public ProjectContext pc;
public ProjectUtils utils ;
public BuildCPIsHelper(ProjectContext _pc) {
pc = _pc;
utils = new ProjectUtils(pc);
}
public void createGroupPolicy() throws IOException {
File groupPolicyFile = new File(String.format("%s/GroupPolicy.json", pc.devEnvWorkspace));
File devnetFile = new File(String.format("%s/config/dev-net.json", pc.project.getRootDir()));
if (!groupPolicyFile.exists() || groupPolicyFile.lastModified() < devnetFile.lastModified()) {
pc.out.println("createGroupPolicy: Creating a GroupPolicy");
LinkedList<String> configX500Ids = utils.getConfigX500Ids(pc.X500ConfigFile);
LinkedList<String> commandList = new LinkedList<>();
commandList.add(String.format("%s/java", pc.javaBinDir));
commandList.add(String.format("-Dpf4j.pluginsDir=%s/plugins/", pc.cordaCliBinDir));
commandList.add("-jar");
commandList.add(String.format("%s/corda-cli.jar", pc.cordaCliBinDir));
commandList.add("mgm");
commandList.add("groupPolicy");
for (String id : configX500Ids) {
commandList.add("--name");
commandList.add(id);
}
commandList.add("--endpoint-protocol=1");
commandList.add("--endpoint=http://localhost:1080");
ProcessBuilder pb = new ProcessBuilder(commandList);
pb.redirectErrorStream(true);
Process proc = pb.start();
BufferedReader reader = new BufferedReader(new InputStreamReader(proc.getInputStream()));
// todo add exception catching
FileWriter fileWriter = new FileWriter(groupPolicyFile);
String line;
while (( line = reader.readLine()) != null){
fileWriter.write(line + "\n");
}
fileWriter.close();
} else {
pc.out.println("createPolicyTask: everything up to date; nothing to do.");
}
}
public void createKeyStore() throws IOException, InterruptedException {
File keystoreFile = new File(pc.keystoreFName);
if(!keystoreFile.exists()) {
pc.out.println("createKeystore: Create a keystore");
generateKeyPair();
addDefaultSigningKey();
exportCert();
} else {
pc.out.println("createKeystore: keystore already created; nothing to do.");
}
}
private void generateKeyPair() throws IOException, InterruptedException {
LinkedList<String> cmdArray = new LinkedList<>();
cmdArray.add(pc.javaBinDir + "/keytool");
cmdArray.add("-genkeypair");
cmdArray.add("-alias");
cmdArray.add(pc.keystoreAlias);
cmdArray.add("-keystore");
cmdArray.add(pc.keystoreFName);
cmdArray.add("-storepass");
cmdArray.add("keystore password");
cmdArray.add("-dname");
cmdArray.add("CN=CPI Example - My Signing Key, O=CorpOrgCorp, L=London, C=GB");
cmdArray.add("-keyalg");
cmdArray.add("RSA");
cmdArray.add("-storetype");
cmdArray.add("pkcs12");
cmdArray.add("-validity");
cmdArray.add("4000");
ProcessBuilder pb = new ProcessBuilder(cmdArray);
pb.redirectErrorStream(true);
Process proc = pb.start();
proc.waitFor();
}
private void addDefaultSigningKey() throws IOException, InterruptedException {
LinkedList<String> cmdArray = new LinkedList<>();
cmdArray.add(pc.javaBinDir + "/keytool");
cmdArray.add("-importcert");
cmdArray.add("-keystore");
cmdArray.add(pc.keystoreFName);
cmdArray.add("-storepass");
cmdArray.add("keystore password");
cmdArray.add("-noprompt");
cmdArray.add("-alias");
cmdArray.add(pc.signingCertAlias);
cmdArray.add("-file");
cmdArray.add(pc.signingCertFName);
ProcessBuilder pb = new ProcessBuilder(cmdArray);
pb.redirectErrorStream(true);
Process proc = pb.start();
proc.waitFor();
}
private void exportCert() throws IOException, InterruptedException {
LinkedList<String> cmdArray = new LinkedList<>();
cmdArray.add(pc.javaBinDir + "/keytool");
cmdArray.add("-exportcert");
cmdArray.add("-rfc");
cmdArray.add("-alias");
cmdArray.add(pc.keystoreAlias);
cmdArray.add("-keystore");
cmdArray.add(pc.keystoreFName);
cmdArray.add("-storepass");
cmdArray.add("keystore password");
cmdArray.add("-file");
cmdArray.add(pc.keystoreCertFName);
ProcessBuilder pb = new ProcessBuilder(cmdArray);
pb.redirectErrorStream(true);
Process proc = pb.start();
proc.waitFor();
}
public void buildCPIs() throws IOException, InterruptedException, CsdeException {
createCorDappCPI();
createNotaryCPI();
}
private void createCorDappCPI() throws IOException, InterruptedException, CsdeException {
String appCPIFilePath = pc.workflowBuildDir + "/" +
pc.project.getRootProject().getName() + "-" +
pc.project.getVersion() + ".cpi";
File appCPIFile = new File(appCPIFilePath);
appCPIFile.delete();
File srcDir = new File(pc.workflowBuildDir + "/libs");
File[] appCPBs = srcDir.listFiles(( x , name ) -> name.endsWith(".cpb"));
if (appCPBs == null) throw new CsdeException("Expecting exactly one CPB but no CPB found.");
if (appCPBs.length != 1) throw new CsdeException("Expecting exactly one CPB but more than one found.");
pc.out.println("appCpbs:");
pc.out.println(appCPBs[0].getAbsolutePath());
LinkedList<String> commandList = new LinkedList<>();
commandList.add(String.format("%s/java", pc.javaBinDir));
commandList.add(String.format("-Dpf4j.pluginsDir=%s/plugins/", pc.cordaCliBinDir));
commandList.add("-jar");
commandList.add(String.format("%s/corda-cli.jar", pc.cordaCliBinDir));
commandList.add("package");
commandList.add("create-cpi");
commandList.add("--cpb");
commandList.add(appCPBs[0].getAbsolutePath());
commandList.add("--group-policy");
commandList.add(pc.devEnvWorkspace + "/GroupPolicy.json");
commandList.add("--cpi-name");
commandList.add(pc.appCPIName);
commandList.add("--cpi-version");
commandList.add(pc.project.getVersion().toString());
commandList.add("--file");
commandList.add(appCPIFilePath);
commandList.add("--keystore");
commandList.add(pc.devEnvWorkspace + "/signingkeys.pfx");
commandList.add("--storepass");
commandList.add("keystore password");
commandList.add("--key");
commandList.add("my-signing-key"); // todo: should be passed as context property
ProcessBuilder pb = new ProcessBuilder(commandList);
pb.redirectErrorStream(true);
Process proc = pb.start();
proc.waitFor();
// todo: work out how to capture error code better than the following code
// BufferedReader reader = new BufferedReader(new InputStreamReader(proc.getInputStream()));
// File tempOutputFile = new File(String.format("%s/tempOutput.txt", pc.devEnvWorkspace));
// tempOutputFile.delete();
// FileWriter fileWriter = new FileWriter(tempOutputFile);
// String line;
// while (( line = reader.readLine()) != null){
// fileWriter.write(line + "\n");
// }
// fileWriter.close();
}
private void createNotaryCPI() throws CsdeException, IOException, InterruptedException {
String notaryCPIFilePath = pc.workflowBuildDir + "/" +
pc.notaryCPIName.replace(' ', '-').toLowerCase() + "-" +
pc.project.getVersion() + ".cpi";
File notaryCPIFile = new File(notaryCPIFilePath);
notaryCPIFile.delete();
File srcDir = new File(pc.cordaNotaryServiceDir);
File[] notaryCPBs = srcDir.listFiles(( x , name ) -> name.endsWith(".cpb") && name.contains(pc.cordaNotaryPluginsVersion));
if (notaryCPBs == null) throw new CsdeException("Expecting exactly one notary CPB but no CPB found.");
if (notaryCPBs.length != 1) throw new CsdeException("Expecting exactly one notary CPB but more than one found.");
pc.out.println("notaryCpbs:");
pc.out.println(notaryCPBs[0]);
LinkedList<String> commandList = new LinkedList<>();
commandList.add(String.format("%s/java", pc.javaBinDir));
commandList.add(String.format("-Dpf4j.pluginsDir=%s/plugins/", pc.cordaCliBinDir));
commandList.add("-jar");
commandList.add(String.format("%s/corda-cli.jar", pc.cordaCliBinDir));
commandList.add("package");
commandList.add("create-cpi");
commandList.add("--cpb");
commandList.add(notaryCPBs[0].getAbsolutePath());
commandList.add("--group-policy");
commandList.add(pc.devEnvWorkspace + "/GroupPolicy.json");
commandList.add("--cpi-name");
commandList.add(pc.notaryCPIName);
commandList.add("--cpi-version");
commandList.add(pc.project.getVersion().toString());
commandList.add("--file");
commandList.add(notaryCPIFilePath);
commandList.add("--keystore");
commandList.add(pc.devEnvWorkspace + "/signingkeys.pfx");
commandList.add("--storepass");
commandList.add("keystore password");
commandList.add("--key");
commandList.add("my-signing-key");
ProcessBuilder pb = new ProcessBuilder(commandList);
pb.redirectErrorStream(true);
Process proc = pb.start();
proc.waitFor();
}
// todo: this might be needed for improved logging
private void printCmdArray(LinkedList<String> cmdArray) {
for (int i = 0; i < cmdArray.size(); i++) {
pc.out.print(cmdArray.get(i) + " ");
}
}
}

View File

@ -0,0 +1,94 @@
package com.r3.csde;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.PrintStream;
import java.util.Scanner;
/**
* Manages Bringing corda up, testing for liveness and taking corda down
*/
public class CordaLifeCycleHelper {
ProjectContext pc;
ProjectUtils utils;
public CordaLifeCycleHelper(ProjectContext _pc) {
pc = _pc;
utils = new ProjectUtils(pc);
}
public void startCorda() throws IOException {
PrintStream pidStore = new PrintStream(new FileOutputStream(pc.cordaPidCache));
File combinedWorkerJar = pc.project.getConfigurations().getByName("combinedWorker").getSingleFile();
// todo: make consistent with other ProcessBuilder set ups (use cmdArray)
new ProcessBuilder(
"docker",
"run", "-d", "--rm",
"-p", "5432:5432",
"--name", pc.dbContainerName,
"-e", "POSTGRES_DB=cordacluster",
"-e", "POSTGRES_USER=postgres",
"-e", "POSTGRES_PASSWORD=password",
"postgres:latest").start();
// todo: is there a better way of doing this - ie poll for readiness
utils.rpcWait(10000);
ProcessBuilder procBuild = new ProcessBuilder(pc.javaBinDir + "/java",
"-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005",
"-Dco.paralleluniverse.fibers.verifyInstrumentation=true",
"-jar",
combinedWorkerJar.toString(),
"--instanceId=0",
"-mbus.busType=DATABASE",
"-spassphrase=password",
"-ssalt=salt",
"-spassphrase=password",
"-ssalt=salt",
"-ddatabase.user=user",
"-ddatabase.pass=password",
"-ddatabase.jdbc.url=jdbc:postgresql://localhost:5432/cordacluster",
"-ddatabase.jdbc.directory="+pc.JDBCDir);
procBuild.redirectErrorStream(true);
Process proc = procBuild.start();
pidStore.print(proc.pid());
pc.out.println("Corda Process-id="+proc.pid());
// todo: should poll for readiness before returning
// Chris comment - We probably do not want to poll for readiness here.
// The combined-worker takes serveral minutes to come up.
// It might be better to warn the user of that and have the readiness detection and polling logic used in other tasks involved in creating v-nodes and deploying the CPI.
// Matt comment - I'm not sure I agree, we need to investigate
}
public void stopCorda() throws IOException, CsdeException {
File cordaPIDFile = new File(pc.cordaPidCache);
if(cordaPIDFile.exists()) {
Scanner sc = new Scanner(cordaPIDFile);
long pid = sc.nextLong();
pc.out.println("pid to kill=" + pid);
if (System.getProperty("os.name").toLowerCase().contains("windows")) {
new ProcessBuilder("Powershell", "-Command", "Stop-Process", "-Id", Long.toString(pid), "-PassThru").start();
} else {
new ProcessBuilder("kill", "-9", Long.toString(pid)).start();
}
Process proc = new ProcessBuilder("docker", "stop", pc.dbContainerName).start();
cordaPIDFile.delete();
}
else {
throw new CsdeException("Cannot stop the Combined worker\nCached process ID file " + pc.cordaPidCache + " missing.\nWas the combined worker not started?");
}
}
}

View File

@ -0,0 +1,63 @@
package com.r3.csde;
import kong.unirest.JsonNode;
import kong.unirest.Unirest;
import kong.unirest.json.JSONArray;
import kong.unirest.json.JSONObject;
import kong.unirest.HttpResponse;
public class CordaStatusQueries {
ProjectContext pc;
public CordaStatusQueries(ProjectContext _pc){ pc = _pc; }
public HttpResponse<JsonNode> getVNodeInfo() {
Unirest.config().verifySsl(false);
return Unirest.get(pc.baseURL + "/api/v1/virtualnode/")
.basicAuth(pc.rpcUser, pc.rpcPasswd)
.asJson();
}
public void listVNodesVerbose() {
HttpResponse<JsonNode> vnodeResponse = getVNodeInfo();
pc.out.println("VNodes:\n" + vnodeResponse.getBody().toPrettyString());
}
// X500Name, shorthash, cpiname
public void listVNodes() {
HttpResponse<JsonNode> vnodeResponse = getVNodeInfo();
JSONArray virtualNodesJson = (JSONArray) vnodeResponse.getBody().getObject().get("virtualNodes");
pc.out.println("X500 Name\tHolding identity short hash\tCPI Name");
for(Object o: virtualNodesJson){
if(o instanceof JSONObject) {
JSONObject idObj = ((JSONObject) o).getJSONObject("holdingIdentity");
JSONObject cpiObj = ((JSONObject) o).getJSONObject("cpiIdentifier");
pc.out.print("\"" + idObj.get("x500Name") + "\"");
pc.out.print("\t\"" + idObj.get("shortHash") + "\"");
pc.out.println("\t\"" + cpiObj.get("cpiName") + "\"");
}
}
}
public HttpResponse<JsonNode> getCpiInfo() {
Unirest.config().verifySsl(false);
return Unirest.get(pc.baseURL + "/api/v1/cpi/")
.basicAuth(pc.rpcUser, pc.rpcPasswd)
.asJson();
}
public void listCPIs() {
HttpResponse<JsonNode> cpiResponse = getCpiInfo();
JSONArray jArray = (JSONArray) cpiResponse.getBody().getObject().get("cpis");
for(Object o: jArray){
if(o instanceof JSONObject) {
JSONObject idObj = ((JSONObject) o).getJSONObject("id");
pc.out.print("cpiName=" + idObj.get("cpiName"));
pc.out.println(", cpiVersion=" + idObj.get("cpiVersion"));
}
}
}
}

View File

@ -0,0 +1,289 @@
package com.r3.csde;
import com.fasterxml.jackson.databind.ObjectMapper;
import kong.unirest.HttpResponse;
import kong.unirest.JsonNode;
import kong.unirest.Unirest;
import kong.unirest.json.JSONArray;
import kong.unirest.json.JSONObject;
import net.corda.v5.base.types.MemberX500Name;
import org.jetbrains.annotations.NotNull;
import javax.naming.ConfigurationException;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import static java.net.HttpURLConnection.*;
public class CreateAndRegisterVNodesHelper {
ProjectContext pc;
ProjectUtils utils;
CordaStatusQueries queries;
public CreateAndRegisterVNodesHelper(ProjectContext _pc) {
pc = _pc;
queries = new CordaStatusQueries(pc);
utils = new ProjectUtils(pc);
}
public void createAndRegVNodes() throws IOException, CsdeException, ConfigurationException {
Unirest.config().verifySsl(false);
String appCpiCheckSum = getLastCPIUploadChkSum( pc.CPIUploadStatusFName );
String notaryCpiCheckSum = getLastCPIUploadChkSum( pc.CPIUploadStatusFName, "-NotaryServer" );
LinkedList<String> x500Ids = utils.getConfigX500Ids(pc.X500ConfigFile);
// For each identity check that it already exists.
Set<MemberX500Name> existingX500 = new HashSet<>();
HttpResponse<JsonNode> vnodeListResponse = queries.getVNodeInfo();
JSONArray virtualNodesJson = (JSONArray) vnodeListResponse.getBody().getObject().get("virtualNodes");
for(Object o: virtualNodesJson){
if(o instanceof JSONObject) {
JSONObject idObj = ((JSONObject) o).getJSONObject("holdingIdentity");
String x500id = (String) idObj.get("x500Name");
existingX500.add(MemberX500Name.parse( x500id) );
}
}
Map<String, CompletableFuture<HttpResponse<JsonNode>>> responses = new LinkedHashMap<>();
// Create the VNodes
for(String x500id: x500Ids) {
if(!existingX500.contains(MemberX500Name.parse(x500id) )) {
String cpiCheckSum = getNotaryRepresentatives().containsKey(x500id) ? notaryCpiCheckSum : appCpiCheckSum;
pc.out.println("Creating VNode for x500id=\"" + x500id + "\" cpi checksum=" + cpiCheckSum);
responses.put(x500id, Unirest
.post(pc.baseURL + "/api/v1/virtualnode")
.body("{ \"request\" : { \"cpiFileChecksum\": " + cpiCheckSum + ", \"x500Name\": \"" + x500id + "\" } }")
.basicAuth(pc.rpcUser, pc.rpcPasswd)
.asJsonAsync()
);
}
else {
pc.out.println("Not creating a vnode for \"" + x500id + "\", vnode already exists.");
}
}
pc.out.println("Waiting for VNode creation results...");
for (Map.Entry<String, CompletableFuture<HttpResponse<JsonNode>>> response: responses.entrySet()) {
try {
HttpResponse<JsonNode> jsonNode = response.getValue().get();
// need to check this and report errors.
// 200/HTTP_OK - OK
// 409/HTTP_CONFLICT - Vnode already exists
// 500/HTTP_INTERNAL_ERROR
// - Can mean that the request timed out.
// - However, the cluster may still have created the V-node successfully, so we want to poll later.
pc.out.println("Vnode creation end point status:" + jsonNode.getStatus());
switch(jsonNode.getStatus()) {
case HTTP_OK: break;
case HTTP_CONFLICT: break;
case HTTP_INTERNAL_ERROR: break;
default:
utils.reportError(jsonNode);
}
} catch (ExecutionException | InterruptedException e) {
throw new CsdeException("Unexpected exception while waiting for response to " +
"membership submission for holding identity" + response.getKey());
}
}
Map<String, String> OKHoldingX500AndShortIds = pollForVNodeShortHoldingHashIds(x500Ids, 60, 5000);
// Register the VNodes
responses.clear();
for(String okId: OKHoldingX500AndShortIds.keySet()) {
responses.put(okId, Unirest
.post(pc.baseURL + "/api/v1/membership/" + OKHoldingX500AndShortIds.get(okId))
.body(getMemberRegistrationBody(okId))
.basicAuth(pc.rpcUser, pc.rpcPasswd)
.asJsonAsync( response ->
pc.out.println("Vnode membership submission for \"" + okId + "\"" +
System.lineSeparator() + response.getBody().toPrettyString()))
);
}
pc.out.println("Vnode membership requests submitted, waiting for acknowledgement from MGM...");
for (Map.Entry<String, CompletableFuture<HttpResponse<JsonNode>>> response: responses.entrySet()) {
try {
response.getValue().get();
} catch (ExecutionException | InterruptedException e) {
throw new CsdeException("Unexpected exception while waiting for response to " +
"membership submission for holding identity" + response.getKey());
}
}
pollForCompleteMembershipRegistration(OKHoldingX500AndShortIds);
}
public String getLastCPIUploadChkSum(@NotNull String CPIUploadStatusFName) throws IOException, NullPointerException {
return getLastCPIUploadChkSum(CPIUploadStatusFName, "");
}
public String getLastCPIUploadChkSum(@NotNull String CPIUploadStatusFName,
String uploadStatusQualifier) throws IOException, NullPointerException {
String qualifiedCPIUploadStatusFName =
CPIUploadStatusFName.replace(".json", uploadStatusQualifier + ".json");
ObjectMapper mapper = new ObjectMapper();
FileInputStream in = new FileInputStream(qualifiedCPIUploadStatusFName);
com.fasterxml.jackson.databind.JsonNode jsonNode = mapper.readTree(in);
String checksum = jsonNode.get("cpiFileChecksum").toString();
if(checksum == null || checksum.equals("null")) {
throw new NullPointerException("Missing cpiFileChecksum in file " +
qualifiedCPIUploadStatusFName + " with contents:" + jsonNode);
}
return checksum;
}
// KV pairs of representative x500 name and corresponding notary service x500 name
public Map<String, String> getNotaryRepresentatives() throws IOException, ConfigurationException {
if (pc.notaryRepresentatives == null) {
pc.notaryRepresentatives = new HashMap<>();
ObjectMapper mapper = new ObjectMapper();
FileInputStream in = new FileInputStream(pc.X500ConfigFile);
com.fasterxml.jackson.databind.JsonNode jsonNode = mapper.readTree(in);
List<String> identities = utils.getConfigX500Ids(pc.X500ConfigFile);
for (com.fasterxml.jackson.databind.JsonNode notary : jsonNode.get("notaries")) {
String svcX500Id = utils.jsonNodeToString(notary.get("serviceX500Name"));
com.fasterxml.jackson.databind.JsonNode repsForThisService = notary.get("representatives");
if (repsForThisService.isEmpty()) {
throw new ConfigurationException(
"Notary service \"" + svcX500Id + "\" must have at least one representative.");
} else if (repsForThisService.size() > 1) {
// Temporary restriction while the MGM only supports a 1-1 association
throw new ConfigurationException(
"Notary service \"" + svcX500Id + "\" can only have a single representative at this time.");
}
for (com.fasterxml.jackson.databind.JsonNode representative : repsForThisService) {
String repAsString = utils.jsonNodeToString(representative);
if (identities.contains(repAsString)) {
pc.notaryRepresentatives.put(repAsString, svcX500Id);
} else {
throw new ConfigurationException(
"Notary representative \"" + repAsString + "\" is not a valid identity");
}
}
}
}
return pc.notaryRepresentatives;
}
private String getMemberRegistrationBody(String memberX500Name) throws ConfigurationException, IOException {
Map<String, String> notaryReps = getNotaryRepresentatives();
String context = "\"corda.key.scheme\" : \"CORDA.ECDSA.SECP256R1\"" + (
notaryReps.containsKey(memberX500Name)
? ", \"corda.roles.0\" : \"notary\", " +
"\"corda.notary.service.name\" : \"" + notaryReps.get(memberX500Name) + "\", " +
// This will need revisiting in the long term when additional protocols are added, and will
// need to be specified in config. We will also need to review the hard-coded name once
// notary plugin selection logic is re-instated in CORE-7248.
"\"corda.notary.service.plugin\" : \"net.corda.notary.NonValidatingNotary\""
: ""
);
return "{ \"memberRegistrationRequest\": { \"action\": \"requestJoin\", \"context\": { " + context + " } } }";
}
Map<String, String> pollForVNodeShortHoldingHashIds(List<String> x500Ids, int retryCount, int coolDownMs ) throws CsdeException {
HashMap<String, String> x500NameToShortHashes = new HashMap<>();
Set<String> vnodesToCheck = new HashSet<String>(x500Ids);
while(!vnodesToCheck.isEmpty() && retryCount-- > 0) {
utils.rpcWait(coolDownMs);
kong.unirest.json.JSONArray virtualNodes = (JSONArray) queries.getVNodeInfo().getBody().getObject().get("virtualNodes");
Map<String, String> vnodesMap = new HashMap<String, String>();
for (Object virtualNode : virtualNodes) {
if (virtualNode instanceof JSONObject) {
JSONObject idObj = ((JSONObject) virtualNode).getJSONObject("holdingIdentity");
vnodesMap.put(idObj.get("x500Name").toString(), idObj.get("shortHash").toString());
}
}
for(String x500Name: vnodesToCheck) {
if(vnodesMap.containsKey(x500Name)) {
x500NameToShortHashes.put(x500Name, vnodesMap.get(x500Name));
}
}
vnodesMap.keySet().forEach(vnodesToCheck::remove);
}
if(!vnodesToCheck.isEmpty()) {
throw new CsdeException("VNode creation timed out. Not all expected vnodes were reported as created:" + vnodesToCheck.toString());
}
return x500NameToShortHashes;
}
private void pollForCompleteMembershipRegistration(Map<String, String> X500ToShortIdHash) throws CsdeException {
HashSet<String> vnodesToCheck = new HashSet<String>(X500ToShortIdHash.keySet());
LinkedList<String> approved = new LinkedList<String>();
while (!vnodesToCheck.isEmpty()) {
utils.rpcWait(2000);
approved.clear();
for (String vnodeX500 : vnodesToCheck) {
try {
pc.out.println("Checking membership registration progress for v-node '" + vnodeX500 + "':");
HttpResponse<JsonNode> statusResponse = Unirest
.get(pc.baseURL + "/api/v1/membership/" + X500ToShortIdHash.get(vnodeX500) + "/")
.basicAuth(pc.rpcUser, pc.rpcPasswd)
.asJson();
if (isMembershipRegComplete(statusResponse)) {
approved.add(vnodeX500);
}
} catch (Exception e) {
throw new CsdeException("Error when registering V-Node '" + vnodeX500 + "'", e);
}
}
approved.forEach(vnodesToCheck::remove);
}
}
private boolean isMembershipRegComplete(HttpResponse<JsonNode> response) throws CsdeException {
if(response.getStatus() == HTTP_OK) {
JsonNode responseBody = response.getBody();
pc.out.println(responseBody.toPrettyString());
if(responseBody.getArray().length() > 0) {
JSONObject memRegStatusInfo = (JSONObject) responseBody
.getArray()
.getJSONObject(0);
String memRegStatus = memRegStatusInfo.get("registrationStatus").toString();
if (memRegStatus.equals("DECLINED")) {
throw new CsdeException("V-Node membership registration declined by Corda");
}
return memRegStatus.equals("APPROVED");
}
else {
return false;
}
}
else {
utils.reportError(response);
}
return false;
}
}

View File

@ -1,6 +1,9 @@
package com.r3.csde;
public class CsdeException extends Exception {
public CsdeException(String message, Throwable cause) {
super(message, cause);
}
public CsdeException(String message){
super(message);
}

View File

@ -1,415 +0,0 @@
package com.r3.csde;
import kong.unirest.json.JSONArray;
import org.gradle.api.Project;
import net.corda.v5.base.types.MemberX500Name;
import kong.unirest.Unirest;
import kong.unirest.json.JSONObject;
import org.jetbrains.annotations.NotNull;
import java.io.*;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.Scanner;
import java.util.Set;
import static java.lang.Thread.sleep;
public class CsdeRpcInterface {
private Project project;
private String baseURL = "https://localhost:8888";
private String rpcUser = "admin";
private String rpcPasswd = "admin";
private String workspaceDir = "workspace";
static private int retryWaitMs = 1000;
static PrintStream out = System.out;
static private String CPIUploadStatusBaseName = "CPIFileStatus.json";
static private String CPIUploadStatusFName;
static private String X500ConfigFile = "config/dev-net.json";
static private String javaBinDir;
static private String cordaPidCache = "CordaPIDCache.dat";
static private String dbContainerName;
private String JDBCDir;
private String combinedWorkerBinRe;
public CsdeRpcInterface() {
}
public CsdeRpcInterface (Project inProject,
String inBaseUrl,
String inRpcUser,
String inRpcPasswd,
String inWorkspaceDir,
String inJavaBinDir,
String inDbContainerName,
String inJDBCDir,
String inCordaPidCache
) {
project = inProject;
baseURL = inBaseUrl;
rpcUser = inRpcUser;
rpcPasswd = inRpcPasswd;
workspaceDir = inWorkspaceDir;
javaBinDir = inJavaBinDir;
cordaPidCache = inCordaPidCache;
dbContainerName = inDbContainerName;
JDBCDir = inJDBCDir;
CPIUploadStatusFName = workspaceDir +"/"+ CPIUploadStatusBaseName;
}
static private void rpcWait(int millis) {
try {
sleep(millis);
}
catch(InterruptedException e) {
throw new UnsupportedOperationException("Interrupts not supported.", e);
}
}
static private void rpcWait() {
rpcWait(retryWaitMs);
}
public LinkedList<String> getConfigX500Ids() throws IOException {
LinkedList<String> x500Ids = new LinkedList<>();
com.fasterxml.jackson.databind.ObjectMapper mapper = new com.fasterxml.jackson.databind.ObjectMapper();
FileInputStream in = new FileInputStream(X500ConfigFile);
com.fasterxml.jackson.databind.JsonNode jsonNode = mapper.readTree(in);
for( com.fasterxml.jackson.databind.JsonNode identity: jsonNode.get("identities")) {
String idAsString = identity.toString();
x500Ids.add(idAsString.substring(1,idAsString.length()-1));
}
return x500Ids;
}
static public String getLastCPIUploadChkSum(@NotNull String CPIUploadStatusFName) throws IOException, NullPointerException {
com.fasterxml.jackson.databind.ObjectMapper mapper = new com.fasterxml.jackson.databind.ObjectMapper();
FileInputStream in = new FileInputStream(CPIUploadStatusFName);
com.fasterxml.jackson.databind.JsonNode jsonNode = mapper.readTree(in);
String checksum = jsonNode.get("cpiFileChecksum").toString();
if(checksum == null || checksum.equals("null")) {
throw new NullPointerException("Missing cpiFileChecksum in file " + CPIUploadStatusFName+ " with contents:" + jsonNode);
}
return checksum;
}
public void reportError(@NotNull kong.unirest.HttpResponse<kong.unirest.JsonNode> response) throws CsdeException {
out.println("*** *** ***");
out.println("Unexpected response from Corda");
out.println("Status="+ response.getStatus());
out.println("*** Headers ***\n"+ response.getHeaders());
out.println("*** Body ***\n"+ response.getBody());
out.println("*** *** ***");
throw new CsdeException("Error: unexpected response from Corda.");
}
public void downloadFile(String url, String targetPath) {
Unirest.get(url)
.asFile(targetPath)
.getBody();
}
public kong.unirest.HttpResponse<kong.unirest.JsonNode> getVNodeInfo() {
Unirest.config().verifySsl(false);
return Unirest.get(baseURL + "/api/v1/virtualnode/")
.basicAuth(rpcUser, rpcPasswd)
.asJson();
}
public void listVNodesVerbose() {
kong.unirest.HttpResponse<kong.unirest.JsonNode> vnodeResponse = getVNodeInfo();
out.println("VNodes:\n" + vnodeResponse.getBody().toPrettyString());
}
// X500Name, cpiname, shorthash,
public void listVNodes() {
kong.unirest.HttpResponse<kong.unirest.JsonNode> vnodeResponse = getVNodeInfo();
kong.unirest.json.JSONArray virtualNodesJson = (JSONArray) vnodeResponse.getBody().getObject().get("virtualNodes");
out.println("X500 Name\tHolding identity short hash");
for(Object o: virtualNodesJson){
if(o instanceof kong.unirest.json.JSONObject) {
kong.unirest.json.JSONObject idObj = ((kong.unirest.json.JSONObject) o).getJSONObject("holdingIdentity");
out.print("\"" + idObj.get("x500Name") + "\"");
out.println("\t\"" + idObj.get("shortHash") + "\"");
}
}
}
public kong.unirest.HttpResponse<kong.unirest.JsonNode> getCpiInfo() {
Unirest.config().verifySsl(false);
return Unirest.get(baseURL + "/api/v1/cpi/")
.basicAuth(rpcUser, rpcPasswd)
.asJson();
}
public void listCPIs() {
kong.unirest.HttpResponse<kong.unirest.JsonNode> cpiResponse = getCpiInfo();
kong.unirest.json.JSONArray jArray = (JSONArray) cpiResponse.getBody().getObject().get("cpis");
for(Object o: jArray){
if(o instanceof kong.unirest.json.JSONObject) {
kong.unirest.json.JSONObject idObj = ((kong.unirest.json.JSONObject) o).getJSONObject("id");
out.print("cpiName=" + idObj.get("cpiName"));
out.println(", cpiVersion=" + idObj.get("cpiVersion"));
}
}
}
public void uploadCertificate(String certAlias, String certFName) {
Unirest.config().verifySsl(false);
kong.unirest.HttpResponse<kong.unirest.JsonNode> uploadResponse = Unirest.put(baseURL + "/api/v1/certificates/cluster/code-signer")
.field("alias", certAlias)
.field("certificate", new File(certFName))
.basicAuth(rpcUser, rpcPasswd)
.asJson();
out.println("Certificate/key upload, alias "+certAlias+" certificate/key file "+certFName);
out.println(uploadResponse.getBody().toPrettyString());
}
public void forceuploadCPI(String cpiFName) throws FileNotFoundException, CsdeException {
Unirest.config().verifySsl(false);
kong.unirest.HttpResponse<kong.unirest.JsonNode> jsonResponse = Unirest.post(baseURL + "/api/v1/maintenance/virtualnode/forcecpiupload/")
.field("upload", new File(cpiFName))
.basicAuth(rpcUser, rpcPasswd)
.asJson();
if(jsonResponse.getStatus() == 200) {
String id = (String) jsonResponse.getBody().getObject().get("id");
out.println("get id:\n" +id);
kong.unirest.HttpResponse<kong.unirest.JsonNode> statusResponse = uploadStatus(id);
if (statusResponse.getStatus() == 200) {
PrintStream cpiUploadStatus = new PrintStream(new FileOutputStream(CPIUploadStatusFName));
cpiUploadStatus.print(statusResponse.getBody());
out.println("Caching CPI file upload status:\n" + statusResponse.getBody());
} else {
reportError(statusResponse);
}
}
else {
reportError(jsonResponse);
}
}
private boolean uploadStatusRetry(kong.unirest.HttpResponse<kong.unirest.JsonNode> response) {
int status = response.getStatus();
kong.unirest.JsonNode body = response.getBody();
// Do not retry on success
if(status == 200) {
// Keep retrying until we get "OK" may move through "Validateing upload", "Persisting CPI"
return !(body.getObject().get("status").equals("OK"));
}
else if (status == 400){
JSONObject details = response.getBody().getObject().getJSONObject("details");
if( details != null ){
String code = (String) details.getString("code");
return !code.equals("BAD_REQUEST");
}
else {
// 400 otherwise means some transient problem
return true;
}
}
return false;
}
public kong.unirest.HttpResponse<kong.unirest.JsonNode> uploadStatus(String requestId) {
kong.unirest.HttpResponse<kong.unirest.JsonNode> statusResponse = null;
do {
rpcWait(1000);
statusResponse = Unirest
.get(baseURL + "/api/v1/cpi/status/" + requestId + "/")
.basicAuth(rpcUser, rpcPasswd)
.asJson();
out.println("Upload status="+statusResponse.getStatus()+", status query response:\n"+statusResponse.getBody().toPrettyString());
}
while(uploadStatusRetry(statusResponse));
return statusResponse;
}
public void deployCPI(String cpiFName, String cpiName, String cpiVersion) throws FileNotFoundException, CsdeException {
Unirest.config().verifySsl(false);
kong.unirest.HttpResponse<kong.unirest.JsonNode> cpiResponse = getCpiInfo();
kong.unirest.json.JSONArray jArray = (JSONArray) cpiResponse.getBody().getObject().get("cpis");
int matches = 0;
for(Object o: jArray.toList() ) {
if(o instanceof JSONObject) {
JSONObject idObj = ((JSONObject) o).getJSONObject("id");
if((idObj.get("cpiName").toString().equals(cpiName)
&& idObj.get("cpiVersion").toString().equals(cpiVersion))) {
matches++;
}
}
}
out.println("Matching CPIS="+matches);
if(matches == 0) {
kong.unirest.HttpResponse<kong.unirest.JsonNode> uploadResponse = Unirest.post(baseURL + "/api/v1/cpi/")
.field("upload", new File(cpiFName))
.basicAuth(rpcUser, rpcPasswd)
.asJson();
kong.unirest.JsonNode body = uploadResponse.getBody();
int status = uploadResponse.getStatus();
out.println("Upload Status:" + status);
out.println("Pretty print the body\n" + body.toPrettyString());
// We expect the id field to be a string.
if (status == 200) {
String id = (String) body.getObject().get("id");
out.println("get id:\n" + id);
kong.unirest.HttpResponse<kong.unirest.JsonNode> statusResponse = uploadStatus(id);
if (statusResponse.getStatus() == 200) {
PrintStream cpiUploadStatus = new PrintStream(new FileOutputStream(CPIUploadStatusFName));
cpiUploadStatus.print(statusResponse.getBody());
out.println("Caching CPI file upload status:\n" + statusResponse.getBody());
} else {
reportError(statusResponse);
}
} else {
reportError(uploadResponse);
}
}
else {
out.println("CPI already uploaded doing a 'force' upload.");
forceuploadCPI(cpiFName);
}
}
public void createAndRegVNodes() throws IOException, CsdeException{
Unirest.config().verifySsl(false);
String cpiCheckSum = getLastCPIUploadChkSum( CPIUploadStatusFName );
LinkedList<String> x500Ids = getConfigX500Ids();
LinkedList<String> OKHoldingShortIds = new LinkedList<>();
// For each identity check that it already exists.
Set<MemberX500Name> existingX500 = new HashSet<>();
kong.unirest.HttpResponse<kong.unirest.JsonNode> vnodeListResponse = getVNodeInfo();
kong.unirest.json.JSONArray virtualNodesJson = (JSONArray) vnodeListResponse.getBody().getObject().get("virtualNodes");
for(Object o: virtualNodesJson){
if(o instanceof kong.unirest.json.JSONObject) {
kong.unirest.json.JSONObject idObj = ((kong.unirest.json.JSONObject) o).getJSONObject("holdingIdentity");
String x500id = (String) idObj.get("x500Name");
existingX500.add(MemberX500Name.parse( x500id) );
}
}
// Create the VNodes
for(String x500id: x500Ids) {
if(!existingX500.contains(MemberX500Name.parse(x500id) )) {
out.println("Creating VNode for x500id=\"" + x500id + "\" cpi checksum=" + cpiCheckSum);
kong.unirest.HttpResponse<kong.unirest.JsonNode> jsonNode = Unirest.post(baseURL + "/api/v1/virtualnode")
.body("{ \"request\" : { \"cpiFileChecksum\": " + cpiCheckSum + ", \"x500Name\": \"" + x500id + "\" } }")
.basicAuth(rpcUser, rpcPasswd)
.asJson();
// Logging.
// need to check this and report errors.
// 200 - OK
// 409 - Vnode already exists
if (jsonNode.getStatus() != 409) {
if (jsonNode.getStatus() != 200) {
reportError(jsonNode);
} else {
JSONObject thing = jsonNode.getBody().getObject().getJSONObject("holdingIdentity");
String shortHash = (String) thing.get("shortHash");
OKHoldingShortIds.add(shortHash);
}
}
}
else {
out.println("Not creating a vnode for \"" + x500id + "\", vnode already exists.");
}
}
// Register the VNodes
for(String shortHoldingIdHash: OKHoldingShortIds) {
kong.unirest.HttpResponse<kong.unirest.JsonNode> vnodeResponse = Unirest.post(baseURL + "/api/v1/membership/" + shortHoldingIdHash)
.body("{ \"memberRegistrationRequest\": { \"action\": \"requestJoin\", \"context\": { \"corda.key.scheme\" : \"CORDA.ECDSA.SECP256R1\" } } }")
.basicAuth(rpcUser, rpcPasswd)
.asJson();
out.println("Vnode membership submission:\n" + vnodeResponse.getBody().toPrettyString());
}
}
public void startCorda() throws IOException {
PrintStream pidStore = new PrintStream(new FileOutputStream(cordaPidCache));
File combinedWorkerJar = project.getConfigurations().getByName("combinedWorker").getSingleFile();
new ProcessBuilder(
"docker",
"run", "-d", "--rm",
"-p", "5432:5432",
"--name", dbContainerName,
"-e", "POSTGRES_DB=cordacluster",
"-e", "POSTGRES_USER=postgres",
"-e", "POSTGRES_PASSWORD=password",
"postgres:latest").start();
rpcWait(10000);
ProcessBuilder procBuild = new ProcessBuilder(javaBinDir + "/java",
"-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005",
"-Dco.paralleluniverse.fibers.verifyInstrumentation=true",
"-jar",
combinedWorkerJar.toString(),
"--instanceId=0",
"-mbus.busType=DATABASE",
"-spassphrase=password",
"-ssalt=salt",
"-spassphrase=password",
"-ssalt=salt",
"-ddatabase.user=user",
"-ddatabase.pass=password",
"-ddatabase.jdbc.url=jdbc:postgresql://localhost:5432/cordacluster",
"-ddatabase.jdbc.directory="+JDBCDir);
procBuild.redirectErrorStream(true);
Process proc = procBuild.start();
pidStore.print(proc.pid());
out.println("Corda Process-id="+proc.pid());
}
public void stopCorda() throws IOException, NoPidFile {
File cordaPIDFile = new File(cordaPidCache);
if(cordaPIDFile.exists()) {
Scanner sc = new Scanner(cordaPIDFile);
long pid = sc.nextLong();
out.println("pid to kill=" + pid);
if (System.getProperty("os.name").toLowerCase().contains("windows")) {
new ProcessBuilder("Powershell", "-Command", "Stop-Process", "-Id", Long.toString(pid), "-PassThru").start();
} else {
new ProcessBuilder("kill", "-9", Long.toString(pid)).start();
}
Process proc = new ProcessBuilder("docker", "stop", dbContainerName).start();
cordaPIDFile.delete();
}
else {
throw new NoPidFile("Cannot stop the Combined worker\nCached process ID file " + cordaPidCache + " missing.\nWas the combined worker not started?");
}
}
}

View File

@ -0,0 +1,188 @@
package com.r3.csde;
import kong.unirest.JsonNode;
import kong.unirest.Unirest;
import kong.unirest.json.JSONArray;
import kong.unirest.json.JSONObject;
import kong.unirest.HttpResponse;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.PrintStream;
import static java.net.HttpURLConnection.HTTP_BAD_REQUEST;
import static java.net.HttpURLConnection.HTTP_OK;
public class DeployCPIsHelper {
public DeployCPIsHelper() {
}
ProjectContext pc;
CordaStatusQueries queries;
ProjectUtils utils;
public DeployCPIsHelper(ProjectContext _pc) {
pc = _pc;
queries = new CordaStatusQueries(pc);
utils = new ProjectUtils(pc);
}
public void deployCPIs() throws FileNotFoundException, CsdeException{
uploadCertificate(pc.signingCertAlias, pc.signingCertFName);
uploadCertificate(pc.keystoreAlias, pc.keystoreCertFName);
// todo: make consistent with other string building code - remove String.format
String appCPILocation = String.format("%s/%s-%s.cpi",
pc.workflowBuildDir,
pc.project.getName(),
pc.project.getVersion());
deployCPI(appCPILocation, pc.appCPIName,pc.project.getVersion().toString());
String notaryCPILocation = String.format("%s/%s-%s.cpi",
pc.workflowBuildDir,
pc.notaryCPIName.replace(' ','-').toLowerCase(),
pc.project.getVersion());
deployCPI(notaryCPILocation,
pc.notaryCPIName,
pc.project.getVersion().toString(),
"-NotaryServer" );
}
public void uploadCertificate(String certAlias, String certFName) {
Unirest.config().verifySsl(false);
HttpResponse<JsonNode> uploadResponse = Unirest.put(pc.baseURL + "/api/v1/certificates/cluster/code-signer")
.field("alias", certAlias)
.field("certificate", new File(certFName))
.basicAuth(pc.rpcUser, pc.rpcPasswd)
.asJson();
pc.out.println("Certificate/key upload, alias "+certAlias+" certificate/key file "+certFName);
pc.out.println(uploadResponse.getBody().toPrettyString());
}
public void forceuploadCPI(String cpiFName) throws FileNotFoundException, CsdeException {
forceuploadCPI(cpiFName, "");
}
public void forceuploadCPI(String cpiFName, String uploadStatusQualifier) throws FileNotFoundException, CsdeException {
Unirest.config().verifySsl(false);
HttpResponse<JsonNode> jsonResponse = Unirest.post(pc.baseURL + "/api/v1/maintenance/virtualnode/forcecpiupload/")
.field("upload", new File(cpiFName))
.basicAuth(pc.rpcUser, pc.rpcPasswd)
.asJson();
if(jsonResponse.getStatus() == HTTP_OK) {
String id = (String) jsonResponse.getBody().getObject().get("id");
pc.out.println("get id:\n" +id);
HttpResponse<JsonNode> statusResponse = uploadStatus(id);
if (statusResponse.getStatus() == HTTP_OK) {
PrintStream cpiUploadStatus = new PrintStream(new FileOutputStream(
pc.CPIUploadStatusFName.replace(".json", uploadStatusQualifier + ".json" )));
cpiUploadStatus.print(statusResponse.getBody());
pc.out.println("Caching CPI file upload status:\n" + statusResponse.getBody());
} else {
utils.reportError(statusResponse);
}
}
else {
utils.reportError(jsonResponse);
}
}
private boolean uploadStatusRetry(HttpResponse<JsonNode> response) {
int status = response.getStatus();
JsonNode body = response.getBody();
// Do not retry on success // todo: need to think through the possible outcomes here - what if the bodyTitle is null, it won't retry
if(status == HTTP_OK) {
// Keep retrying until we get "OK" may move through "Validating upload", "Persisting CPI"
return !(body.getObject().get("status").equals("OK"));
}
else if (status == HTTP_BAD_REQUEST){
String bodyTitle = response.getBody().getObject().getString("title");
return bodyTitle != null && bodyTitle.matches("No such requestId=[-0-9a-f]+");
}
return false;
}
public HttpResponse<JsonNode> uploadStatus(String requestId) {
HttpResponse<JsonNode> statusResponse = null;
do {
utils.rpcWait(1000);
statusResponse = Unirest
.get(pc.baseURL + "/api/v1/cpi/status/" + requestId + "/")
.basicAuth(pc.rpcUser, pc.rpcPasswd)
.asJson();
pc.out.println("Upload status="+statusResponse.getStatus()+", status query response:\n"+statusResponse.getBody().toPrettyString());
}
while(uploadStatusRetry(statusResponse));
return statusResponse;
}
public void deployCPI(String cpiFName, String cpiName, String cpiVersion) throws FileNotFoundException, CsdeException {
deployCPI(cpiFName, cpiName, cpiVersion, "");
}
public void deployCPI(String cpiFName,
String cpiName,
String cpiVersion,
String uploadStatusQualifier) throws FileNotFoundException, CsdeException {
// todo: where is the primary instance declared?
Unirest.config().verifySsl(false);
HttpResponse<JsonNode> cpiResponse = queries.getCpiInfo();
JSONArray jArray = (JSONArray) cpiResponse.getBody().getObject().get("cpis");
int matches = 0;
for(Object o: jArray.toList() ) {
if(o instanceof JSONObject) {
JSONObject idObj = ((JSONObject) o).getJSONObject("id");
if((idObj.get("cpiName").toString().equals(cpiName)
&& idObj.get("cpiVersion").toString().equals(cpiVersion))) {
matches++;
}
}
}
pc.out.println("Matching CPIS="+matches);
if(matches == 0) {
HttpResponse<JsonNode> uploadResponse = Unirest.post(pc.baseURL + "/api/v1/cpi/")
.field("upload", new File(cpiFName))
.basicAuth(pc.rpcUser, pc.rpcPasswd)
.asJson();
JsonNode body = uploadResponse.getBody();
int status = uploadResponse.getStatus();
pc.out.println("Upload Status:" + status);
pc.out.println("Pretty print the body\n" + body.toPrettyString());
// We expect the id field to be a string.
if (status == HTTP_OK) {
String id = (String) body.getObject().get("id");
pc.out.println("get id:\n" + id);
HttpResponse<JsonNode> statusResponse = uploadStatus(id);
if (statusResponse.getStatus() == HTTP_OK) {
PrintStream cpiUploadStatus = new PrintStream(new FileOutputStream(
pc.CPIUploadStatusFName.replace(".json", uploadStatusQualifier + ".json" )));
cpiUploadStatus.print(statusResponse.getBody());
pc.out.println("Caching CPI file upload status:\n" + statusResponse.getBody());
} else {
utils.reportError(statusResponse);
}
} else {
utils.reportError(uploadResponse);
}
}
else {
pc.out.println("CPI already uploaded doing a 'force' upload.");
forceuploadCPI(cpiFName);
}
}
}

View File

@ -1,7 +0,0 @@
package com.r3.csde;
public class NoPidFile extends Exception {
public NoPidFile(String message){
super(message);
}
}

View File

@ -0,0 +1,83 @@
package com.r3.csde;
import org.gradle.api.Project;
import java.io.PrintStream;
import java.util.Map;
public class ProjectContext {
Project project;
String baseURL = "https://localhost:8888";
String rpcUser = "admin";
String rpcPasswd = "admin";
String workspaceDir = "workspace";
int retryWaitMs = 1000;
PrintStream out = System.out;
String CPIUploadStatusBaseName = "CPIFileStatus.json";
String CPIUploadStatusFName;
String X500ConfigFile = "config/dev-net.json";
String javaBinDir;
String cordaPidCache = "CordaPIDCache.dat";
String dbContainerName;
String JDBCDir;
String combinedWorkerBinRe;
Map<String, String> notaryRepresentatives = null;
String signingCertAlias;
String signingCertFName;
String keystoreAlias;
String keystoreFName;
String keystoreCertFName;
String appCPIName;
String notaryCPIName;
String devEnvWorkspace;
String cordaCliBinDir;
String cordaNotaryServiceDir;
String workflowBuildDir;
String cordaNotaryPluginsVersion;
public ProjectContext (Project inProject,
String inBaseUrl,
String inRpcUser,
String inRpcPasswd,
String inWorkspaceDir,
String inJavaBinDir,
String inDbContainerName,
String inJDBCDir,
String inCordaPidCache,
String inSigningCertAlias,
String inSigningCertFName,
String inKeystoreAlias,
String inKeystoreFName,
String inKeystoreCertFName,
String inAppCPIName,
String inNotaryCPIName,
String inDevEnvWorkspace,
String inCordaCLiBinDir,
String inCordaNotaryServiceDir,
String inWorkflowBuildDir,
String inCordaNotaryPluginsVersion
) {
project = inProject;
baseURL = inBaseUrl;
rpcUser = inRpcUser;
rpcPasswd = inRpcPasswd;
workspaceDir = inWorkspaceDir;
javaBinDir = inJavaBinDir;
cordaPidCache = inCordaPidCache;
dbContainerName = inDbContainerName;
JDBCDir = inJDBCDir;
CPIUploadStatusFName = workspaceDir + "/" + CPIUploadStatusBaseName;
signingCertAlias = inSigningCertAlias;
signingCertFName = inSigningCertFName;
keystoreAlias = inKeystoreAlias;
keystoreFName = inKeystoreFName;
keystoreCertFName = inKeystoreCertFName;
appCPIName = inAppCPIName;
notaryCPIName = inNotaryCPIName;
devEnvWorkspace = inDevEnvWorkspace;
cordaCliBinDir = inCordaCLiBinDir;
cordaNotaryServiceDir = inCordaNotaryServiceDir;
workflowBuildDir = inWorkflowBuildDir;
cordaNotaryPluginsVersion = inCordaNotaryPluginsVersion;
}
}

View File

@ -0,0 +1,71 @@
package com.r3.csde;
import com.fasterxml.jackson.databind.ObjectMapper;
import kong.unirest.HttpResponse;
import kong.unirest.JsonNode;
import kong.unirest.Unirest;
import org.jetbrains.annotations.NotNull;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.LinkedList;
import static java.lang.Thread.sleep;
public class ProjectUtils {
ProjectContext pc;
ProjectUtils(ProjectContext _pc) {
pc = _pc;
}
void rpcWait(int millis) {
try {
sleep(millis);
}
catch(InterruptedException e) {
throw new UnsupportedOperationException("Interrupts not supported.", e);
}
}
void rpcWait() {
rpcWait( pc.retryWaitMs);
}
public LinkedList<String> getConfigX500Ids(String configFile) throws IOException {
LinkedList<String> x500Ids = new LinkedList<>();
// com.fasterxml.jackson.databind.ObjectMapper mapper = new com.fasterxml.jackson.databind.ObjectMapper();
ObjectMapper mapper = new ObjectMapper();
FileInputStream in = new FileInputStream(configFile);
com.fasterxml.jackson.databind.JsonNode jsonNode = mapper.readTree(in);
for( com.fasterxml.jackson.databind.JsonNode identity: jsonNode.get("identities")) {
x500Ids.add(jsonNodeToString(identity));
}
return x500Ids;
}
public String jsonNodeToString(com.fasterxml.jackson.databind.JsonNode jsonNode) {
String jsonString = jsonNode.toString();
return jsonString.substring(1, jsonString.length()-1);
}
public void downloadFile(String url, String targetPath) {
Unirest.get(url)
.asFile(targetPath)
.getBody();
}
public void reportError(@NotNull HttpResponse<JsonNode> response) throws CsdeException {
pc.out.println("*** *** ***");
pc.out.println("Unexpected response from Corda");
pc.out.println("Status="+ response.getStatus());
pc.out.println("*** Headers ***\n"+ response.getHeaders());
pc.out.println("*** Body ***\n"+ response.getBody());
pc.out.println("*** *** ***");
throw new CsdeException("Error: unexpected response from Corda.");
}
}

View File

@ -1,4 +0,0 @@
import com.r3.csde.CsdeRpcInterface;
public class CsdeRpcInterfaceTests {
}

View File

@ -2,6 +2,11 @@
"identities" : [
"CN=Alice, OU=Test Dept, O=R3, L=London, C=GB",
"CN=Bob, OU=Test Dept, O=R3, L=London, C=GB",
"CN=Charlie, OU=Test Dept, O=R3, L=London, C=GB"
]
"CN=Charlie, OU=Test Dept, O=R3, L=London, C=GB",
"CN=NotaryRep1, OU=Test Dept, O=R3, L=London, C=GB"],
"notaries" : [
{
"serviceX500Name": "CN=NotaryService, OU=Test Dept, O=R3, L=London, C=GB",
"representatives": ["CN=NotaryRep1, OU=Test Dept, O=R3, L=London, C=GB"]
}]
}

91
contracts/build.gradle Normal file
View File

@ -0,0 +1,91 @@
plugins {
// Include the cordapp-cpb plugin. This automatically includes the cordapp-cpk plugin as well.
// These extend existing build environment so that CPB and CPK files can be built.
// This includes a CorDapp DSL that allows the developer to supply metadata for the CorDapp
// required by Corda.
id 'net.corda.plugins.cordapp-cpb2'
id 'org.jetbrains.kotlin.jvm'
id 'maven-publish'
}
// Declare dependencies for the modules we will use.
// A cordaProvided declaration is required for anything that we use that the Corda API provides.
// This is required to allow us to build CorDapp modules as OSGi bundles that CPI and CPB files are built on.
dependencies {
// We need a version of kotlin-stdlib-jdk8 built as an OSGi bundle, this is "kotlin-stdlib-jdk8-osgi".
// R3 builds kotlin-stdlib-jdk8-osgi from Kotlin's kotlin-stdlib-jdk8.
// NB:
// Kotlin's kotlin-osgi-bundle does not provide all of the Kotlin API that is required,
// There is no kotlin-stdlib-jdk11, but one is not needed even though we are targetting Java 11.
cordaProvided 'net.corda.kotlin:kotlin-stdlib-jdk8-osgi'
// Declare a "platform" so that we use the correct set of dependency versions for the version of the
// Corda API specified.
cordaProvided platform("net.corda:corda-api:$cordaApiVersion")
// If using transistive dependencies this will provide most of Corda-API:
// cordaProvided 'net.corda:corda-application'
// Alternatively we can explicitly specify all our Corda-API dependencies:
cordaProvided 'net.corda:corda-base'
cordaProvided 'net.corda:corda-application'
cordaProvided 'net.corda:corda-crypto'
cordaProvided 'net.corda:corda-membership'
// cordaProvided 'net.corda:corda-persistence'
cordaProvided 'net.corda:corda-serialization'
cordaProvided 'net.corda:corda-ledger-utxo'
cordaProvided 'net.corda:corda-ledger-consensual'
// CorDapps that use the UTXO ledger must include at least one notary client plugin
cordapp "com.r3.corda.notary.plugin.nonvalidating:notary-plugin-non-validating-client:$cordaNotaryPluginsVersion"
// The CorDapp uses the slf4j logging framework. Corda-API provides this so we need a 'cordaProvided' declaration.
cordaProvided 'org.slf4j:slf4j-api'
// This are shared so should be here.
// Dependencies Required By Test Tooling
testImplementation "net.corda:corda-simulator-api:$simulatorVersion"
testRuntimeOnly "net.corda:corda-simulator-runtime:$simulatorVersion"
// 3rd party libraries
// Required
testImplementation "org.slf4j:slf4j-simple:2.0.0"
testImplementation "org.junit.jupiter:junit-jupiter:$junitVersion"
testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$junitVersion"
// Optional but used by exmaple tests.
testImplementation "org.mockito:mockito-core:$mockitoVersion"
testImplementation "org.mockito.kotlin:mockito-kotlin:$mockitoKotlinVersion"
testImplementation "org.hamcrest:hamcrest-library:$hamcrestVersion"
}
// The CordApp section.
// This is part of the DSL provided by the corda plugins to define metadata for our CorDapp.
// Each component of the CorDapp would get its own CorDapp section in the build.gradle file for the components
// subproject.
// This is required by the corda plugins to build the CorDapp.
cordapp {
// "targetPlatformVersion" and "minimumPlatformVersion" are intended to specify the preferred
// and earliest versions of the Corda platform that the CorDapp will run on respectively.
// Enforced versioning has not implemented yet so we need to pass in a dummy value for now.
// The platform version will correspond to and be roughly equivalent to the Corda API version.
targetPlatformVersion = platformVersion.toInteger()
minimumPlatformVersion = platformVersion.toInteger()
// The cordapp section contains either a workflow or contract subsection depending on the type of component.
// Declares the type and metadata of the CPK (this CPB has one CPK).
contract {
name "ContractsModuleNameHere"
versionId 1
vendor "VendorNameHere"
}
}
publishing {
publications {
maven(MavenPublication) {
from components.cordapp
}
}
}

View File

@ -2,11 +2,18 @@ kotlin.code.style=official
# Specify the version of the Corda-API to use.
# This needs to match the version supported by the Corda Cluster the CorDapp will run on.
# cordaApiVersion=5.0.0.190-DevPreview-2
cordaApiVersion=5.0.0.505-Beta1.0-HC00
cordaApiVersion=5.0.0.523-Fox1.0
# Settings For Development Utilities
combinedWorkerVersion=5.0.0.0-Fox1.0
simulatorVersion=5.0.0.0-Fox1.0
# Specify the version of the notary plugins to use.
# Currently packaged as part of corda-runtime-os, so should be set to a corda-runtime-os version.
cordaNotaryPluginsVersion=5.0.0.0-Fox1.0
# Specify the version of the cordapp-cpb and cordapp-cpk plugins
cordaPluginsVersion=7.0.0
cordaPluginsVersion=7.0.1
# For the time being this just needs to be set to a dummy value.
platformVersion = 999
@ -24,15 +31,9 @@ mockitoKotlinVersion=4.0.0
mockitoVersion=4.6.1
hamcrestVersion=2.2
# Settings For Development Utilities
combinedWorkerVersion=5.0.0.0-Beta1.0-HC00
simulatorVersion=5.0.0.0-Beta1.0-HC00
cordaClusterURL=https://localhost:8888
cordaRpcUser=admin
cordaRpcPasswd=admin
devEnvWorkspace=workspace
dbContainerName=CSDEpostgresql
# R3 internal repository
artifactoryContextUrl=https://staging.download.corda.net/maven/20ede3c6-29c0-11ed-966d-b7c36748b9f6-Beta1.0-HC00

View File

@ -2,9 +2,7 @@ pluginManagement {
// Declare the repositories where plugins are stored.
repositories {
gradlePluginPortal()
maven {
url = "$artifactoryContextUrl/"
}
mavenCentral()
}
// The plugin dependencies with versions of the plugins congruent with the specified CorDapp plugin version,
@ -21,5 +19,9 @@ pluginManagement {
// Root project name, used in naming the project as a whole and used in naming objects built by the project.
rootProject.name = 'csde-cordapp-template-java'
include ':workflows'
include ':contracts'

View File

@ -1,20 +0,0 @@
package com.r3.developers.csdetemplate;
import net.corda.v5.base.types.MemberX500Name;
// A class to hold the arguments required to start the flow
public class MyFirstFlowStartArgs {
public MemberX500Name otherMember;
public MemberX500Name getOtherMember() {
return otherMember;
}
public MyFirstFlowStartArgs(MemberX500Name otherMember) {
this.otherMember = otherMember;
}
// Without the following we get
// "Cannot construct instance of `com.r3.developers.csdetemplate.MyFirstFlowStartArgs` (no Creators, like default constructor, exist): cannot deserialize from Object value (no delegate- or property-based Creator)\n at [Source: (String)\"{\"otherMember\":\"CN=Bob, OU=Test Dept, O=R3, L=London, C=GB\"}\"; line: 1, column: 2]"
public MyFirstFlowStartArgs() {}
}

94
workflows/build.gradle Normal file
View File

@ -0,0 +1,94 @@
plugins {
// Include the cordapp-cpb plugin. This automatically includes the cordapp-cpk plugin as well.
// These extend existing build environment so that CPB and CPK files can be built.
// This includes a CorDapp DSL that allows the developer to supply metadata for the CorDapp
// required by Corda.
id 'net.corda.plugins.cordapp-cpb2'
id 'org.jetbrains.kotlin.jvm'
id 'maven-publish'
}
// Declare dependencies for the modules we will use.
// A cordaProvided declaration is required for anything that we use that the Corda API provides.
// This is required to allow us to build CorDapp modules as OSGi bundles that CPI and CPB files are built on.
dependencies {
// From other subprojects:
cordapp project(':contracts')
// We need a version of kotlin-stdlib-jdk8 built as an OSGi bundle, this is "kotlin-stdlib-jdk8-osgi".
// R3 builds kotlin-stdlib-jdk8-osgi from Kotlin's kotlin-stdlib-jdk8.
// NB:
// Kotlin's kotlin-osgi-bundle does not provide all of the Kotlin API that is required,
// There is no kotlin-stdlib-jdk11, but one is not needed even though we are targetting Java 11.
cordaProvided 'net.corda.kotlin:kotlin-stdlib-jdk8-osgi'
// Declare a "platform" so that we use the correct set of dependency versions for the version of the
// Corda API specified.
cordaProvided platform("net.corda:corda-api:$cordaApiVersion")
// If using transistive dependencies this will provide most of Corda-API:
// cordaProvided 'net.corda:corda-application'
// Alternatively we can explicitly specify all our Corda-API dependencies:
cordaProvided 'net.corda:corda-base'
cordaProvided 'net.corda:corda-application'
cordaProvided 'net.corda:corda-crypto'
cordaProvided 'net.corda:corda-membership'
// cordaProvided 'net.corda:corda-persistence'
cordaProvided 'net.corda:corda-serialization'
cordaProvided 'net.corda:corda-ledger-utxo'
cordaProvided 'net.corda:corda-ledger-consensual'
// CorDapps that use the UTXO ledger must include at least one notary client plugin
cordapp "com.r3.corda.notary.plugin.nonvalidating:notary-plugin-non-validating-client:$cordaNotaryPluginsVersion"
// The CorDapp uses the slf4j logging framework. Corda-API provides this so we need a 'cordaProvided' declaration.
cordaProvided 'org.slf4j:slf4j-api'
// This are shared so should be here.
// Dependencies Required By Test Tooling
testImplementation "net.corda:corda-simulator-api:$simulatorVersion"
testRuntimeOnly "net.corda:corda-simulator-runtime:$simulatorVersion"
// 3rd party libraries
// Required
testImplementation "org.slf4j:slf4j-simple:2.0.0"
testImplementation "org.junit.jupiter:junit-jupiter:$junitVersion"
testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$junitVersion"
// Optional but used by exmaple tests.
testImplementation "org.mockito:mockito-core:$mockitoVersion"
testImplementation "org.mockito.kotlin:mockito-kotlin:$mockitoKotlinVersion"
testImplementation "org.hamcrest:hamcrest-library:$hamcrestVersion"
}
// The CordApp section.
// This is part of the DSL provided by the corda plugins to define metadata for our CorDapp.
// Each component of the CorDapp would get its own CorDapp section in the build.gradle file for the components
// subproject.
// This is required by the corda plugins to build the CorDapp.
cordapp {
// "targetPlatformVersion" and "minimumPlatformVersion" are intended to specify the preferred
// and earliest versions of the Corda platform that the CorDapp will run on respectively.
// Enforced versioning has not implemented yet so we need to pass in a dummy value for now.
// The platform version will correspond to and be roughly equivalent to the Corda API version.
targetPlatformVersion = platformVersion.toInteger()
minimumPlatformVersion = platformVersion.toInteger()
// The cordapp section contains either a workflow or contract subsection depending on the type of component.
// Declares the type and metadata of the CPK (this CPB has one CPK).
workflow {
name "WorkflowsModuleNameHere"
versionId 1
vendor "VendorNameHere"
}
}
publishing {
publications {
maven(MavenPublication) {
from components.cordapp
}
}
}

View File

@ -1,17 +1,17 @@
package com.r3.developers.csdetemplate;
package com.r3.developers.csdetemplate.workflows;
import net.corda.v5.base.annotations.CordaSerializable;
import net.corda.v5.base.types.MemberX500Name;
// A class which will contain a message, It must be marked with @CordaSerializable for Corda
// to be able to send from one virtual node to another.
// Where a class contains a message, mark it with @CordaSerializable to enable Corda to
// send it from one virtual node to another.
@CordaSerializable
public class Message {
// public Message() {}
public Message(MemberX500Name sender, String message) {
this.sender = sender;
this.message = message;
}
public MemberX500Name getSender() {
return sender;
}

View File

@ -1,4 +1,4 @@
package com.r3.developers.csdetemplate;
package com.r3.developers.csdetemplate.workflows;
import net.corda.v5.application.flows.*;
import net.corda.v5.application.marshalling.JsonMarshallingService;
@ -14,50 +14,50 @@ import org.slf4j.LoggerFactory;
// MyFirstFlow is an initiating flow, it's corresponding responder flow is called MyFirstFlowResponder (defined below)
// to link the two sides of the flow together they need to have the same protocol.
@InitiatingFlow(protocol = "another-flow")
@InitiatingFlow(protocol = "my-first-flow")
// MyFirstFlow should inherit from RPCStartableFlow, which tells Corda it can be started via an RPC call
public class MyFirstFlow implements RPCStartableFlow {
// It is useful to be able to log messages from the flows for debugging.
// Log messages from the flows for debugging.
private final Logger log = LoggerFactory.getLogger(MyFirstFlow.class);
// Corda has a set of injectable services which are injected into the flow at runtime.
// Flows declare them with @CordaInjectable, then the flows have access to their services.
// JsonMarshallingService provides a Service for manipulating json
// JsonMarshallingService provides a service for manipulating JSON.
@CordaInject
public JsonMarshallingService jsonMarshallingService;
// FlowMessaging provides a service for establishing flow sessions between Virtual Nodes and
// sending and receiving payloads between them
// FlowMessaging provides a service that establishes flow sessions between virtual nodes
// that send and receive payloads between them.
@CordaInject
public FlowMessaging flowMessaging;
// MemberLookup provides a service for looking up information about members of the Virtual Network which
// this CorDapp is operating in.
// MemberLookup provides a service for looking up information about members of the virtual network which
// this CorDapp operates in.
@CordaInject
public MemberLookup memberLookup;
public MyFirstFlow() {}
// When a flow is invoked it's call() method is called.
// call() methods must be marked as @Suspendable, this allows Corda to pause mid-execution to wait
// for a response from the other flows and services
// When a flow is invoked its call() method is called.
// Call() methods must be marked as @Suspendable, this allows Corda to pause mid-execution to wait
// for a response from the other flows and services.
@NotNull
@Suspendable
@Override
public String call(@NotNull RPCRequestData requestBody) {
public String call(RPCRequestData requestBody) {
// Useful logging to follow what's happening in the console or logs
// Follow what happens in the console or logs.
log.info("MFF: MyFirstFlow.call() called");
// Show the requestBody in the logs - this can be used to help establish the format for starting a flow on corda
// Show the requestBody in the logs - this can be used to help establish the format for starting a flow on Corda.
log.info("MFF: requestBody: " + requestBody.getRequestBody());
// Deserialize the Json requestBody into the MyfirstFlowStartArgs class using the JsonSerialisation Service
// Deserialize the Json requestBody into the MyfirstFlowStartArgs class using the JsonSerialisation service.
MyFirstFlowStartArgs flowArgs = requestBody.getRequestBodyAs(jsonMarshallingService, MyFirstFlowStartArgs.class);
// Obtain the MemberX500Name of counterparty
// Obtain the MemberX500Name of the counterparty.
MemberX500Name otherMember = flowArgs.otherMember;
// Get our identity from the MemberLookup service.
@ -69,19 +69,19 @@ public class MyFirstFlow implements RPCStartableFlow {
// Log the message to be sent.
log.info("MFF: message.message: " + message.message);
// Start a flow session with the otherMember using the FlowMessaging service
// The otherMember's Virtual Node will run the corresponding MyFirstFlowResponder responder flow
// Start a flow session with the otherMember using the FlowMessaging service.
// The otherMember's virtual node will run the corresponding MyFirstFlowResponder responder flow.
FlowSession session = flowMessaging.initiateFlow(otherMember);
// Send the Payload using the send method on the session to the MyFirstFlowResponder Responder flow
// Send the Payload using the send method on the session to the MyFirstFlowResponder responder flow.
session.send(message);
// Receive a response from the Responder flow
// Receive a response from the responder flow.
Message response = session.receive(Message.class);
// The return value of a RPCStartableFlow must always be a String, this string will be passed
// The return value of a RPCStartableFlow must always be a String. This will be passed
// back as the REST RPC response when the status of the flow is queried on Corda, or as the return
// value from the flow when testing using the Simulator
// value from the flow when testing using the simulator.
return response.message;
}
}
@ -91,7 +91,7 @@ public class MyFirstFlow implements RPCStartableFlow {
RequestBody for triggering the flow via http-rpc:
{
"clientRequestId": "r1",
"flowClassName": "com.r3.developers.csdetemplate.MyFirstFlow",
"flowClassName": "com.r3.developers.csdetemplate.workflows.MyFirstFlow",
"requestData": {
"otherMember":"CN=Bob, OU=Test Dept, O=R3, L=London, C=GB"
}

View File

@ -1,4 +1,4 @@
package com.r3.developers.csdetemplate;
package com.r3.developers.csdetemplate.workflows;
import net.corda.v5.application.flows.CordaInject;
import net.corda.v5.application.flows.InitiatedBy;
@ -11,36 +11,36 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
// MyFirstFlowResponder is a responder flow, it's corresponding initiating flow is called MyFirstFlow (defined above)
// MyFirstFlowResponder is a responder flow, its corresponding initiating flow is called MyFirstFlow (defined in MyFirstFlow.java)
// to link the two sides of the flow together they need to have the same protocol.
@InitiatedBy(protocol = "another-flow")
@InitiatedBy(protocol = "my-first-flow")
// Responder flows must inherit from ResponderFlow
public class MyFirstFlowResponder implements ResponderFlow {
// It is useful to be able to log messages from the flows for debugging.
// Log messages from the flows for debugging.
private final Logger log = LoggerFactory.getLogger(MyFirstFlowResponder.class);
// MemberLookup provides a service for looking up information about members of the Virtual Network which
// this CorDapp is operating in.
// MemberLookup looks for information about members of the virtual network which
// this CorDapp operates in.
@CordaInject
public MemberLookup memberLookup;
public MyFirstFlowResponder() {}
// Responder flows are invoked when an initiating flow makes a call via a session set up with the Virtual
// node hosting the Responder flow. When a responder flow is invoked it's call() method is called.
// call() methods must be marked as @Suspendable, this allows Corda to pause mid-execution to wait
// for a response from the other flows and services/
// Responder flows are invoked when an initiating flow makes a call via a session set up with the virtual
// node hosting the responder flow. When a responder flow is invoked its call() method is called.
// Call() methods must be marked as @Suspendable, this allows Corda to pause mid-execution to wait
// for a response from the other flows and services.
// The Call method has the flow session passed in as a parameter by Corda so the session is available to
// responder flow code, you don't need to inject the FlowMessaging service.
@Suspendable
@Override
public void call(FlowSession session) {
// Useful logging to follow what's happening in the console or logs
// Follow what happens in the console or logs.
log.info("MFF: MyFirstResponderFlow.call() called");
// Receive the payload and deserialize it into a Message class
// Receive the payload and deserialize it into a message class.
Message receivedMessage = session.receive(Message.class);
// Log the message as a proxy for performing some useful operation on it.
@ -49,7 +49,7 @@ public class MyFirstFlowResponder implements ResponderFlow {
// Get our identity from the MemberLookup service.
MemberX500Name ourIdentity = memberLookup.myInfo().getName();
// Create a response to greet the sender
// Create a message to greet the sender.
Message response = new Message(ourIdentity,
"Hello " + session.getCounterparty().getCommonName() + ", best wishes from " + ourIdentity.getCommonName());

View File

@ -0,0 +1,15 @@
package com.r3.developers.csdetemplate.workflows;
import net.corda.v5.base.types.MemberX500Name;
// A class to hold the arguments required to start the flow
public class MyFirstFlowStartArgs {
public MemberX500Name otherMember;
public MyFirstFlowStartArgs(MemberX500Name otherMember) {
this.otherMember = otherMember;
}
// The JSON Marshalling Service, which handles serialisation, needs this constructor.
public MyFirstFlowStartArgs() {}
}

View File

@ -1,50 +1,41 @@
package com.r3.developers.csdetemplate;
package com.r3.developers.csdetemplate.workflows;
import net.corda.simulator.HoldingIdentity;
import net.corda.simulator.RequestData;
import net.corda.simulator.SimulatedVirtualNode;
import net.corda.simulator.Simulator;
import net.corda.v5.base.types.MemberX500Name;
import org.junit.jupiter.api.Test;
class MyFirstFlowTest {
// Names picked to match the corda network in config/dev-net.json
private MemberX500Name aliceX500 = MemberX500Name.parse("CN=Alice, OU=Test Dept, O=R3, L=London, C=GB");
private MemberX500Name bobX500 = MemberX500Name.parse("CN=Bob, OU=Test Dept, O=R3, L=London, C=GB");
@Test
@SuppressWarnings("unchecked")
public void test_that_MyFirstFLow_returns_correct_message() {
// Instantiate an instance of the Simulator
// Instantiate an instance of the simulator.
Simulator simulator = new Simulator();
// Create Alice's and Bob HoldingIDs
HoldingIdentity aliceHoldingID = HoldingIdentity.Companion.create(aliceX500);
HoldingIdentity bobHoldingID = HoldingIdentity.Companion.create(bobX500);
// Create Alice's and Bob's virtual nodes, including the classes of the flows which will be registered on each node.
// Don't assign Bob's virtual node to a value. You don't need it for this particular test.
SimulatedVirtualNode aliceVN = simulator.createVirtualNode(aliceX500, MyFirstFlow.class);
simulator.createVirtualNode(bobX500, MyFirstFlowResponder.class);
// Create Alice and Bob's virtual nodes, including the Class's of the flows which will be registered on each node.
// We don't assign Bob's virtual node to a val because we don't need it for this particular test.
SimulatedVirtualNode aliceVN = simulator.createVirtualNode(aliceHoldingID, MyFirstFlow.class);
simulator.createVirtualNode(bobHoldingID, MyFirstFlowResponder.class);
// Create an instance of the MyFirstFlowStartArgs which contains the request arguments for starting the flow
// Create an instance of the MyFirstFlowStartArgs which contains the request arguments for starting the flow.
MyFirstFlowStartArgs myFirstFlowStartArgs = new MyFirstFlowStartArgs(bobX500);
// Create a requestData object
// Create a requestData object.
RequestData requestData = RequestData.Companion.create(
"request no 1", // A unique reference for the instance of the flow request
MyFirstFlow.class, // The name of the flow class which is to be started
myFirstFlowStartArgs // The object which contains the start arguments of the flow
"request no 1", // A unique reference for the instance of the flow request.
MyFirstFlow.class, // The name of the flow class which is to be started.
myFirstFlowStartArgs // The object which contains the start arguments of the flow.
);
// Call the Flow on Alice's virtual node and capture the response from the flow
// Call the flow on Alice's virtual node and capture the response.
String flowResponse = aliceVN.callFlow(requestData);
// Check that the flow has returned the expected string
// Check that the flow has returned the expected string.
assert(flowResponse.equals("Hello Alice, best wishes from Bob"));
}
}