mirror of
https://github.com/corda/corda.git
synced 2025-02-01 08:48:09 +00:00
[CORDA-2235]: Add overrides for network parameters via command line and file (#4279)
* Temp commit * Print the error message first by default, makes error output more natural. * Polishing * Further modifications after testing * Documentation updates * Couple of fixes after review * Removing unnecessary tests * Fix broken test * Add interface to bootstrapper for testign * Added unit tests * Remove unused class * Fix up bootstrapper unit tests and add a couple more * Refactor the tests slightly * Review comments * Couple of minor tweaks
This commit is contained in:
parent
d399e3c242
commit
b7d04b1c6e
@ -40,6 +40,7 @@ buildscript {
|
||||
ext.fileupload_version = '1.3.3'
|
||||
ext.junit_version = '4.12'
|
||||
ext.mockito_version = '2.18.3'
|
||||
ext.mockito_kotlin_version = '1.5.0'
|
||||
ext.hamkrest_version = '1.4.2.2'
|
||||
ext.jopt_simple_version = '5.0.2'
|
||||
ext.jansi_version = '1.14'
|
||||
|
@ -448,7 +448,7 @@ object Configuration {
|
||||
|
||||
override fun toString(): String {
|
||||
|
||||
return "(keyName='$keyName', typeName='$typeName', path=$path, message='$message')"
|
||||
return "$message: (keyName='$keyName', typeName='$typeName', path=$path)"
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -107,8 +107,8 @@ data class NetworkParameters(
|
||||
private fun owns(packageName: String, fullClassName: String) = fullClassName.startsWith("$packageName.", ignoreCase = true)
|
||||
|
||||
// Make sure that packages don't overlap so that ownership is clear.
|
||||
private fun noOverlap(packages: Collection<String>) = packages.all { currentPackage ->
|
||||
packages.none { otherPackage -> otherPackage != currentPackage && otherPackage.startsWith("${currentPackage}.") }
|
||||
fun noOverlap(packages: Collection<String>) = packages.all { currentPackage ->
|
||||
packages.none { otherPackage -> otherPackage != currentPackage && otherPackage.startsWith("$currentPackage.") }
|
||||
}
|
||||
|
||||
private fun KProperty1<out NetworkParameters, Any?>.isAutoAcceptable(): Boolean {
|
||||
@ -117,14 +117,14 @@ data class NetworkParameters(
|
||||
}
|
||||
|
||||
init {
|
||||
require(minimumPlatformVersion > 0) { "minimumPlatformVersion must be at least 1" }
|
||||
require(minimumPlatformVersion > 0) { "Minimum platform level must be at least 1" }
|
||||
require(notaries.distinctBy { it.identity } == notaries) { "Duplicate notary identities" }
|
||||
require(epoch > 0) { "epoch must be at least 1" }
|
||||
require(maxMessageSize > 0) { "maxMessageSize must be at least 1" }
|
||||
require(maxTransactionSize > 0) { "maxTransactionSize must be at least 1" }
|
||||
require(!eventHorizon.isNegative) { "eventHorizon must be positive value" }
|
||||
require(epoch > 0) { "Epoch must be at least 1" }
|
||||
require(maxMessageSize > 0) { "Maximum message size must be at least 1" }
|
||||
require(maxTransactionSize > 0) { "Maximum transaction size must be at least 1" }
|
||||
require(!eventHorizon.isNegative) { "Event Horizon must be a positive value" }
|
||||
packageOwnership.keys.forEach(::requirePackageValid)
|
||||
require(noOverlap(packageOwnership.keys)) { "multiple packages added to the packageOwnership overlap." }
|
||||
require(noOverlap(packageOwnership.keys)) { "Multiple packages added to the packageOwnership overlap." }
|
||||
}
|
||||
|
||||
fun copy(minimumPlatformVersion: Int,
|
||||
@ -217,3 +217,7 @@ data class NotaryInfo(val identity: Party, val validating: Boolean)
|
||||
* version.
|
||||
*/
|
||||
class ZoneVersionTooLowException(message: String) : CordaRuntimeException(message)
|
||||
|
||||
private fun KProperty1<out NetworkParameters, Any?>.isAutoAcceptable(): Boolean {
|
||||
return this.findAnnotation<AutoAcceptable>() != null
|
||||
}
|
@ -288,7 +288,7 @@ Configuring a node where the Corda Compatibility Zone's registration and Network
|
||||
|
||||
.. literalinclude:: example-code/src/main/resources/example-node-with-networkservices.conf
|
||||
|
||||
Fields Override
|
||||
Fields override
|
||||
---------------
|
||||
JVM options or environmental variables prefixed with ``corda.`` can override ``node.conf`` fields.
|
||||
Provided system properties can also set values for absent fields in ``node.conf``.
|
||||
@ -299,7 +299,7 @@ This is an example of adding/overriding the keyStore password :
|
||||
|
||||
java -Dcorda.rpcSettings.ssl.keyStorePassword=mypassword -jar node.jar
|
||||
|
||||
CRL Configuration
|
||||
CRL configuration
|
||||
-----------------
|
||||
The Corda Network provides an endpoint serving an empty certificate revocation list for the TLS-level certificates.
|
||||
This is intended for deployments that do not provide a CRL infrastructure but still require a strict CRL mode checking.
|
||||
@ -318,7 +318,9 @@ Together with the above configuration `tlsCertCrlIssuer` option needs to be set
|
||||
This set-up ensures that the TLS-level certificates are embedded with the CRL distribution point referencing the CRL issued by R3.
|
||||
In cases where a proprietary CRL infrastructure is provided those values need to be changed accordingly.
|
||||
|
||||
Hiding Sensitive Data
|
||||
.. _corda-configuration-hiding-sensitive-data:
|
||||
|
||||
Hiding sensitive data
|
||||
---------------------
|
||||
A frequent requirement is that configuration files must not expose passwords to unauthorised readers. By leveraging environment variables, it is possible to hide passwords and other similar fields.
|
||||
|
||||
|
@ -15,7 +15,7 @@ In addition to the network map, all the nodes must also use the same set of netw
|
||||
which guarantee interoperability between the nodes. The HTTP network map distributes the network parameters which are downloaded
|
||||
automatically by the nodes. In the absence of this the network parameters must be generated locally.
|
||||
|
||||
For these reasons, test deployments can avail themselves of the network bootstrapper. This is a tool that scans all the
|
||||
For these reasons, test deployments can avail themselves of the Network Bootstrapper. This is a tool that scans all the
|
||||
node configurations from a common directory to generate the network parameters file, which is then copied to all the nodes'
|
||||
directories. It also copies each node's node-info file to every other node so that they can all be visible to each other.
|
||||
|
||||
@ -41,7 +41,7 @@ For example running the command on a directory containing these files:
|
||||
└── partyb_node.conf // Party B's node.conf file
|
||||
|
||||
will generate directories containing three nodes: ``notary``, ``partya`` and ``partyb``. They will each use the ``corda.jar``
|
||||
that comes with the bootstrapper. If a different version of Corda is required then simply place that ``corda.jar`` file
|
||||
that comes with the Network Bootstrapper. If a different version of Corda is required then simply place that ``corda.jar`` file
|
||||
alongside the configuration files in the directory.
|
||||
|
||||
You can also have the node directories containing their "node.conf" files already laid out. The previous example would be:
|
||||
@ -56,7 +56,7 @@ You can also have the node directories containing their "node.conf" files alread
|
||||
└── partyb
|
||||
└── node.conf
|
||||
|
||||
Similarly, each node directory may contain its own ``corda.jar``, which the bootstrapper will use instead.
|
||||
Similarly, each node directory may contain its own ``corda.jar``, which the Bootstrapper will use instead.
|
||||
|
||||
Providing CorDapps to the Network Bootstrapper
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
@ -87,7 +87,7 @@ Any CorDapps provided when bootstrapping a network will be scanned for contracts
|
||||
The CorDapp JARs will be hashed and scanned for ``Contract`` classes. These contract class implementations will become part
|
||||
of the whitelisted contracts in the network parameters (see ``NetworkParameters.whitelistedContractImplementations`` :doc:`network-map`).
|
||||
|
||||
By default the bootstrapper will whitelist all the contracts found in the unsigned CorDapp JARs (a JAR file not signed by jarSigner tool).
|
||||
By default the Bootstrapper will whitelist all the contracts found in the unsigned CorDapp JARs (a JAR file not signed by jarSigner tool).
|
||||
Whitelisted contracts are checked by `Zone constraints`, while contract classes from signed JARs will be checked by `Signature constraints`.
|
||||
To prevent certain contracts from unsigned JARs from being whitelisted, add their fully qualified class name in the ``exclude_whitelist.txt``.
|
||||
These will instead use the more restrictive ``HashAttachmentConstraint``.
|
||||
@ -103,7 +103,7 @@ For example:
|
||||
Modifying a bootstrapped network
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
The network bootstrapper is provided as a development tool for setting up Corda networks for development and testing.
|
||||
The Network Bootstrapper is provided as a development tool for setting up Corda networks for development and testing.
|
||||
There is some limited functionality which can be used to make changes to a network, but for anything more complicated consider
|
||||
using a :doc:`network-map` server.
|
||||
|
||||
@ -115,7 +115,7 @@ the nodes are being run on different machines you need to do the following:
|
||||
* Run the Network Bootstrapper from the root directory
|
||||
* Copy each individual node's directory back to the original machine
|
||||
|
||||
The network bootstrapper cannot dynamically update the network if an existing node has changed something in their node-info,
|
||||
The Network Bootstrapper cannot dynamically update the network if an existing node has changed something in their node-info,
|
||||
e.g. their P2P address. For this the new node-info file will need to be placed in the other nodes' ``additional-node-infos`` directory.
|
||||
If the nodes are located on different machines, then a utility such as `rsync <https://en.wikipedia.org/wiki/Rsync>`_ can be used
|
||||
so that the nodes can share node-infos.
|
||||
@ -123,11 +123,11 @@ so that the nodes can share node-infos.
|
||||
Adding a new node to the network
|
||||
--------------------------------
|
||||
|
||||
Running the bootstrapper again on the same network will allow a new node to be added and its
|
||||
Running the Bootstrapper again on the same network will allow a new node to be added and its
|
||||
node-info distributed to the existing nodes.
|
||||
|
||||
As an example, if we have an existing bootstrapped network, with a Notary and PartyA and we want to add a PartyB, we
|
||||
can use the network bootstrapper on the following network structure:
|
||||
can use the Network Bootstrapper on the following network structure:
|
||||
|
||||
.. sourcecode:: none
|
||||
|
||||
@ -148,7 +148,7 @@ can use the network bootstrapper on the following network structure:
|
||||
│ └── node-info-partya
|
||||
└── partyb_node.conf // the node.conf for the node to be added
|
||||
|
||||
Then run the network bootstrapper again from the root dir:
|
||||
Then run the Network Bootstrapper again from the root dir:
|
||||
|
||||
``java -jar network-bootstrapper-VERSION.jar --dir <nodes-root-dir>``
|
||||
|
||||
@ -182,19 +182,19 @@ Which will give the following:
|
||||
├── node-info-partya
|
||||
└── node-info-partyb
|
||||
|
||||
The bootstrapper will generate a directory and the ``node-info`` file for PartyB, and will also make sure a copy of each
|
||||
The Bootstrapper will generate a directory and the ``node-info`` file for PartyB, and will also make sure a copy of each
|
||||
nodes' ``node-info`` file is in the ``additional-node-info`` directory of every node. Any other files in the existing nodes,
|
||||
such a generated keys, will be unaffected.
|
||||
|
||||
.. note:: The bootstrapper is provided for test deployments and can only generate information for nodes collected on
|
||||
the same machine. If a network needs to be updated using the bootstrapper once deployed, the nodes will need
|
||||
.. note:: The Network Bootstrapper is provided for test deployments and can only generate information for nodes collected on
|
||||
the same machine. If a network needs to be updated using the Bootstrapper once deployed, the nodes will need
|
||||
collecting back together.
|
||||
|
||||
Updating the contract whitelist for bootstrapped networks
|
||||
---------------------------------------------------------
|
||||
|
||||
If the network already has a set of network parameters defined (i.e. the node directories all contain the same network-parameters
|
||||
file) then the bootstrapper can be used to append contracts from new CorDapps to the current whitelist.
|
||||
file) then the Network Bootstrapper can be used to append contracts from new CorDapps to the current whitelist.
|
||||
For example, with the following pre-generated network:
|
||||
|
||||
.. sourcecode:: none
|
||||
@ -217,7 +217,7 @@ For example, with the following pre-generated network:
|
||||
│ └── cordapp-a.jar
|
||||
└── cordapp-b.jar // The new cordapp to add to the existing nodes
|
||||
|
||||
Then run the network bootstrapper again from the root dir:
|
||||
Then run the Network Bootstrapper again from the root dir:
|
||||
|
||||
``java -jar network-bootstrapper-VERSION.jar --dir <nodes-root-dir>``
|
||||
|
||||
@ -247,81 +247,183 @@ To give the following:
|
||||
|
||||
.. note:: The whitelist can only ever be appended to. Once added a contract implementation can never be removed.
|
||||
|
||||
Package namespace ownership
|
||||
----------------------------
|
||||
Modifying the network parameters
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Package namespace ownership is a Corda security feature that allows a compatibility zone to give ownership of parts of the Java package namespace to registered users (e.g. CorDapp development organisations).
|
||||
The exact mechanism used to claim a namespace is up to the zone operator. A typical approach would be to accept an SSL
|
||||
certificate with the domain in it as proof of domain ownership, or to accept an email from that domain.
|
||||
The Network Bootstrapper creates a network parameters file when bootstrapping a network, using a set of sensible defaults. However, if you would like
|
||||
to override these defaults when testing, there are two ways of doing this. Options can be overridden via the command line or by supplying a configuration
|
||||
file. If the same parameter is overridden both by a command line argument and in the configuration file, the command line value
|
||||
will take precedence.
|
||||
|
||||
Overriding network parameters via command line
|
||||
----------------------------------------------
|
||||
|
||||
The ``--minimum-platform-version``, ``--max-message-size``, ``--max-transaction-size`` and ``--event-horizon`` command line parameters can
|
||||
be used to override the default network parameters. See `Command line options`_ for more information.
|
||||
|
||||
Overriding network parameters via a file
|
||||
----------------------------------------
|
||||
|
||||
You can provide a network parameters overrides file using the following syntax:
|
||||
|
||||
``java -jar network-bootstrapper-VERSION.jar --network-parameters-overrides=<path_to_file>``
|
||||
|
||||
Or alternatively, by using the short form version:
|
||||
|
||||
``java -jar network-bootstrapper-VERSION.jar -n=<path_to_file>``
|
||||
|
||||
The network parameter overrides file is a HOCON file with the following fields, all of which are optional. Any field that is not provided will be
|
||||
ignored. If a field is not provided and you are bootstrapping a new network, a sensible default value will be used. If a field is not provided and you
|
||||
are updating an existing network, the value in the existing network parameters file will be used.
|
||||
|
||||
.. note:: All fields can be used with placeholders for environment variables. For example: ``${KEY_STORE_PASSWORD}`` would be replaced by the contents of environment
|
||||
variable ``KEY_STORE_PASSWORD``. See: :ref:`corda-configuration-hiding-sensitive-data` .
|
||||
|
||||
The available configuration fields are listed below:
|
||||
|
||||
:minimumPlatformVersion: The minimum supported version of the Corda platform that is required for nodes in the network.
|
||||
|
||||
:maxMessageSize: The maximum permitted message size, in bytes. This is currently ignored but will be used in a future release.
|
||||
|
||||
:maxTransactionSize: The maximum permitted transaction size, in bytes.
|
||||
|
||||
:eventHorizon: The time after which nodes will be removed from the network map if they have not been seen during this period. This parameter uses
|
||||
the ``parse`` function on the ``java.time.Duration`` class to interpret the data. See `here <https://docs.oracle.com/javase/8/docs/api/java/time/Duration.html#parse-java.lang.CharSequence->`_
|
||||
for information on valid inputs.
|
||||
|
||||
:packageOwnership: A list of package owners. See `Package namespace ownership`_ for more information. For each package owner, the following fields
|
||||
are required:
|
||||
|
||||
:packageName: Java package name (e.g `com.my_company` ).
|
||||
|
||||
:keystore: The path of the keystore file containing the signed certificate.
|
||||
|
||||
:keystorePassword: The password for the given keystore (not to be confused with the key password).
|
||||
|
||||
:keystoreAlias: The alias for the name associated with the certificate to be associated with the package namespace.
|
||||
|
||||
An example configuration file:
|
||||
|
||||
.. parsed-literal::
|
||||
|
||||
minimumPlatformVersion=4
|
||||
maxMessageSize=10485760
|
||||
maxTransactionSize=524288000
|
||||
eventHorizon="30 days"
|
||||
packageOwnership=[
|
||||
{
|
||||
packageName="com.example"
|
||||
keystore="myteststore"
|
||||
keystorePassword="MyStorePassword"
|
||||
keystoreAlias="MyKeyAlias"
|
||||
}
|
||||
]
|
||||
|
||||
Package namespace ownership
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Package namespace ownership is a Corda security feature that allows a compatibility zone to give ownership of parts of the Java package
|
||||
namespace to registered users (e.g. a CorDapp development organisation). The exact mechanism used to claim a namespace is up to the zone
|
||||
operator. A typical approach would be to accept an SSL certificate with the domain in it as proof of domain ownership, or to accept an email from that domain.
|
||||
|
||||
.. note:: Read more about *Package ownership* :doc:`here<design/data-model-upgrades/package-namespace-ownership>`.
|
||||
|
||||
A Java package namespace is case insensitive and cannot be a sub-package of an existing registered namespace.
|
||||
See `Naming a Package <https://docs.oracle.com/javase/tutorial/java/package/namingpkgs.html>`_ and `Naming Conventions <https://www.oracle.com/technetwork/java/javase/documentation/codeconventions-135099.html#28840 for guidelines and conventions>`_ for guidelines on naming conventions.
|
||||
|
||||
Registration of a java package namespace requires creation of a signed certificate as generated by the
|
||||
The registration of a Java package namespace requires the creation of a signed certificate as generated by the
|
||||
`Java keytool <https://docs.oracle.com/javase/8/docs/technotes/tools/windows/keytool.html>`_.
|
||||
|
||||
The following four items are passed as a semi-colon separated string to the ``--register-package-owner`` command:
|
||||
The packages can be registered by supplying a network parameters override config file via the command line, using the ``--network-parameters-overrides`` command.
|
||||
|
||||
1. Java package name (e.g `com.my_company` ).
|
||||
2. Keystore file refers to the full path of the file containing the signed certificate.
|
||||
3. Password refers to the key store password (not to be confused with the key password).
|
||||
4. Alias refers to the name associated with a certificate containing the public key to be associated with the package namespace.
|
||||
For each package to be registered, the following are required:
|
||||
|
||||
Let's use the `Example CorDapp <https://github.com/corda/cordapp-example>`_ to initialise a simple network, and then register and unregister a package namespace.
|
||||
:packageName: Java package name (e.g `com.my_company` ).
|
||||
|
||||
:keystore: The path of the keystore file containing the signed certificate. If a relative path is provided, it is assumed to be relative to the
|
||||
location of the configuration file.
|
||||
|
||||
:keystorePassword: The password for the given keystore (not to be confused with the key password).
|
||||
|
||||
:keystoreAlias: The alias for the name associated with the certificate to be associated with the package namespace.
|
||||
|
||||
Using the `Example CorDapp <https://github.com/corda/cordapp-example>`_ as an example, we will initialise a simple network and then register and unregister a package namespace.
|
||||
Checkout the Example CorDapp and follow the instructions to build it `here <https://docs.corda.net/tutorial-cordapp.html#building-the-example-cordapp>`_.
|
||||
|
||||
.. note:: You can point to any existing bootstrapped corda network (this will have the effect of updating the associated network parameters file).
|
||||
|
||||
1. Create a new public key to use for signing the java package namespace we wish to register:
|
||||
#. Create a new public key to use for signing the Java package namespace we wish to register:
|
||||
|
||||
.. code-block:: shell
|
||||
|
||||
$JAVA_HOME/bin/keytool -genkeypair -keystore _teststore -storepass MyStorePassword -keyalg RSA -alias MyKeyAlias -keypass MyKeyPassword -dname "O=Alice Corp, L=Madrid, C=ES"
|
||||
|
||||
This will generate a key store file called ``_teststore`` in the current directory.
|
||||
|
||||
#. Create a ``network-parameters.conf`` file in the same directory, with the following information:
|
||||
|
||||
.. parsed-literal::
|
||||
|
||||
packageOwnership=[
|
||||
{
|
||||
packageName="com.example"
|
||||
keystore="_teststore"
|
||||
keystorePassword="MyStorePassword"
|
||||
keystoreAlias="MyKeyAlias"
|
||||
}
|
||||
]
|
||||
|
||||
#. Register the package namespace to be claimed by the public key generated above:
|
||||
|
||||
.. code-block:: shell
|
||||
|
||||
# Register the Java package namespace using the Network Bootstrapper
|
||||
java -jar network-bootstrapper.jar --dir build/nodes --network-parameter-overrides=network-parameters.conf
|
||||
|
||||
|
||||
#. To unregister the package namespace, edit the ``network-parameters.conf`` file to remove the package:
|
||||
|
||||
.. parsed-literal::
|
||||
|
||||
packageOwnership=[]
|
||||
|
||||
#. Unregister the package namespace:
|
||||
|
||||
.. code-block:: shell
|
||||
|
||||
# Unregister the Java package namespace using the Network Bootstrapper
|
||||
java -jar network-bootstrapper.jar --dir build/nodes --network-parameter-overrides=network-parameters.conf
|
||||
|
||||
Command line options
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
The Network Bootstrapper can be started with the following command line options:
|
||||
|
||||
.. code-block:: shell
|
||||
|
||||
$JAVA_HOME/bin/keytool -genkeypair -keystore _teststore -storepass MyStorePassword -keyalg RSA -alias MyKeyAlias -keypass MyKeyPassword -dname "O=Alice Corp, L=Madrid, C=ES"
|
||||
|
||||
This will generate a key store file called ``_teststore`` in the current directory.
|
||||
|
||||
2. Register the package namespace to be claimed by the public key generated above:
|
||||
|
||||
.. code-block:: shell
|
||||
|
||||
# Register the java package namespace using the bootstrapper tool
|
||||
java -jar network-bootstrapper.jar --dir build/nodes --register-package-owner com.example;./_teststore;MyStorePassword;MyKeyAlias
|
||||
|
||||
3. Unregister the package namespace:
|
||||
|
||||
.. code-block:: shell
|
||||
|
||||
# Unregister the java package namespace using the bootstrapper tool
|
||||
java -jar network-bootstrapper.jar --dir build/nodes --unregister-package-owner com.example
|
||||
|
||||
Command-line options
|
||||
--------------------
|
||||
|
||||
The network bootstrapper can be started with the following command-line options:
|
||||
|
||||
.. code-block:: shell
|
||||
|
||||
bootstrapper [-hvV] [--no-copy] [--dir=<dir>] [--logging-level=<loggingLevel>]
|
||||
[--minimum-platform-version=<minimumPlatformVersion>]
|
||||
[--register-package-owner java-package-namespace=keystore-file:password:alias]
|
||||
[--unregister-package-owner java-package-namespace]
|
||||
[COMMAND]
|
||||
bootstrapper [-hvV] [--no-copy] [--dir=<dir>] [--event-horizon=<eventHorizon>]
|
||||
[--logging-level=<loggingLevel>]
|
||||
[--max-message-size=<maxMessageSize>]
|
||||
[--max-transaction-size=<maxTransactionSize>]
|
||||
[--minimum-platform-version=<minimumPlatformVersion>]
|
||||
[-n=<networkParametersFile>] [COMMAND]
|
||||
|
||||
* ``--dir=<dir>``: Root directory containing the node configuration files and CorDapp JARs that will form the test network.
|
||||
It may also contain existing node directories. Defaults to the current directory.
|
||||
It may also contain existing node directories. Defaults to the current directory.
|
||||
* ``--no-copy``: Don't copy the CorDapp JARs into the nodes' "cordapps" directories.
|
||||
* ``--verbose``, ``--log-to-console``, ``-v``: If set, prints logging to the console as well as to a file.
|
||||
* ``--logging-level=<loggingLevel>``: Enable logging at this level and higher. Possible values: ERROR, WARN, INFO, DEBUG, TRACE. Default: INFO.
|
||||
* ``--help``, ``-h``: Show this help message and exit.
|
||||
* ``--version``, ``-V``: Print version information and exit.
|
||||
* ``--minimum-platform-version``: The minimum platform version to use in the generated network-parameters.
|
||||
* ``--register-package-owner``: Register a java package namespace with its owners public key.
|
||||
* ``--unregister-package-owner``: Unregister a java package namespace.
|
||||
* ``--minimum-platform-version``: The minimum platform version to use in the network-parameters.
|
||||
* ``--max-message-size``: The maximum message size to use in the network-parameters, in bytes.
|
||||
* ``--max-transaction-size``: The maximum transaction size to use in the network-parameters, in bytes.
|
||||
* ``--event-horizon``: The event horizon to use in the network-parameters.
|
||||
* ``--network-parameter-overrides=<networkParametersFile>``, ``-n=<networkParametersFile>`: Overrides the default network parameters with those
|
||||
in the given file. See `Overriding network parameters via a file`_ for more information.
|
||||
|
||||
|
||||
Sub-commands
|
||||
^^^^^^^^^^^^
|
||||
|
||||
``install-shell-extensions``: Install ``bootstrapper`` alias and auto completion for bash and zsh. See :doc:`cli-application-shell-extensions` for more info.
|
||||
------------
|
||||
|
||||
``install-shell-extensions``: Install ``bootstrapper`` alias and auto completion for bash and zsh. See :doc:`cli-application-shell-extensions` for more info.
|
@ -3,7 +3,6 @@ package net.corda.nodeapi.internal.network
|
||||
import com.typesafe.config.Config
|
||||
import com.typesafe.config.ConfigException
|
||||
import com.typesafe.config.ConfigFactory
|
||||
import net.corda.core.crypto.toStringShort
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.internal.*
|
||||
@ -19,7 +18,6 @@ import net.corda.core.serialization.deserialize
|
||||
import net.corda.core.serialization.internal.SerializationEnvironment
|
||||
import net.corda.core.serialization.internal._contextSerializationEnv
|
||||
import net.corda.core.utilities.days
|
||||
import net.corda.core.utilities.filterNotNullValues
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.core.utilities.seconds
|
||||
import net.corda.nodeapi.internal.*
|
||||
@ -36,6 +34,7 @@ import java.nio.file.FileAlreadyExistsException
|
||||
import java.nio.file.Path
|
||||
import java.nio.file.StandardCopyOption.REPLACE_EXISTING
|
||||
import java.security.PublicKey
|
||||
import java.time.Duration
|
||||
import java.time.Instant
|
||||
import java.util.*
|
||||
import java.util.concurrent.Executors
|
||||
@ -56,7 +55,7 @@ class NetworkBootstrapper
|
||||
internal constructor(private val initSerEnv: Boolean,
|
||||
private val embeddedCordaJar: () -> InputStream,
|
||||
private val nodeInfosGenerator: (List<Path>) -> List<Path>,
|
||||
private val contractsJarConverter: (Path) -> ContractsJar) {
|
||||
private val contractsJarConverter: (Path) -> ContractsJar) : NetworkBootstrapperWithOverridableParameters {
|
||||
|
||||
constructor() : this(
|
||||
initSerEnv = true,
|
||||
@ -128,6 +127,9 @@ internal constructor(private val initSerEnv: Boolean,
|
||||
throw IllegalStateException("Error while generating node info file. Please check the logs in $nodeDir.")
|
||||
}
|
||||
}
|
||||
|
||||
const val DEFAULT_MAX_MESSAGE_SIZE: Int = 10485760
|
||||
const val DEFAULT_MAX_TRANSACTION_SIZE: Int = 524288000
|
||||
}
|
||||
|
||||
sealed class NotaryCluster {
|
||||
@ -189,14 +191,15 @@ internal constructor(private val initSerEnv: Boolean,
|
||||
}
|
||||
|
||||
/** Entry point for the tool */
|
||||
fun bootstrap(directory: Path, copyCordapps: Boolean, minimumPlatformVersion: Int, packageOwnership: Map<String, PublicKey?> = emptyMap()) {
|
||||
require(minimumPlatformVersion <= PLATFORM_VERSION) { "Minimum platform version cannot be greater than $PLATFORM_VERSION" }
|
||||
// Don't accidently include the bootstrapper jar as a CorDapp!
|
||||
override fun bootstrap(directory: Path, copyCordapps: Boolean, networkParameterOverrides: NetworkParametersOverrides) {
|
||||
require(networkParameterOverrides.minimumPlatformVersion == null || networkParameterOverrides.minimumPlatformVersion <= PLATFORM_VERSION) { "Minimum platform version cannot be greater than $PLATFORM_VERSION" }
|
||||
// Don't accidentally include the bootstrapper jar as a CorDapp!
|
||||
val bootstrapperJar = javaClass.location.toPath()
|
||||
val cordappJars = directory.list { paths ->
|
||||
paths.filter { it.toString().endsWith(".jar") && !it.isSameAs(bootstrapperJar) && it.fileName.toString() != "corda.jar" }.toList()
|
||||
paths.filter { it.toString().endsWith(".jar") && !it.isSameAs(bootstrapperJar) && it.fileName.toString() != "corda.jar" }
|
||||
.toList()
|
||||
}
|
||||
bootstrap(directory, cordappJars, copyCordapps, fromCordform = false, minimumPlatformVersion = minimumPlatformVersion, packageOwnership = packageOwnership)
|
||||
bootstrap(directory, cordappJars, copyCordapps, fromCordform = false, networkParametersOverrides = networkParameterOverrides)
|
||||
}
|
||||
|
||||
private fun bootstrap(
|
||||
@ -204,8 +207,7 @@ internal constructor(private val initSerEnv: Boolean,
|
||||
cordappJars: List<Path>,
|
||||
copyCordapps: Boolean,
|
||||
fromCordform: Boolean,
|
||||
minimumPlatformVersion: Int = PLATFORM_VERSION,
|
||||
packageOwnership: Map<String, PublicKey?> = emptyMap()
|
||||
networkParametersOverrides: NetworkParametersOverrides = NetworkParametersOverrides()
|
||||
) {
|
||||
directory.createDirectories()
|
||||
println("Bootstrapping local test network in $directory")
|
||||
@ -250,8 +252,9 @@ internal constructor(private val initSerEnv: Boolean,
|
||||
println("Gathering notary identities")
|
||||
val notaryInfos = gatherNotaryInfos(nodeInfoFiles, configs)
|
||||
println("Generating contract implementations whitelist")
|
||||
// Only add contracts to the whitelist from unsigned jars
|
||||
val newWhitelist = generateWhitelist(existingNetParams, readExcludeWhitelist(directory), cordappJars.filter { !isSigned(it) }.map(contractsJarConverter))
|
||||
val newNetParams = installNetworkParameters(notaryInfos, newWhitelist, existingNetParams, nodeDirs, minimumPlatformVersion, packageOwnership)
|
||||
val newNetParams = installNetworkParameters(notaryInfos, newWhitelist, existingNetParams, nodeDirs, networkParametersOverrides)
|
||||
if (newNetParams != existingNetParams) {
|
||||
println("${if (existingNetParams == null) "New" else "Updated"} $newNetParams")
|
||||
} else {
|
||||
@ -283,7 +286,8 @@ internal constructor(private val initSerEnv: Boolean,
|
||||
println("Generating node directory for $nodeName")
|
||||
val nodeDir = (directory / nodeName).createDirectories()
|
||||
confFile.copyTo(nodeDir / "node.conf", REPLACE_EXISTING)
|
||||
webServerConfFiles.firstOrNull { directory.relativize(it).toString().removeSuffix("_web-server.conf") == nodeName }?.copyTo(nodeDir / "web-server.conf", REPLACE_EXISTING)
|
||||
webServerConfFiles.firstOrNull { directory.relativize(it).toString().removeSuffix("_web-server.conf") == nodeName }
|
||||
?.copyTo(nodeDir / "web-server.conf", REPLACE_EXISTING)
|
||||
cordaJar.copyToDirectory(nodeDir, REPLACE_EXISTING)
|
||||
}
|
||||
|
||||
@ -378,50 +382,42 @@ internal constructor(private val initSerEnv: Boolean,
|
||||
throw IllegalStateException(msg.toString())
|
||||
}
|
||||
|
||||
private fun defaultNetworkParametersWith(notaryInfos: List<NotaryInfo>,
|
||||
whitelist: Map<String, List<AttachmentId>>): NetworkParameters {
|
||||
return NetworkParameters(
|
||||
minimumPlatformVersion = PLATFORM_VERSION,
|
||||
notaries = notaryInfos,
|
||||
modifiedTime = Instant.now(),
|
||||
maxMessageSize = DEFAULT_MAX_MESSAGE_SIZE,
|
||||
maxTransactionSize = DEFAULT_MAX_TRANSACTION_SIZE,
|
||||
whitelistedContractImplementations = whitelist,
|
||||
packageOwnership = emptyMap(),
|
||||
epoch = 1,
|
||||
eventHorizon = 30.days
|
||||
)
|
||||
}
|
||||
|
||||
private fun installNetworkParameters(
|
||||
notaryInfos: List<NotaryInfo>,
|
||||
whitelist: Map<String, List<AttachmentId>>,
|
||||
existingNetParams: NetworkParameters?,
|
||||
nodeDirs: List<Path>,
|
||||
minimumPlatformVersion: Int,
|
||||
packageOwnership: Map<String, PublicKey?>
|
||||
networkParametersOverrides: NetworkParametersOverrides
|
||||
): NetworkParameters {
|
||||
// TODO Add config for maxMessageSize and maxTransactionSize
|
||||
val netParams = if (existingNetParams != null) {
|
||||
if (existingNetParams.whitelistedContractImplementations == whitelist && existingNetParams.notaries == notaryInfos &&
|
||||
existingNetParams.packageOwnership.entries.containsAll(packageOwnership.entries)) {
|
||||
existingNetParams
|
||||
} else {
|
||||
var updatePackageOwnership = mutableMapOf(*existingNetParams.packageOwnership.map { Pair(it.key, it.value) }.toTypedArray())
|
||||
packageOwnership.forEach { key, value ->
|
||||
if (value == null) {
|
||||
if (updatePackageOwnership.remove(key) != null)
|
||||
println("Unregistering package $key")
|
||||
} else {
|
||||
if (updatePackageOwnership.put(key, value) == null)
|
||||
println("Registering package $key for owner ${value.toStringShort()}")
|
||||
}
|
||||
}
|
||||
existingNetParams.copy(
|
||||
notaries = notaryInfos,
|
||||
val newNetParams = existingNetParams
|
||||
.copy(notaries = notaryInfos, whitelistedContractImplementations = whitelist)
|
||||
.overrideWith(networkParametersOverrides)
|
||||
if (newNetParams != existingNetParams) {
|
||||
newNetParams.copy(
|
||||
modifiedTime = Instant.now(),
|
||||
whitelistedContractImplementations = whitelist,
|
||||
packageOwnership = updatePackageOwnership,
|
||||
epoch = existingNetParams.epoch + 1
|
||||
)
|
||||
} else {
|
||||
existingNetParams
|
||||
}
|
||||
} else {
|
||||
NetworkParameters(
|
||||
minimumPlatformVersion = minimumPlatformVersion,
|
||||
notaries = notaryInfos,
|
||||
modifiedTime = Instant.now(),
|
||||
maxMessageSize = 10485760,
|
||||
maxTransactionSize = 524288000,
|
||||
whitelistedContractImplementations = whitelist,
|
||||
packageOwnership = packageOwnership.filterNotNullValues(),
|
||||
epoch = 1,
|
||||
eventHorizon = 30.days
|
||||
)
|
||||
defaultNetworkParametersWith(notaryInfos, whitelist).overrideWith(networkParametersOverrides)
|
||||
}
|
||||
val copier = NetworkParametersCopier(netParams, overwriteFile = true)
|
||||
nodeDirs.forEach(copier::install)
|
||||
@ -435,7 +431,7 @@ internal constructor(private val initSerEnv: Boolean,
|
||||
// Nodes which are part of a distributed notary have a second identity which is the composite identity of the
|
||||
// cluster and is shared by all the other members. This is the notary identity.
|
||||
2 -> legalIdentities[1]
|
||||
else -> throw IllegalArgumentException("Not sure how to get the notary identity in this scenerio: $this")
|
||||
else -> throw IllegalArgumentException("Not sure how to get the notary identity in this scenario: $this")
|
||||
}
|
||||
}
|
||||
|
||||
@ -464,3 +460,27 @@ internal constructor(private val initSerEnv: Boolean,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun NetworkParameters.overrideWith(override: NetworkParametersOverrides): NetworkParameters {
|
||||
return this.copy(
|
||||
minimumPlatformVersion = override.minimumPlatformVersion ?: this.minimumPlatformVersion,
|
||||
maxMessageSize = override.maxMessageSize ?: this.maxMessageSize,
|
||||
maxTransactionSize = override.maxTransactionSize ?: this.maxTransactionSize,
|
||||
eventHorizon = override.eventHorizon ?: this.eventHorizon,
|
||||
packageOwnership = override.packageOwnership?.map { it.javaPackageName to it.publicKey }?.toMap() ?: this.packageOwnership
|
||||
)
|
||||
}
|
||||
|
||||
data class PackageOwner(val javaPackageName: String, val publicKey: PublicKey)
|
||||
|
||||
data class NetworkParametersOverrides(
|
||||
val minimumPlatformVersion: Int? = null,
|
||||
val maxMessageSize: Int? = null,
|
||||
val maxTransactionSize: Int? = null,
|
||||
val packageOwnership: List<PackageOwner>? = null,
|
||||
val eventHorizon: Duration? = null
|
||||
)
|
||||
|
||||
interface NetworkBootstrapperWithOverridableParameters {
|
||||
fun bootstrap(directory: Path, copyCordapps: Boolean, networkParameterOverrides: NetworkParametersOverrides = NetworkParametersOverrides())
|
||||
}
|
@ -11,9 +11,12 @@ import net.corda.core.serialization.serialize
|
||||
import net.corda.node.services.config.NotaryConfig
|
||||
import net.corda.nodeapi.internal.DEV_ROOT_CA
|
||||
import net.corda.core.internal.NODE_INFO_DIRECTORY
|
||||
import net.corda.core.utilities.days
|
||||
import net.corda.nodeapi.internal.SignedNodeInfo
|
||||
import net.corda.nodeapi.internal.config.parseAs
|
||||
import net.corda.nodeapi.internal.config.toConfig
|
||||
import net.corda.nodeapi.internal.network.NetworkBootstrapper.Companion.DEFAULT_MAX_MESSAGE_SIZE
|
||||
import net.corda.nodeapi.internal.network.NetworkBootstrapper.Companion.DEFAULT_MAX_TRANSACTION_SIZE
|
||||
import net.corda.nodeapi.internal.network.NodeInfoFilesCopier.Companion.NODE_INFO_FILE_NAME_PREFIX
|
||||
import net.corda.testing.core.*
|
||||
import net.corda.testing.internal.createNodeInfoAndSigned
|
||||
@ -26,6 +29,7 @@ import org.junit.rules.ExpectedException
|
||||
import org.junit.rules.TemporaryFolder
|
||||
import java.nio.file.Path
|
||||
import java.security.PublicKey
|
||||
import java.time.Duration
|
||||
import kotlin.streams.toList
|
||||
|
||||
class NetworkBootstrapperTest {
|
||||
@ -210,6 +214,24 @@ class NetworkBootstrapperTest {
|
||||
assertThat(networkParameters.epoch).isEqualTo(2)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `network parameters overrides`() {
|
||||
createNodeConfFile("alice", aliceConfig)
|
||||
val minimumPlatformVersion = 2
|
||||
val maxMessageSize = 10000
|
||||
val maxTransactionSize = 20000
|
||||
val eventHorizon = 7.days
|
||||
bootstrap(minimumPlatformVerison = minimumPlatformVersion,
|
||||
maxMessageSize = maxMessageSize,
|
||||
maxTransactionSize = maxTransactionSize,
|
||||
eventHorizon = eventHorizon)
|
||||
val networkParameters = assertBootstrappedNetwork(fakeEmbeddedCordaJar, "alice" to aliceConfig)
|
||||
assertThat(networkParameters.minimumPlatformVersion).isEqualTo(minimumPlatformVersion)
|
||||
assertThat(networkParameters.maxMessageSize).isEqualTo(maxMessageSize)
|
||||
assertThat(networkParameters.maxTransactionSize).isEqualTo(maxTransactionSize)
|
||||
assertThat(networkParameters.eventHorizon).isEqualTo(eventHorizon)
|
||||
}
|
||||
|
||||
private val ALICE = TestIdentity(ALICE_NAME, 70)
|
||||
private val BOB = TestIdentity(BOB_NAME, 80)
|
||||
|
||||
@ -230,7 +252,7 @@ class NetworkBootstrapperTest {
|
||||
assertContainsPackageOwner("alice", mapOf(Pair(alicePackageName, ALICE.publicKey)))
|
||||
// register additional package name
|
||||
createNodeConfFile("bob", bobConfig)
|
||||
bootstrap(packageOwnership = mapOf(Pair(bobPackageName, BOB.publicKey)))
|
||||
bootstrap(packageOwnership = mapOf(Pair(alicePackageName, ALICE.publicKey), Pair(bobPackageName, BOB.publicKey)))
|
||||
assertContainsPackageOwner("bob", mapOf(Pair(alicePackageName, ALICE.publicKey), Pair(bobPackageName, BOB.publicKey)))
|
||||
}
|
||||
|
||||
@ -243,8 +265,8 @@ class NetworkBootstrapperTest {
|
||||
// register overlapping package name
|
||||
createNodeConfFile("bob", bobConfig)
|
||||
expectedEx.expect(IllegalArgumentException::class.java)
|
||||
expectedEx.expectMessage("multiple packages added to the packageOwnership overlap.")
|
||||
bootstrap(packageOwnership = mapOf(Pair(bobPackageName, BOB.publicKey)))
|
||||
expectedEx.expectMessage("Multiple packages added to the packageOwnership overlap.")
|
||||
bootstrap(packageOwnership = mapOf(Pair(greedyNamespace, ALICE.publicKey), Pair(bobPackageName, BOB.publicKey)))
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -253,7 +275,7 @@ class NetworkBootstrapperTest {
|
||||
bootstrap(packageOwnership = mapOf(Pair(alicePackageName, ALICE.publicKey)))
|
||||
assertContainsPackageOwner("alice", mapOf(Pair(alicePackageName, ALICE.publicKey)))
|
||||
// unregister package name
|
||||
bootstrap(packageOwnership = mapOf(Pair(alicePackageName, null)))
|
||||
bootstrap(packageOwnership = emptyMap())
|
||||
assertContainsPackageOwner("alice", emptyMap())
|
||||
}
|
||||
|
||||
@ -262,8 +284,8 @@ class NetworkBootstrapperTest {
|
||||
createNodeConfFile("alice", aliceConfig)
|
||||
bootstrap(packageOwnership = mapOf(Pair(alicePackageName, ALICE.publicKey), Pair(bobPackageName, BOB.publicKey)))
|
||||
// unregister package name
|
||||
bootstrap(packageOwnership = mapOf(Pair(alicePackageName, null)))
|
||||
assertContainsPackageOwner("alice", mapOf(Pair(bobPackageName, BOB.publicKey)))
|
||||
bootstrap(packageOwnership = mapOf(Pair(alicePackageName, ALICE.publicKey)))
|
||||
assertContainsPackageOwner("alice", mapOf(Pair(alicePackageName, ALICE.publicKey)))
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -271,19 +293,10 @@ class NetworkBootstrapperTest {
|
||||
createNodeConfFile("alice", aliceConfig)
|
||||
bootstrap(packageOwnership = mapOf(Pair(alicePackageName, ALICE.publicKey), Pair(bobPackageName, BOB.publicKey)))
|
||||
// unregister all package names
|
||||
bootstrap(packageOwnership = mapOf(Pair(alicePackageName, null), Pair(bobPackageName, null)))
|
||||
bootstrap(packageOwnership = emptyMap())
|
||||
assertContainsPackageOwner("alice", emptyMap())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `register and unregister sample package namespace in network`() {
|
||||
createNodeConfFile("alice", aliceConfig)
|
||||
bootstrap(packageOwnership = mapOf(Pair(alicePackageName, ALICE.publicKey), Pair(alicePackageName, null)))
|
||||
assertContainsPackageOwner("alice", emptyMap())
|
||||
bootstrap(packageOwnership = mapOf(Pair(alicePackageName, null), Pair(alicePackageName, ALICE.publicKey)))
|
||||
assertContainsPackageOwner("alice", mapOf(Pair(alicePackageName, ALICE.publicKey)))
|
||||
}
|
||||
|
||||
private val rootDir get() = tempFolder.root.toPath()
|
||||
|
||||
private fun fakeFileBytes(writeToFile: Path? = null): ByteArray {
|
||||
@ -292,9 +305,20 @@ class NetworkBootstrapperTest {
|
||||
return bytes
|
||||
}
|
||||
|
||||
private fun bootstrap(copyCordapps: Boolean = true, packageOwnership : Map<String, PublicKey?> = emptyMap()) {
|
||||
private fun bootstrap(copyCordapps: Boolean = true,
|
||||
packageOwnership: Map<String, PublicKey>? = emptyMap(),
|
||||
minimumPlatformVerison: Int? = PLATFORM_VERSION,
|
||||
maxMessageSize: Int? = DEFAULT_MAX_MESSAGE_SIZE,
|
||||
maxTransactionSize: Int? = DEFAULT_MAX_TRANSACTION_SIZE,
|
||||
eventHorizon: Duration? = 30.days) {
|
||||
providedCordaJar = (rootDir / "corda.jar").let { if (it.exists()) it.readAll() else null }
|
||||
bootstrapper.bootstrap(rootDir, copyCordapps, PLATFORM_VERSION, packageOwnership)
|
||||
bootstrapper.bootstrap(rootDir, copyCordapps, NetworkParametersOverrides(
|
||||
minimumPlatformVersion = minimumPlatformVerison,
|
||||
maxMessageSize = maxMessageSize,
|
||||
maxTransactionSize = maxTransactionSize,
|
||||
eventHorizon = eventHorizon,
|
||||
packageOwnership = packageOwnership?.map { PackageOwner(it.key, it.value!!) }
|
||||
))
|
||||
}
|
||||
|
||||
private fun createNodeConfFile(nodeDirName: String, config: FakeNodeConfig) {
|
||||
@ -320,19 +344,23 @@ class NetworkBootstrapperTest {
|
||||
return cordappBytes
|
||||
}
|
||||
|
||||
private val Path.networkParameters: NetworkParameters get() {
|
||||
return (this / NETWORK_PARAMS_FILE_NAME).readObject<SignedNetworkParameters>().verifiedNetworkParametersCert(DEV_ROOT_CA.certificate)
|
||||
}
|
||||
private val Path.networkParameters: NetworkParameters
|
||||
get() {
|
||||
return (this / NETWORK_PARAMS_FILE_NAME).readObject<SignedNetworkParameters>()
|
||||
.verifiedNetworkParametersCert(DEV_ROOT_CA.certificate)
|
||||
}
|
||||
|
||||
private val Path.nodeInfoFile: Path get() {
|
||||
return list { it.filter { it.fileName.toString().startsWith(NODE_INFO_FILE_NAME_PREFIX) }.toList() }.single()
|
||||
}
|
||||
private val Path.nodeInfoFile: Path
|
||||
get() {
|
||||
return list { it.filter { it.fileName.toString().startsWith(NODE_INFO_FILE_NAME_PREFIX) }.toList() }.single()
|
||||
}
|
||||
|
||||
private val Path.nodeInfo: NodeInfo get() = nodeInfoFile.readObject<SignedNodeInfo>().verified()
|
||||
|
||||
private val Path.fakeNodeConfig: FakeNodeConfig get() {
|
||||
return ConfigFactory.parseFile((this / "node.conf").toFile()).parseAs(FakeNodeConfig::class)
|
||||
}
|
||||
private val Path.fakeNodeConfig: FakeNodeConfig
|
||||
get() {
|
||||
return ConfigFactory.parseFile((this / "node.conf").toFile()).parseAs(FakeNodeConfig::class)
|
||||
}
|
||||
|
||||
private fun assertBootstrappedNetwork(cordaJar: ByteArray, vararg nodes: Pair<String, FakeNodeConfig>): NetworkParameters {
|
||||
val networkParameters = (rootDir / nodes[0].first).networkParameters
|
||||
|
@ -49,7 +49,6 @@ open class SharedNodeCmdLineOptions {
|
||||
var devMode: Boolean? = null
|
||||
|
||||
open fun parseConfiguration(configuration: Config): Valid<NodeConfiguration> {
|
||||
|
||||
val option = Configuration.Validation.Options(strict = unknownConfigKeysPolicy == UnknownConfigKeysPolicy.FAIL)
|
||||
return configuration.parseAsNodeConfiguration(option)
|
||||
}
|
||||
|
@ -143,21 +143,19 @@ open class NodeStartup : NodeStartupLogging {
|
||||
val configuration = cmdLineOptions.parseConfiguration(rawConfig).doIfValid { logRawConfig(rawConfig) }.doOnErrors(::logConfigurationErrors).optional ?: return ExitCodes.FAILURE
|
||||
|
||||
// Step 6. Configuring special serialisation requirements, i.e., bft-smart relies on Java serialization.
|
||||
attempt { banJavaSerialisation(configuration) }.doOnException { error -> error.logAsUnexpected("Exception while configuring serialisation") } as? Try.Success
|
||||
?: return ExitCodes.FAILURE
|
||||
if (attempt { banJavaSerialisation(configuration) }.doOnException { error -> error.logAsUnexpected("Exception while configuring serialisation") } !is Try.Success) return ExitCodes.FAILURE
|
||||
|
||||
// Step 7. Any actions required before starting up the Corda network layer.
|
||||
attempt { preNetworkRegistration(configuration) }.doOnException(::handleRegistrationError) as? Try.Success
|
||||
?: return ExitCodes.FAILURE
|
||||
if (attempt { preNetworkRegistration(configuration) }.doOnException(::handleRegistrationError) !is Try.Success) return ExitCodes.FAILURE
|
||||
|
||||
// Step 8. Log startup info.
|
||||
logStartupInfo(versionInfo, configuration)
|
||||
|
||||
// Step 9. Start node: create the node, check for other command-line options, add extra logging etc.
|
||||
attempt {
|
||||
cmdLineOptions.baseDirectory.createDirectories()
|
||||
afterNodeInitialisation.run(createNode(configuration, versionInfo))
|
||||
}.doOnException(::handleStartError) as? Try.Success ?: return ExitCodes.FAILURE
|
||||
if (attempt {
|
||||
cmdLineOptions.baseDirectory.createDirectories()
|
||||
afterNodeInitialisation.run(createNode(configuration, versionInfo))
|
||||
}.doOnException(::handleStartError) !is Try.Success) return ExitCodes.FAILURE
|
||||
|
||||
return ExitCodes.SUCCESS
|
||||
}
|
||||
|
@ -109,7 +109,7 @@ class NetworkParametersTest {
|
||||
"com.example.stuff" to key2
|
||||
)
|
||||
)
|
||||
}.withMessage("multiple packages added to the packageOwnership overlap.")
|
||||
}.withMessage("Multiple packages added to the packageOwnership overlap.")
|
||||
|
||||
val params = NetworkParameters(1,
|
||||
emptyList(),
|
||||
|
@ -24,7 +24,7 @@ dependencies {
|
||||
// Unit testing helpers.
|
||||
compile "junit:junit:$junit_version"
|
||||
compile 'org.hamcrest:hamcrest-library:1.3'
|
||||
compile 'com.nhaarman:mockito-kotlin:1.5.0'
|
||||
compile "com.nhaarman:mockito-kotlin:$mockito_kotlin_version"
|
||||
compile "org.mockito:mockito-core:$mockito_version"
|
||||
compile "org.assertj:assertj-core:$assertj_version"
|
||||
compile "com.natpryce:hamkrest:$hamkrest_version"
|
||||
|
@ -3,6 +3,7 @@ package net.corda.testing.core
|
||||
import net.corda.core.internal.JarSignatureCollector
|
||||
import net.corda.core.internal.div
|
||||
import net.corda.nodeapi.internal.crypto.loadKeyStore
|
||||
import net.corda.testing.core.JarSignatureTestUtils.signJar
|
||||
import java.io.FileInputStream
|
||||
import java.nio.file.Path
|
||||
import java.nio.file.Paths
|
||||
@ -44,6 +45,11 @@ object JarSignatureTestUtils {
|
||||
return ks.getCertificate(alias).publicKey
|
||||
}
|
||||
|
||||
fun Path.getPublicKey(alias: String, storePassword: String) : PublicKey {
|
||||
val ks = loadKeyStore(this.resolve("_teststore"), storePassword)
|
||||
return ks.getCertificate(alias).publicKey
|
||||
}
|
||||
|
||||
fun Path.getJarSigners(fileName: String) =
|
||||
JarInputStream(FileInputStream((this / fileName).toFile())).use(JarSignatureCollector::collectSigners)
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ description 'Network bootstrapper'
|
||||
dependencies {
|
||||
compile project(':node-api')
|
||||
compile project(':tools:cliutils')
|
||||
compile project(':common-configuration-parsing')
|
||||
compile "org.apache.logging.log4j:log4j-slf4j-impl:$log4j_version"
|
||||
|
||||
testCompile(project(':test-utils')) {
|
||||
@ -14,6 +15,8 @@ dependencies {
|
||||
}
|
||||
|
||||
testCompile(project(':test-cli'))
|
||||
testCompile "com.nhaarman:mockito-kotlin:$mockito_kotlin_version"
|
||||
testCompile "org.mockito:mockito-core:$mockito_version"
|
||||
}
|
||||
|
||||
processResources {
|
||||
|
@ -1,109 +1,96 @@
|
||||
package net.corda.bootstrapper
|
||||
|
||||
import com.typesafe.config.ConfigFactory
|
||||
import com.typesafe.config.ConfigParseOptions
|
||||
import net.corda.cliutils.CordaCliWrapper
|
||||
import net.corda.cliutils.ExitCodes
|
||||
import net.corda.cliutils.printError
|
||||
import net.corda.cliutils.start
|
||||
import net.corda.common.configuration.parsing.internal.Configuration
|
||||
import net.corda.core.internal.PLATFORM_VERSION
|
||||
import net.corda.core.internal.requirePackageValid
|
||||
import net.corda.nodeapi.internal.crypto.loadKeyStore
|
||||
import net.corda.core.internal.exists
|
||||
import net.corda.nodeapi.internal.network.NetworkBootstrapper
|
||||
import picocli.CommandLine
|
||||
import net.corda.nodeapi.internal.network.NetworkBootstrapper.Companion.DEFAULT_MAX_MESSAGE_SIZE
|
||||
import net.corda.nodeapi.internal.network.NetworkBootstrapper.Companion.DEFAULT_MAX_TRANSACTION_SIZE
|
||||
import net.corda.nodeapi.internal.network.NetworkBootstrapperWithOverridableParameters
|
||||
import net.corda.nodeapi.internal.network.NetworkParametersOverrides
|
||||
import picocli.CommandLine.Option
|
||||
import java.io.IOException
|
||||
import java.io.FileNotFoundException
|
||||
import java.nio.file.Path
|
||||
import java.nio.file.Paths
|
||||
import java.security.KeyStoreException
|
||||
import java.security.PublicKey
|
||||
import java.time.Duration
|
||||
|
||||
fun main(args: Array<String>) {
|
||||
NetworkBootstrapperRunner().start(args)
|
||||
}
|
||||
|
||||
class NetworkBootstrapperRunner : CordaCliWrapper("bootstrapper", "Bootstrap a local test Corda network using a set of node configuration files and CorDapp JARs") {
|
||||
@Option(
|
||||
names = ["--dir"],
|
||||
description = [
|
||||
"Root directory containing the node configuration files and CorDapp JARs that will form the test network.",
|
||||
"It may also contain existing node directories."
|
||||
]
|
||||
)
|
||||
class NetworkBootstrapperRunner(private val bootstrapper: NetworkBootstrapperWithOverridableParameters = NetworkBootstrapper()) : CordaCliWrapper("bootstrapper", "Bootstrap a local test Corda network using a set of node configuration files and CorDapp JARs") {
|
||||
@Option(names = ["--dir"],
|
||||
description = [ "Root directory containing the node configuration files and CorDapp JARs that will form the test network.",
|
||||
"It may also contain existing node directories."])
|
||||
var dir: Path = Paths.get(".")
|
||||
|
||||
@Option(names = ["--no-copy"], description = ["""Don't copy the CorDapp JARs into the nodes' "cordapps" directories."""])
|
||||
var noCopy: Boolean = false
|
||||
|
||||
@Option(names = ["--minimum-platform-version"], description = ["The minimumPlatformVersion to use in the network-parameters."])
|
||||
var minimumPlatformVersion = PLATFORM_VERSION
|
||||
@Option(names = ["--minimum-platform-version"], description = ["The minimum platform version to use in the network-parameters. Current default is $PLATFORM_VERSION."])
|
||||
var minimumPlatformVersion: Int? = null
|
||||
|
||||
@Option(names = ["--register-package-owner"],
|
||||
converter = [PackageOwnerConverter::class],
|
||||
description = [
|
||||
"Register owner of Java package namespace in the network-parameters.",
|
||||
"Format: [java-package-namespace;keystore-file;password;alias]",
|
||||
" `java-package-namespace` is case insensitive and cannot be a sub-package of an existing registered namespace",
|
||||
" `keystore-file` refers to the location of key store file containing the signed certificate as generated by the Java 'keytool' tool (see https://docs.oracle.com/javase/8/docs/technotes/tools/windows/keytool.html)",
|
||||
" `password` to open the key store",
|
||||
" `alias` refers to the name associated with a certificate containing the public key to be associated with the package namespace"
|
||||
])
|
||||
var registerPackageOwnership: List<PackageOwner> = mutableListOf()
|
||||
@Option(names = ["--max-message-size"], description = ["The maximum message size to use in the network-parameters, in bytes. Current default is $DEFAULT_MAX_MESSAGE_SIZE."])
|
||||
var maxMessageSize: Int? = null
|
||||
|
||||
@Option(names = ["--unregister-package-owner"],
|
||||
description = [
|
||||
"Unregister owner of Java package namespace in the network-parameters.",
|
||||
"Format: [java-package-namespace]",
|
||||
" `java-package-namespace` is case insensitive and cannot be a sub-package of an existing registered namespace"
|
||||
])
|
||||
var unregisterPackageOwnership: List<String> = mutableListOf()
|
||||
@Option(names = ["--max-transaction-size"], description = ["The maximum transaction size to use in the network-parameters, in bytes. Current default is $DEFAULT_MAX_TRANSACTION_SIZE."])
|
||||
var maxTransactionSize: Int? = null
|
||||
|
||||
@Option(names = ["--event-horizon"], description = ["The event horizon to use in the network-parameters. Default is 30 days."])
|
||||
var eventHorizon: Duration? = null
|
||||
|
||||
@Option(names = ["--network-parameter-overrides", "-n"], description = ["Overrides the default network parameters with those in the given file."])
|
||||
var networkParametersFile: Path? = null
|
||||
|
||||
|
||||
private fun verifyInputs() {
|
||||
require(minimumPlatformVersion == null || minimumPlatformVersion ?: 0 > 0) { "The --minimum-platform-version parameter must be at least 1" }
|
||||
require(eventHorizon == null || eventHorizon?.isNegative == false) { "The --event-horizon parameter must be a positive value" }
|
||||
require(maxTransactionSize == null || maxTransactionSize ?: 0 > 0) { "The --max-transaction-size parameter must be at least 1" }
|
||||
require(maxMessageSize == null || maxMessageSize ?: 0 > 0) { "The --max-message-size parameter must be at least 1" }
|
||||
}
|
||||
|
||||
private fun commandLineOverrides(): Map<String, Any> {
|
||||
val overrides = mutableMapOf<String, Any>()
|
||||
overrides += minimumPlatformVersion?.let { mapOf("minimumPlatformVersion" to minimumPlatformVersion!!) } ?: mutableMapOf()
|
||||
overrides += maxMessageSize?.let { mapOf("maxMessageSize" to maxMessageSize!!) } ?: emptyMap()
|
||||
overrides += maxTransactionSize?.let { mapOf("maxTransactionSize" to maxTransactionSize!!) } ?: emptyMap()
|
||||
overrides += eventHorizon?.let { mapOf("eventHorizon" to eventHorizon!!) } ?: emptyMap()
|
||||
return overrides
|
||||
}
|
||||
|
||||
private fun getNetworkParametersOverrides(): Valid<NetworkParametersOverrides> {
|
||||
val parseOptions = ConfigParseOptions.defaults()
|
||||
val config = if (networkParametersFile == null) {
|
||||
ConfigFactory.empty()
|
||||
} else {
|
||||
if (networkParametersFile?.exists() != true) throw FileNotFoundException("Unable to find specified network parameters config file at $networkParametersFile")
|
||||
ConfigFactory.parseFile(networkParametersFile!!.toFile(), parseOptions)
|
||||
}
|
||||
val finalConfig = ConfigFactory.parseMap(commandLineOverrides()).withFallback(config).resolve()
|
||||
return finalConfig.parseAsNetworkParametersConfiguration()
|
||||
}
|
||||
|
||||
private fun <T> Collection<T>.pluralise() = if (this.count() > 1) "s" else ""
|
||||
|
||||
private fun reportErrors(errors: Set<Configuration.Validation.Error>) {
|
||||
printError("Error${errors.pluralise()} found parsing the network parameter overrides file at $networkParametersFile:")
|
||||
errors.forEach { printError("Error parsing ${it.pathAsString}: ${it.message}") }
|
||||
}
|
||||
|
||||
override fun runProgram(): Int {
|
||||
NetworkBootstrapper().bootstrap(dir.toAbsolutePath().normalize(),
|
||||
verifyInputs()
|
||||
val networkParameterOverrides = getNetworkParametersOverrides().doOnErrors(::reportErrors).optional ?: return ExitCodes.FAILURE
|
||||
bootstrapper.bootstrap(dir.toAbsolutePath().normalize(),
|
||||
copyCordapps = !noCopy,
|
||||
minimumPlatformVersion = minimumPlatformVersion,
|
||||
packageOwnership = registerPackageOwnership.map { Pair(it.javaPackageName, it.publicKey) }.toMap()
|
||||
.plus(unregisterPackageOwnership.map { Pair(it, null) })
|
||||
networkParameterOverrides = networkParameterOverrides
|
||||
)
|
||||
return 0 //exit code
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
data class PackageOwner(val javaPackageName: String, val publicKey: PublicKey)
|
||||
|
||||
/**
|
||||
* Converter from String to PackageOwner (String and PublicKey)
|
||||
*/
|
||||
class PackageOwnerConverter : CommandLine.ITypeConverter<PackageOwner> {
|
||||
override fun convert(packageOwner: String): PackageOwner {
|
||||
if (!packageOwner.isBlank()) {
|
||||
val packageOwnerSpec = packageOwner.split(";")
|
||||
if (packageOwnerSpec.size < 4)
|
||||
throw IllegalArgumentException("Package owner must specify 4 elements separated by semi-colon: 'java-package-namespace;keyStorePath;keyStorePassword;alias'")
|
||||
|
||||
// java package name validation
|
||||
val javaPackageName = packageOwnerSpec[0]
|
||||
requirePackageValid(javaPackageName)
|
||||
|
||||
// cater for passwords that include the argument delimiter field
|
||||
val keyStorePassword =
|
||||
if (packageOwnerSpec.size > 4)
|
||||
packageOwnerSpec.subList(2, packageOwnerSpec.size-1).joinToString(";")
|
||||
else packageOwnerSpec[2]
|
||||
try {
|
||||
val ks = loadKeyStore(Paths.get(packageOwnerSpec[1]), keyStorePassword)
|
||||
try {
|
||||
val publicKey = ks.getCertificate(packageOwnerSpec[packageOwnerSpec.size-1]).publicKey
|
||||
return PackageOwner(javaPackageName,publicKey)
|
||||
}
|
||||
catch(kse: KeyStoreException) {
|
||||
throw IllegalArgumentException("Keystore has not been initialized for alias ${packageOwnerSpec[3]}")
|
||||
}
|
||||
}
|
||||
catch(kse: KeyStoreException) {
|
||||
throw IllegalArgumentException("Password is incorrect or the key store is damaged for keyStoreFilePath: ${packageOwnerSpec[1]} and keyStorePassword: $keyStorePassword")
|
||||
}
|
||||
catch(e: IOException) {
|
||||
throw IllegalArgumentException("Error reading the key store from the file for keyStoreFilePath: ${packageOwnerSpec[1]} and keyStorePassword: $keyStorePassword")
|
||||
}
|
||||
}
|
||||
else throw IllegalArgumentException("Must specify package owner argument: 'java-package-namespace;keyStorePath;keyStorePassword;alias'")
|
||||
return ExitCodes.SUCCESS
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,103 @@
|
||||
package net.corda.bootstrapper
|
||||
|
||||
import com.typesafe.config.Config
|
||||
import net.corda.common.configuration.parsing.internal.Configuration
|
||||
import net.corda.common.configuration.parsing.internal.get
|
||||
import net.corda.common.configuration.parsing.internal.mapValid
|
||||
import net.corda.common.configuration.parsing.internal.nested
|
||||
import net.corda.common.validation.internal.Validated
|
||||
import net.corda.core.internal.div
|
||||
import net.corda.core.internal.requirePackageValid
|
||||
import net.corda.core.node.NetworkParameters
|
||||
import net.corda.nodeapi.internal.crypto.loadKeyStore
|
||||
import net.corda.nodeapi.internal.network.NetworkParametersOverrides
|
||||
import net.corda.nodeapi.internal.network.PackageOwner
|
||||
import java.io.IOException
|
||||
import java.nio.file.InvalidPathException
|
||||
import java.nio.file.Path
|
||||
import java.nio.file.Paths
|
||||
import java.security.KeyStoreException
|
||||
|
||||
internal typealias Valid<TARGET> = Validated<TARGET, Configuration.Validation.Error>
|
||||
|
||||
fun Config.parseAsNetworkParametersConfiguration(options: Configuration.Validation.Options = Configuration.Validation.Options(strict = false)):
|
||||
Valid<NetworkParametersOverrides> = NetworkParameterOverridesSpec.parse(this, options)
|
||||
|
||||
internal fun <T> badValue(msg: String): Valid<T> = Validated.invalid(sequenceOf(Configuration.Validation.Error.BadValue.of(msg)).toSet())
|
||||
internal fun <T> valid(value: T): Valid<T> = Validated.valid(value)
|
||||
|
||||
internal object NetworkParameterOverridesSpec : Configuration.Specification<NetworkParametersOverrides>("DefaultNetworkParameters") {
|
||||
private val minimumPlatformVersion by int().mapValid(::parsePositiveInteger).optional()
|
||||
private val maxMessageSize by int().mapValid(::parsePositiveInteger).optional()
|
||||
private val maxTransactionSize by int().mapValid(::parsePositiveInteger).optional()
|
||||
private val packageOwnership by nested(PackageOwnershipSpec).list().optional()
|
||||
private val eventHorizon by duration().optional()
|
||||
|
||||
internal object PackageOwnershipSpec : Configuration.Specification<PackageOwner>("PackageOwners") {
|
||||
private val packageName by string().mapValid(::toPackageName)
|
||||
private val keystore by string().mapValid(::toPath)
|
||||
private val keystorePassword by string()
|
||||
private val keystoreAlias by string()
|
||||
|
||||
override fun parseValid(configuration: Config): Validated<PackageOwner, Configuration.Validation.Error> {
|
||||
val suppliedKeystorePath = configuration[keystore]
|
||||
val keystorePassword = configuration[keystorePassword]
|
||||
return try {
|
||||
val javaPackageName = configuration[packageName]
|
||||
val absoluteKeystorePath = if (suppliedKeystorePath.isAbsolute) {
|
||||
suppliedKeystorePath
|
||||
} else {
|
||||
//If a relative path is supplied, make it relative to the location of the config file
|
||||
Paths.get(configuration.origin().filename()).resolveSibling(suppliedKeystorePath.toString())
|
||||
}.toAbsolutePath()
|
||||
val ks = loadKeyStore(absoluteKeystorePath, keystorePassword)
|
||||
return try {
|
||||
val publicKey = ks.getCertificate(configuration[keystoreAlias]).publicKey
|
||||
valid(PackageOwner(javaPackageName, publicKey))
|
||||
} catch (kse: KeyStoreException) {
|
||||
badValue("Keystore has not been initialized for alias ${configuration[keystoreAlias]}")
|
||||
}
|
||||
} catch (kse: KeyStoreException) {
|
||||
badValue("Password is incorrect or the key store is damaged for keyStoreFilePath: $suppliedKeystorePath and keyStorePassword: $keystorePassword")
|
||||
} catch (e: IOException) {
|
||||
badValue("Error reading the key store from the file for keyStoreFilePath: $suppliedKeystorePath and keyStorePassword: $keystorePassword ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
private fun toPackageName(rawValue: String): Validated<String, Configuration.Validation.Error> {
|
||||
return try {
|
||||
requirePackageValid(rawValue)
|
||||
valid(rawValue)
|
||||
} catch (e: Exception) {
|
||||
return badValue(e.message ?: e.toString())
|
||||
}
|
||||
}
|
||||
|
||||
private fun toPath(rawValue: String): Validated<Path, Configuration.Validation.Error> {
|
||||
return try {
|
||||
valid(Paths.get(rawValue))
|
||||
} catch (e: InvalidPathException) {
|
||||
return badValue("Path $rawValue not found")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun parseValid(configuration: Config): Valid<NetworkParametersOverrides> {
|
||||
val packageOwnership = configuration[packageOwnership]
|
||||
if (packageOwnership != null && !NetworkParameters.noOverlap(packageOwnership.map { it.javaPackageName })) {
|
||||
return Validated.invalid(sequenceOf(Configuration.Validation.Error.BadValue.of("Package namespaces must not overlap", keyName = "packageOwnership", containingPath = listOf())).toSet())
|
||||
}
|
||||
return valid(NetworkParametersOverrides(
|
||||
minimumPlatformVersion = configuration[minimumPlatformVersion],
|
||||
maxMessageSize = configuration[maxMessageSize],
|
||||
maxTransactionSize = configuration[maxTransactionSize],
|
||||
packageOwnership = packageOwnership,
|
||||
eventHorizon = configuration[eventHorizon]
|
||||
))
|
||||
}
|
||||
|
||||
private fun parsePositiveInteger(rawValue: Int): Valid<Int> {
|
||||
if (rawValue > 0) return valid(rawValue)
|
||||
return badValue("The value must be at least 1")
|
||||
}
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
package net.corda.bootstrapper
|
||||
|
||||
import net.corda.testing.CliBackwardsCompatibleTest
|
||||
|
||||
class NetworkBootstrapperBackwardsCompatibilityTest : CliBackwardsCompatibleTest(NetworkBootstrapperRunner::class.java)
|
@ -1,5 +0,0 @@
|
||||
package net.corda.bootstrapper
|
||||
|
||||
import net.corda.testing.CliBackwardsCompatibleTest
|
||||
|
||||
class NetworkBootstrapperRunnerTest : CliBackwardsCompatibleTest(NetworkBootstrapperRunner::class.java)
|
@ -0,0 +1,255 @@
|
||||
package net.corda.bootstrapper
|
||||
|
||||
import com.nhaarman.mockito_kotlin.mock
|
||||
import com.nhaarman.mockito_kotlin.verify
|
||||
import net.corda.core.internal.copyTo
|
||||
import net.corda.core.internal.deleteRecursively
|
||||
import net.corda.core.internal.div
|
||||
import net.corda.core.utilities.days
|
||||
import net.corda.nodeapi.internal.network.NetworkBootstrapperWithOverridableParameters
|
||||
import net.corda.nodeapi.internal.network.NetworkParametersOverrides
|
||||
import net.corda.nodeapi.internal.network.PackageOwner
|
||||
import net.corda.testing.core.ALICE_NAME
|
||||
import net.corda.testing.core.BOB_NAME
|
||||
import net.corda.testing.core.CHARLIE_NAME
|
||||
import net.corda.testing.core.JarSignatureTestUtils.generateKey
|
||||
import net.corda.testing.core.JarSignatureTestUtils.getPublicKey
|
||||
import org.junit.*
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.FileNotFoundException
|
||||
import java.io.PrintStream
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
import java.nio.file.Paths
|
||||
import java.security.PublicKey
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFailsWith
|
||||
|
||||
class NetworkBootstrapperRunnerTests {
|
||||
private val outContent = ByteArrayOutputStream()
|
||||
private val errContent = ByteArrayOutputStream()
|
||||
private val originalOut = System.out
|
||||
private val originalErr = System.err
|
||||
|
||||
@Before
|
||||
fun setUpStreams() {
|
||||
System.setOut(PrintStream(outContent))
|
||||
System.setErr(PrintStream(errContent))
|
||||
}
|
||||
|
||||
@After
|
||||
fun restoreStreams() {
|
||||
System.setOut(originalOut)
|
||||
System.setErr(originalErr)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val ALICE = "alice"
|
||||
private const val ALICE_PASS = "alicepass"
|
||||
|
||||
private const val aliceConfigFile = "alice-network.conf"
|
||||
private const val correctNetworkFile = "correct-network.conf"
|
||||
private const val packageOverlapConfigFile = "package-overlap.conf"
|
||||
|
||||
private val dirAlice = Files.createTempDirectory(ALICE)
|
||||
private val dirAliceEC = Files.createTempDirectory("sdfsdfds")
|
||||
private val dirAliceDSA = Files.createTempDirectory(ALICE)
|
||||
|
||||
private lateinit var alicePublicKey: PublicKey
|
||||
private lateinit var alicePublicKeyEC: PublicKey
|
||||
private lateinit var alicePublicKeyDSA: PublicKey
|
||||
|
||||
private val resourceDirectory = Paths.get(".") / "src" / "test" / "resources"
|
||||
|
||||
private fun String.copyToTestDir(dir: Path = dirAlice): Path {
|
||||
return (resourceDirectory / this).copyTo(dir / this)
|
||||
}
|
||||
|
||||
@BeforeClass
|
||||
@JvmStatic
|
||||
fun beforeClass() {
|
||||
dirAlice.generateKey(ALICE, ALICE_PASS, ALICE_NAME.toString())
|
||||
dirAliceEC.generateKey(ALICE, ALICE_PASS, ALICE_NAME.toString(), "EC")
|
||||
dirAliceDSA.generateKey(ALICE, ALICE_PASS, ALICE_NAME.toString(), "DSA")
|
||||
alicePublicKey = dirAlice.getPublicKey(ALICE, ALICE_PASS)
|
||||
alicePublicKeyEC = dirAliceEC.getPublicKey(ALICE, ALICE_PASS)
|
||||
alicePublicKeyDSA = dirAliceDSA.getPublicKey(ALICE, ALICE_PASS)
|
||||
}
|
||||
|
||||
@AfterClass
|
||||
@JvmStatic
|
||||
fun afterClass() {
|
||||
dirAlice.deleteRecursively()
|
||||
}
|
||||
}
|
||||
|
||||
private fun getRunner(): Pair<NetworkBootstrapperRunner, NetworkBootstrapperWithOverridableParameters> {
|
||||
val mockBootstrapper = mock<NetworkBootstrapperWithOverridableParameters>()
|
||||
return Pair(NetworkBootstrapperRunner(mockBootstrapper), mockBootstrapper)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test when defaults are run bootstrapper is called correctly`() {
|
||||
val (runner, mockBootstrapper) = getRunner()
|
||||
val exitCode = runner.runProgram()
|
||||
verify(mockBootstrapper).bootstrap(Paths.get(".").toAbsolutePath().normalize(), true, NetworkParametersOverrides())
|
||||
assertEquals(0, exitCode)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test when base directory is specified it is passed through to the bootstrapper`() {
|
||||
val (runner, mockBootstrapper) = getRunner()
|
||||
val tempDir = createTempDir()
|
||||
runner.dir = tempDir.toPath()
|
||||
val exitCode = runner.runProgram()
|
||||
verify(mockBootstrapper).bootstrap(tempDir.toPath().toAbsolutePath().normalize(), true, NetworkParametersOverrides())
|
||||
assertEquals(0, exitCode)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test when copy cordapps is specified it is passed through to the bootstrapper`() {
|
||||
val (runner, mockBootstrapper) = getRunner()
|
||||
runner.noCopy = true
|
||||
val exitCode = runner.runProgram()
|
||||
verify(mockBootstrapper).bootstrap(Paths.get(".").toAbsolutePath().normalize(), false, NetworkParametersOverrides())
|
||||
assertEquals(0, exitCode)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test when min platform version is specified it is passed through to the bootstrapper`() {
|
||||
val (runner, mockBootstrapper) = getRunner()
|
||||
runner.minimumPlatformVersion = 1
|
||||
val exitCode = runner.runProgram()
|
||||
verify(mockBootstrapper).bootstrap(Paths.get(".").toAbsolutePath().normalize(), true, NetworkParametersOverrides(minimumPlatformVersion = 1))
|
||||
assertEquals(0, exitCode)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test when min platform version is invalid it fails to run with a sensible error message`() {
|
||||
val runner = getRunner().first
|
||||
runner.minimumPlatformVersion = 0
|
||||
val exception = assertFailsWith<IllegalArgumentException> { runner.runProgram() }
|
||||
assertEquals("The --minimum-platform-version parameter must be at least 1", exception.message)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test when max message size is specified it is passed through to the bootstrapper`() {
|
||||
val (runner, mockBootstrapper) = getRunner()
|
||||
runner.maxMessageSize = 1
|
||||
val exitCode = runner.runProgram()
|
||||
verify(mockBootstrapper).bootstrap(Paths.get(".").toAbsolutePath().normalize(), true, NetworkParametersOverrides(maxMessageSize = 1))
|
||||
assertEquals(0, exitCode)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test when max message size is invalid it fails to run with a sensible error message`() {
|
||||
val runner = getRunner().first
|
||||
runner.maxMessageSize = 0
|
||||
val exception = assertFailsWith<IllegalArgumentException> { runner.runProgram() }
|
||||
assertEquals("The --max-message-size parameter must be at least 1", exception.message)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test when max transaction size is specified it is passed through to the bootstrapper`() {
|
||||
val (runner, mockBootstrapper) = getRunner()
|
||||
runner.maxTransactionSize = 1
|
||||
val exitCode = runner.runProgram()
|
||||
verify(mockBootstrapper).bootstrap(Paths.get(".").toAbsolutePath().normalize(), true, NetworkParametersOverrides(maxTransactionSize = 1))
|
||||
assertEquals(0, exitCode)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test when max transaction size is invalid it fails to run with a sensible error message`() {
|
||||
val runner = getRunner().first
|
||||
runner.maxTransactionSize = 0
|
||||
val exception = assertFailsWith<IllegalArgumentException> { runner.runProgram() }
|
||||
assertEquals("The --max-transaction-size parameter must be at least 1", exception.message)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test when event horizon is specified it is passed through to the bootstrapper`() {
|
||||
val (runner, mockBootstrapper) = getRunner()
|
||||
runner.eventHorizon = 7.days
|
||||
val exitCode = runner.runProgram()
|
||||
verify(mockBootstrapper).bootstrap(Paths.get(".").toAbsolutePath().normalize(), true, NetworkParametersOverrides(eventHorizon = 7.days))
|
||||
assertEquals(0, exitCode)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test when event horizon is invalid it fails to run with a sensible error message`() {
|
||||
val runner = getRunner().first
|
||||
runner.eventHorizon = (-7).days
|
||||
val exception = assertFailsWith<IllegalArgumentException> { runner.runProgram() }
|
||||
assertEquals("The --event-horizon parameter must be a positive value", exception.message)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test when a network parameters is specified the values are passed through to the bootstrapper`() {
|
||||
val (runner, mockBootstrapper) = getRunner()
|
||||
val conf = correctNetworkFile.copyToTestDir()
|
||||
runner.networkParametersFile = conf
|
||||
val exitCode = runner.runProgram()
|
||||
verify(mockBootstrapper).bootstrap(Paths.get(".").toAbsolutePath().normalize(), true, NetworkParametersOverrides(
|
||||
maxMessageSize = 10000,
|
||||
maxTransactionSize = 2000,
|
||||
eventHorizon = 5.days,
|
||||
minimumPlatformVersion = 2
|
||||
))
|
||||
assertEquals(0, exitCode)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test when a package is specified in the network parameters file it is passed through to the bootstrapper`() {
|
||||
val (runner, mockBootstrapper) = getRunner()
|
||||
val conf = aliceConfigFile.copyToTestDir()
|
||||
runner.networkParametersFile = conf
|
||||
val exitCode = runner.runProgram()
|
||||
verify(mockBootstrapper).bootstrap(Paths.get(".").toAbsolutePath().normalize(), true, NetworkParametersOverrides(
|
||||
packageOwnership = listOf(PackageOwner("com.example.stuff", publicKey = alicePublicKey))
|
||||
))
|
||||
assertEquals(0, exitCode)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test when a package is specified in the network parameters file it is passed through to the bootstrapper EC`() {
|
||||
val (runner, mockBootstrapper) = getRunner()
|
||||
val conf = aliceConfigFile.copyToTestDir(dirAliceEC)
|
||||
runner.networkParametersFile = conf
|
||||
val exitCode = runner.runProgram()
|
||||
verify(mockBootstrapper).bootstrap(Paths.get(".").toAbsolutePath().normalize(), true, NetworkParametersOverrides(
|
||||
packageOwnership = listOf(PackageOwner("com.example.stuff", publicKey = alicePublicKeyEC))
|
||||
))
|
||||
assertEquals(0, exitCode)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test when a package is specified in the network parameters file it is passed through to the bootstrapper DSA`() {
|
||||
val (runner, mockBootstrapper) = getRunner()
|
||||
val conf = aliceConfigFile.copyToTestDir(dirAliceDSA)
|
||||
runner.networkParametersFile = conf
|
||||
val exitCode = runner.runProgram()
|
||||
verify(mockBootstrapper).bootstrap(Paths.get(".").toAbsolutePath().normalize(), true, NetworkParametersOverrides(
|
||||
packageOwnership = listOf(PackageOwner("com.example.stuff", publicKey = alicePublicKeyDSA))
|
||||
))
|
||||
assertEquals(0, exitCode)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test when packages overlap that the bootstrapper fails with a sensible message`() {
|
||||
val (runner, mockBootstrapper) = getRunner()
|
||||
val conf = packageOverlapConfigFile.copyToTestDir()
|
||||
runner.networkParametersFile = conf
|
||||
val exitCode = runner.runProgram()
|
||||
val output = errContent.toString()
|
||||
assert(output.contains("Error parsing packageOwnership: Package namespaces must not overlap"))
|
||||
assertEquals(1, exitCode)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test when keyfile does not exist then bootstrapper fails with a sensible message`() {
|
||||
val (runner, mockBootstrapper) = getRunner()
|
||||
runner.networkParametersFile = dirAlice / "filename-that-doesnt-exist"
|
||||
val exception = assertFailsWith<FileNotFoundException> { runner.runProgram() }
|
||||
assert(exception.message!!.startsWith("Unable to find specified network parameters config file at"))
|
||||
}
|
||||
}
|
@ -1,164 +0,0 @@
|
||||
package net.corda.bootstrapper
|
||||
|
||||
import net.corda.core.internal.deleteRecursively
|
||||
import net.corda.core.internal.div
|
||||
import net.corda.testing.core.ALICE_NAME
|
||||
import net.corda.testing.core.BOB_NAME
|
||||
import net.corda.testing.core.CHARLIE_NAME
|
||||
import net.corda.testing.core.JarSignatureTestUtils.generateKey
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.junit.*
|
||||
import org.junit.rules.ExpectedException
|
||||
import picocli.CommandLine
|
||||
import java.nio.file.Files
|
||||
|
||||
class PackageOwnerParsingTest {
|
||||
|
||||
@Rule
|
||||
@JvmField
|
||||
val expectedEx: ExpectedException = ExpectedException.none()
|
||||
|
||||
companion object {
|
||||
|
||||
private const val ALICE = "alice"
|
||||
private const val ALICE_PASS = "alicepass"
|
||||
private const val BOB = "bob"
|
||||
private const val BOB_PASS = "bobpass"
|
||||
private const val CHARLIE = "charlie"
|
||||
private const val CHARLIE_PASS = "charliepass"
|
||||
|
||||
private val dirAlice = Files.createTempDirectory(ALICE)
|
||||
private val dirBob = Files.createTempDirectory(BOB)
|
||||
private val dirCharlie = Files.createTempDirectory(CHARLIE)
|
||||
|
||||
val networkBootstrapper = NetworkBootstrapperRunner()
|
||||
val commandLine = CommandLine(networkBootstrapper)
|
||||
|
||||
@BeforeClass
|
||||
@JvmStatic
|
||||
fun beforeClass() {
|
||||
dirAlice.generateKey(ALICE, ALICE_PASS, ALICE_NAME.toString())
|
||||
dirBob.generateKey(BOB, BOB_PASS, BOB_NAME.toString(), "EC")
|
||||
dirCharlie.generateKey(CHARLIE, CHARLIE_PASS, CHARLIE_NAME.toString(), "DSA")
|
||||
}
|
||||
|
||||
@AfterClass
|
||||
@JvmStatic
|
||||
fun afterClass() {
|
||||
dirAlice.deleteRecursively()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `parse registration request with single mapping`() {
|
||||
val aliceKeyStorePath = dirAlice / "_teststore"
|
||||
val args = arrayOf("--register-package-owner", "com.example.stuff;$aliceKeyStorePath;$ALICE_PASS;$ALICE")
|
||||
commandLine.parse(*args)
|
||||
assertThat(networkBootstrapper.registerPackageOwnership[0].javaPackageName).isEqualTo("com.example.stuff")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `parse registration request with invalid arguments`() {
|
||||
val args = arrayOf("--register-package-owner", "com.!example.stuff")
|
||||
expectedEx.expect(CommandLine.ParameterException::class.java)
|
||||
expectedEx.expectMessage("Package owner must specify 4 elements separated by semi-colon")
|
||||
commandLine.parse(*args)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `parse registration request with incorrect keystore specification`() {
|
||||
val aliceKeyStorePath = dirAlice / "_teststore"
|
||||
val args = arrayOf("--register-package-owner", "com.example.stuff;$aliceKeyStorePath$ALICE_PASS")
|
||||
expectedEx.expect(CommandLine.ParameterException::class.java)
|
||||
expectedEx.expectMessage("Package owner must specify 4 elements separated by semi-colon")
|
||||
commandLine.parse(*args)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `parse registration request with invalid java package name`() {
|
||||
val args = arrayOf("--register-package-owner", "com.!example.stuff;A;B;C")
|
||||
expectedEx.expect(CommandLine.ParameterException::class.java)
|
||||
expectedEx.expectMessage("Invalid Java package name")
|
||||
commandLine.parse(*args)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `parse registration request with invalid keystore file`() {
|
||||
val args = arrayOf("--register-package-owner", "com.example.stuff;NONSENSE;B;C")
|
||||
expectedEx.expect(CommandLine.ParameterException::class.java)
|
||||
expectedEx.expectMessage("Error reading the key store from the file")
|
||||
commandLine.parse(*args)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `parse registration request with invalid keystore password`() {
|
||||
val aliceKeyStorePath = dirAlice / "_teststore"
|
||||
val args = arrayOf("--register-package-owner", "com.example.stuff;$aliceKeyStorePath;BAD_PASSWORD;$ALICE")
|
||||
expectedEx.expect(CommandLine.ParameterException::class.java)
|
||||
expectedEx.expectMessage("Error reading the key store from the file")
|
||||
commandLine.parse(*args)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `parse registration request with invalid keystore alias`() {
|
||||
val aliceKeyStorePath = dirAlice / "_teststore"
|
||||
val args = arrayOf("--register-package-owner", "com.example.stuff;$aliceKeyStorePath;$ALICE_PASS;BAD_ALIAS")
|
||||
expectedEx.expect(CommandLine.ParameterException::class.java)
|
||||
expectedEx.expectMessage("must not be null")
|
||||
commandLine.parse(*args)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `parse registration request with multiple arguments`() {
|
||||
val aliceKeyStorePath = dirAlice / "_teststore"
|
||||
val bobKeyStorePath = dirBob / "_teststore"
|
||||
val charlieKeyStorePath = dirCharlie / "_teststore"
|
||||
val args = arrayOf("--register-package-owner", "com.example.stuff;$aliceKeyStorePath;$ALICE_PASS;$ALICE",
|
||||
"--register-package-owner", "com.example.more.stuff;$bobKeyStorePath;$BOB_PASS;$BOB",
|
||||
"--register-package-owner", "com.example.even.more.stuff;$charlieKeyStorePath;$CHARLIE_PASS;$CHARLIE")
|
||||
commandLine.parse(*args)
|
||||
assertThat(networkBootstrapper.registerPackageOwnership).hasSize(3)
|
||||
}
|
||||
|
||||
@Ignore("Ignoring this test as the delimiters don't work correctly, see CORDA-2191")
|
||||
@Test
|
||||
fun `parse registration request with delimiter inclusive passwords`() {
|
||||
val aliceKeyStorePath1 = dirAlice / "_alicestore1"
|
||||
dirAlice.generateKey("${ALICE}1", "passw;rd", ALICE_NAME.toString(), storeName = "_alicestore1")
|
||||
val aliceKeyStorePath2 = dirAlice / "_alicestore2"
|
||||
dirAlice.generateKey("${ALICE}2", "\"passw;rd\"", ALICE_NAME.toString(), storeName = "_alicestore2")
|
||||
val aliceKeyStorePath3 = dirAlice / "_alicestore3"
|
||||
dirAlice.generateKey("${ALICE}3", "passw;rd", ALICE_NAME.toString(), storeName = "_alicestore3")
|
||||
val aliceKeyStorePath4 = dirAlice / "_alicestore4"
|
||||
dirAlice.generateKey("${ALICE}4", "\'passw;rd\'", ALICE_NAME.toString(), storeName = "_alicestore4")
|
||||
val aliceKeyStorePath5 = dirAlice / "_alicestore5"
|
||||
dirAlice.generateKey("${ALICE}5", "\"\"passw;rd\"\"", ALICE_NAME.toString(), storeName = "_alicestore5")
|
||||
val packageOwnerSpecs = listOf("net.something0;$aliceKeyStorePath1;passw;rd;${ALICE}1",
|
||||
"net.something1;$aliceKeyStorePath2;\"passw;rd\";${ALICE}2",
|
||||
"\"net.something2;$aliceKeyStorePath3;passw;rd;${ALICE}3\"",
|
||||
"net.something3;$aliceKeyStorePath4;\'passw;rd\';${ALICE}4",
|
||||
"net.something4;$aliceKeyStorePath5;\"\"passw;rd\"\";${ALICE}5")
|
||||
packageOwnerSpecs.forEachIndexed { i, packageOwnerSpec ->
|
||||
commandLine.parse(*arrayOf("--register-package-owner", packageOwnerSpec))
|
||||
assertThat(networkBootstrapper.registerPackageOwnership[0].javaPackageName).isEqualTo("net.something$i")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `parse unregister request with single mapping`() {
|
||||
val args = arrayOf("--unregister-package-owner", "com.example.stuff")
|
||||
commandLine.parse(*args)
|
||||
assertThat(networkBootstrapper.unregisterPackageOwnership).contains("com.example.stuff")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `parse mixed register and unregister request`() {
|
||||
val aliceKeyStorePath = dirAlice / "_teststore"
|
||||
val args = arrayOf("--register-package-owner", "com.example.stuff;$aliceKeyStorePath;$ALICE_PASS;$ALICE",
|
||||
"--unregister-package-owner", "com.example.stuff2")
|
||||
commandLine.parse(*args)
|
||||
assertThat(networkBootstrapper.registerPackageOwnership.map { it.javaPackageName }).contains("com.example.stuff")
|
||||
assertThat(networkBootstrapper.unregisterPackageOwnership).contains("com.example.stuff2")
|
||||
}
|
||||
}
|
||||
|
8
tools/bootstrapper/src/test/resources/alice-network.conf
Normal file
8
tools/bootstrapper/src/test/resources/alice-network.conf
Normal file
@ -0,0 +1,8 @@
|
||||
packageOwnership=[
|
||||
{
|
||||
packageName="com.example.stuff"
|
||||
keystore="_teststore"
|
||||
keystorePassword="alicepass"
|
||||
keystoreAlias="alice"
|
||||
}
|
||||
]
|
@ -0,0 +1,4 @@
|
||||
minimumPlatformVersion=2
|
||||
maxMessageSize=10000
|
||||
maxTransactionSize=2000
|
||||
eventHorizon="5 days"
|
14
tools/bootstrapper/src/test/resources/package-overlap.conf
Normal file
14
tools/bootstrapper/src/test/resources/package-overlap.conf
Normal file
@ -0,0 +1,14 @@
|
||||
packageOwnership=[
|
||||
{
|
||||
packageName="com.example"
|
||||
keystore="_teststore"
|
||||
keystorePassword="alicepass"
|
||||
keystoreAlias="alice"
|
||||
}
|
||||
{
|
||||
packageName="com.example.overlap"
|
||||
keystore="_teststore"
|
||||
keystorePassword="alicepass"
|
||||
keystoreAlias="alice"
|
||||
}
|
||||
]
|
@ -53,6 +53,7 @@ object CordaSystemUtils {
|
||||
|
||||
object ShellConstants {
|
||||
const val RED = "\u001B[31m"
|
||||
const val YELLOW = "\u001B[33m"
|
||||
const val RESET = "\u001B[0m"
|
||||
}
|
||||
|
||||
@ -86,8 +87,8 @@ fun CordaCliWrapper.start(args: Array<String>) {
|
||||
if (this.verbose || this.subCommands().any { it.verbose }) {
|
||||
throwable.printStackTrace()
|
||||
} else {
|
||||
System.err.println("*ERROR*: ${throwable.rootMessage ?: "Use --verbose for more details"}")
|
||||
}
|
||||
printError(throwable.rootMessage ?: "Use --verbose for more details")
|
||||
exitProcess(ExitCodes.FAILURE)
|
||||
}
|
||||
}
|
||||
@ -185,11 +186,12 @@ abstract class CordaCliWrapper(alias: String, description: String) : CliWrapperB
|
||||
|
||||
fun printHelp() = cmd.usage(System.out)
|
||||
|
||||
fun printlnErr(message: String) = System.err.println(message)
|
||||
|
||||
fun printlnWarn(message: String) = System.err.println(message)
|
||||
}
|
||||
|
||||
fun printWarning(message: String) = System.err.println("${ShellConstants.YELLOW}$message${ShellConstants.RESET}")
|
||||
fun printError(message: String) = System.err.println("${ShellConstants.RED}$message${ShellConstants.RESET}")
|
||||
|
||||
|
||||
/**
|
||||
* Useful commonly used constants applicable to many CLI tools
|
||||
*/
|
||||
|
@ -94,7 +94,7 @@ private class ShellExtensionsGenerator(val parent: CordaCliWrapper) {
|
||||
val semanticParts = declaredBashVersion().split(".")
|
||||
semanticParts.firstOrNull()?.toIntOrNull()?.let { major ->
|
||||
if (major < minSupportedBashVersion) {
|
||||
parent.printlnWarn("Cannot install shell extension for bash major version earlier than $minSupportedBashVersion. Please upgrade your bash version. Aliases should still work.")
|
||||
printWarning("Cannot install shell extension for bash major version earlier than $minSupportedBashVersion. Please upgrade your bash version. Aliases should still work.")
|
||||
generateAutoCompleteFile = false
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user