CORDA-3068: Pass base directory when resolving relative paths (#5297)

This commit is contained in:
Viktor Kolomeyko 2019-07-15 10:51:39 +01:00 committed by Shams Asari
parent 903cdba57e
commit e96dcedfc6
4 changed files with 58 additions and 14 deletions

View File

@ -49,8 +49,10 @@ interface ConfigParser<T> {
const val CUSTOM_NODE_PROPERTIES_ROOT = "custom" const val CUSTOM_NODE_PROPERTIES_ROOT = "custom"
// TODO Move other config parsing to use parseAs and remove this // TODO Move other config parsing to use parseAs and remove this
// This is to enable constructs like:
// `val keyStorePassword: String by config`
operator fun <T : Any> Config.getValue(receiver: Any, metadata: KProperty<*>): T { operator fun <T : Any> Config.getValue(receiver: Any, metadata: KProperty<*>): T {
return getValueInternal(metadata.name, metadata.returnType, UnknownConfigKeysPolicy.IGNORE::handle) return getValueInternal(metadata.name, metadata.returnType, UnknownConfigKeysPolicy.IGNORE::handle, nestedPath = null, baseDirectory = null)
} }
// Problems: // Problems:
@ -59,7 +61,8 @@ operator fun <T : Any> Config.getValue(receiver: Any, metadata: KProperty<*>): T
// - Cannot support a many-to-one relationship between configuration file structures and configuration domain type. This is essential for versioning of the configuration files. // - Cannot support a many-to-one relationship between configuration file structures and configuration domain type. This is essential for versioning of the configuration files.
// - It's complicated and based on reflection, meaning problems with it are typically found at runtime. // - It's complicated and based on reflection, meaning problems with it are typically found at runtime.
// - It doesn't support validation errors in a structured way. If something goes wrong, it throws exceptions, which doesn't support good usability practices like displaying all the errors at once. // - It doesn't support validation errors in a structured way. If something goes wrong, it throws exceptions, which doesn't support good usability practices like displaying all the errors at once.
fun <T : Any> Config.parseAs(clazz: KClass<T>, onUnknownKeys: ((Set<String>, logger: Logger) -> Unit) = UnknownConfigKeysPolicy.FAIL::handle, nestedPath: String? = null): T { fun <T : Any> Config.parseAs(clazz: KClass<T>, onUnknownKeys: ((Set<String>, logger: Logger) -> Unit) = UnknownConfigKeysPolicy.FAIL::handle,
nestedPath: String? = null, baseDirectory: Path? = null): T {
// Use custom parser if provided, instead of treating the object as data class. // Use custom parser if provided, instead of treating the object as data class.
clazz.findAnnotation<CustomConfigParser>()?.let { return uncheckedCast(it.parser.createInstance().parse(this)) } clazz.findAnnotation<CustomConfigParser>()?.let { return uncheckedCast(it.parser.createInstance().parse(this)) }
@ -85,7 +88,7 @@ fun <T : Any> Config.parseAs(clazz: KClass<T>, onUnknownKeys: ((Set<String>, log
// Get the matching property for this parameter // Get the matching property for this parameter
val property = clazz.memberProperties.first { it.name == param.name } val property = clazz.memberProperties.first { it.name == param.name }
val path = defaultToOldPath(property) val path = defaultToOldPath(property)
getValueInternal<Any>(path, param.type, onUnknownKeys, nestedPath) getValueInternal<Any>(path, param.type, onUnknownKeys, nestedPath, baseDirectory)
} }
try { try {
return constructor.callBy(args) return constructor.callBy(args)
@ -114,11 +117,11 @@ fun Config.toProperties(): Properties {
{ it.value.unwrapped().toString() }) { it.value.unwrapped().toString() })
} }
private fun <T : Any> Config.getValueInternal(path: String, type: KType, onUnknownKeys: ((Set<String>, logger: Logger) -> Unit), nestedPath: String? = null): T { private fun <T : Any> Config.getValueInternal(path: String, type: KType, onUnknownKeys: ((Set<String>, logger: Logger) -> Unit), nestedPath: String?, baseDirectory: Path?): T {
return uncheckedCast(if (type.arguments.isEmpty()) getSingleValue(path, type, onUnknownKeys, nestedPath) else getCollectionValue(path, type, onUnknownKeys, nestedPath)) return uncheckedCast(if (type.arguments.isEmpty()) getSingleValue(path, type, onUnknownKeys, nestedPath, baseDirectory) else getCollectionValue(path, type, onUnknownKeys, nestedPath, baseDirectory))
} }
private fun Config.getSingleValue(path: String, type: KType, onUnknownKeys: (Set<String>, logger: Logger) -> Unit, nestedPath: String? = null): Any? { private fun Config.getSingleValue(path: String, type: KType, onUnknownKeys: (Set<String>, logger: Logger) -> Unit, nestedPath: String?, baseDirectory: Path?): Any? {
if (type.isMarkedNullable && !hasPath(path)) return null if (type.isMarkedNullable && !hasPath(path)) return null
val typeClass = type.jvmErasure val typeClass = type.jvmErasure
return try { return try {
@ -132,7 +135,10 @@ private fun Config.getSingleValue(path: String, type: KType, onUnknownKeys: (Set
Duration::class -> getDuration(path) Duration::class -> getDuration(path)
Instant::class -> Instant.parse(getString(path)) Instant::class -> Instant.parse(getString(path))
NetworkHostAndPort::class -> NetworkHostAndPort.parse(getString(path)) NetworkHostAndPort::class -> NetworkHostAndPort.parse(getString(path))
Path::class -> Paths.get(getString(path)) Path::class -> {
val pathAsString = getString(path)
resolvePath(pathAsString, baseDirectory)
}
URL::class -> URL(getString(path)) URL::class -> URL(getString(path))
UUID::class -> UUID.fromString(getString(path)) UUID::class -> UUID.fromString(getString(path))
X500Principal::class -> X500Principal(getString(path)) X500Principal::class -> X500Principal(getString(path))
@ -147,7 +153,7 @@ private fun Config.getSingleValue(path: String, type: KType, onUnknownKeys: (Set
else -> if (typeClass.java.isEnum) { else -> if (typeClass.java.isEnum) {
parseEnum(typeClass.java, getString(path)) parseEnum(typeClass.java, getString(path))
} else { } else {
getConfig(path).parseAs(typeClass, onUnknownKeys, nestedPath?.let { "$it.$path" } ?: path) getConfig(path).parseAs(typeClass, onUnknownKeys, nestedPath?.let { "$it.$path" } ?: path, baseDirectory = baseDirectory)
} }
} }
} catch (e: ConfigException.Missing) { } catch (e: ConfigException.Missing) {
@ -155,6 +161,16 @@ private fun Config.getSingleValue(path: String, type: KType, onUnknownKeys: (Set
} }
} }
private fun resolvePath(pathAsString: String, baseDirectory: Path?): Path {
val path = Paths.get(pathAsString)
return if (baseDirectory != null) {
// if baseDirectory been specified try resolving path against it. Note if `pathFromConfig` is an absolute path - this instruction has no effect.
baseDirectory.resolve(path)
} else {
path
}
}
private fun ConfigException.Missing.relative(path: String, nestedPath: String?): ConfigException.Missing { private fun ConfigException.Missing.relative(path: String, nestedPath: String?): ConfigException.Missing {
return when { return when {
nestedPath != null -> throw ConfigException.Missing("$nestedPath.$path", this) nestedPath != null -> throw ConfigException.Missing("$nestedPath.$path", this)
@ -162,7 +178,7 @@ private fun ConfigException.Missing.relative(path: String, nestedPath: String?):
} }
} }
private fun Config.getCollectionValue(path: String, type: KType, onUnknownKeys: (Set<String>, logger: Logger) -> Unit, nestedPath: String? = null): Collection<Any> { private fun Config.getCollectionValue(path: String, type: KType, onUnknownKeys: (Set<String>, logger: Logger) -> Unit, nestedPath: String?, baseDirectory: Path?): Collection<Any> {
val typeClass = type.jvmErasure val typeClass = type.jvmErasure
require(typeClass == List::class || typeClass == Set::class) { "$typeClass is not supported" } require(typeClass == List::class || typeClass == Set::class) { "$typeClass is not supported" }
val elementClass = type.arguments[0].type?.jvmErasure ?: throw IllegalArgumentException("Cannot work with star projection: $type") val elementClass = type.arguments[0].type?.jvmErasure ?: throw IllegalArgumentException("Cannot work with star projection: $type")
@ -179,7 +195,7 @@ private fun Config.getCollectionValue(path: String, type: KType, onUnknownKeys:
LocalDate::class -> getStringList(path).map(LocalDate::parse) LocalDate::class -> getStringList(path).map(LocalDate::parse)
Instant::class -> getStringList(path).map(Instant::parse) Instant::class -> getStringList(path).map(Instant::parse)
NetworkHostAndPort::class -> getStringList(path).map(NetworkHostAndPort.Companion::parse) NetworkHostAndPort::class -> getStringList(path).map(NetworkHostAndPort.Companion::parse)
Path::class -> getStringList(path).map { Paths.get(it) } Path::class -> getStringList(path).map { resolvePath(it, baseDirectory) }
URL::class -> getStringList(path).map(::URL) URL::class -> getStringList(path).map(::URL)
X500Principal::class -> getStringList(path).map(::X500Principal) X500Principal::class -> getStringList(path).map(::X500Principal)
UUID::class -> getStringList(path).map { UUID.fromString(it) } UUID::class -> getStringList(path).map { UUID.fromString(it) }
@ -188,7 +204,7 @@ private fun Config.getCollectionValue(path: String, type: KType, onUnknownKeys:
else -> if (elementClass.java.isEnum) { else -> if (elementClass.java.isEnum) {
getStringList(path).map { parseEnum(elementClass.java, it) } getStringList(path).map { parseEnum(elementClass.java, it) }
} else { } else {
getConfigList(path).map { it.parseAs(elementClass, onUnknownKeys) } getConfigList(path).map { it.parseAs(elementClass, onUnknownKeys, baseDirectory = baseDirectory) }
} }
} }
} catch (e: ConfigException.Missing) { } catch (e: ConfigException.Missing) {

View File

@ -69,10 +69,11 @@ internal object V1NodeConfigurationSpec : Configuration.Specification<NodeConfig
val messagingServerExternal = configuration[messagingServerExternal] ?: Defaults.messagingServerExternal(configuration[messagingServerAddress]) val messagingServerExternal = configuration[messagingServerExternal] ?: Defaults.messagingServerExternal(configuration[messagingServerAddress])
val database = configuration[database] ?: Defaults.database(configuration[devMode]) val database = configuration[database] ?: Defaults.database(configuration[devMode])
val cordappDirectories = configuration[cordappDirectories] ?: Defaults.cordappsDirectories(configuration[baseDirectory]) val baseDirectoryPath = configuration[baseDirectory]
val cordappDirectories = configuration[cordappDirectories] ?: Defaults.cordappsDirectories(baseDirectoryPath)
val result = try { val result = try {
valid<NodeConfigurationImpl, Configuration.Validation.Error>(NodeConfigurationImpl( valid<NodeConfigurationImpl, Configuration.Validation.Error>(NodeConfigurationImpl(
baseDirectory = configuration[baseDirectory], baseDirectory = baseDirectoryPath,
myLegalName = configuration[myLegalName], myLegalName = configuration[myLegalName],
emailAddress = configuration[emailAddress], emailAddress = configuration[emailAddress],
p2pAddress = configuration[p2pAddress], p2pAddress = configuration[p2pAddress],
@ -116,7 +117,7 @@ internal object V1NodeConfigurationSpec : Configuration.Specification<NodeConfig
attachmentContentCacheSizeMegaBytes = configuration[attachmentContentCacheSizeMegaBytes], attachmentContentCacheSizeMegaBytes = configuration[attachmentContentCacheSizeMegaBytes],
h2port = configuration[h2port], h2port = configuration[h2port],
jarDirs = configuration[jarDirs], jarDirs = configuration[jarDirs],
cordappDirectories = cordappDirectories, cordappDirectories = cordappDirectories.map { baseDirectoryPath.resolve(it) },
cordappSignerKeyFingerprintBlacklist = configuration[cordappSignerKeyFingerprintBlacklist] cordappSignerKeyFingerprintBlacklist = configuration[cordappSignerKeyFingerprintBlacklist]
)) ))
} catch (e: Exception) { } catch (e: Exception) {

View File

@ -5,6 +5,7 @@ import com.typesafe.config.ConfigFactory
import com.typesafe.config.ConfigParseOptions import com.typesafe.config.ConfigParseOptions
import com.typesafe.config.ConfigValueFactory import com.typesafe.config.ConfigValueFactory
import net.corda.common.configuration.parsing.internal.Configuration import net.corda.common.configuration.parsing.internal.Configuration
import net.corda.core.internal.div
import net.corda.core.internal.toPath import net.corda.core.internal.toPath
import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.NetworkHostAndPort
import net.corda.core.utilities.seconds import net.corda.core.utilities.seconds
@ -16,7 +17,9 @@ import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.Assertions.assertThatThrownBy import org.assertj.core.api.Assertions.assertThatThrownBy
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull import org.junit.Assert.assertNotNull
import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.rules.TemporaryFolder
import java.net.URI import java.net.URI
import java.net.URL import java.net.URL
import java.nio.file.Paths import java.nio.file.Paths
@ -25,6 +28,11 @@ import kotlin.test.assertFalse
import kotlin.test.assertTrue import kotlin.test.assertTrue
class NodeConfigurationImplTest { class NodeConfigurationImplTest {
@Rule
@JvmField
val tempFolder = TemporaryFolder()
@Test @Test
fun `can't have dev mode options if not in dev mode`() { fun `can't have dev mode options if not in dev mode`() {
val debugOptions = DevModeOptions() val debugOptions = DevModeOptions()
@ -192,6 +200,24 @@ class NodeConfigurationImplTest {
assertThat(rawConfig.parseAsNodeConfiguration().isValid).isTrue() assertThat(rawConfig.parseAsNodeConfiguration().isValid).isTrue()
} }
@Test
fun `relative path correctly parsed`() {
val rawConfig = ConfigFactory.parseResources("working-config.conf", ConfigParseOptions.defaults().setAllowMissing(false))
// Override base directory to have predictable experience on diff OSes
val finalConfig = configOf(
// Add substitution values here
"baseDirectory" to tempFolder.root.canonicalPath)
.withFallback(rawConfig)
.resolve()
val nodeConfiguration = finalConfig.parseAsNodeConfiguration()
assertThat(nodeConfiguration.isValid).isTrue()
val baseDirPath = tempFolder.root.toPath()
assertEquals(listOf(baseDirPath / "./myCorDapps1", baseDirPath / "./myCorDapps2"), nodeConfiguration.value().cordappDirectories)
}
@Test @Test
fun `missing rpcSettings_adminAddress cause a graceful failure`() { fun `missing rpcSettings_adminAddress cause a graceful failure`() {
var rawConfig = ConfigFactory.parseResources("working-config.conf", ConfigParseOptions.defaults().setAllowMissing(false)) var rawConfig = ConfigFactory.parseResources("working-config.conf", ConfigParseOptions.defaults().setAllowMissing(false))

View File

@ -4,6 +4,7 @@ keyStorePassword = "cordacadevpass"
trustStorePassword = "trustpass" trustStorePassword = "trustpass"
crlCheckSoftFail = true crlCheckSoftFail = true
baseDirectory = "/opt/corda" baseDirectory = "/opt/corda"
cordappDirectories = ["./myCorDapps1", "./myCorDapps2"]
dataSourceProperties = { dataSourceProperties = {
dataSourceClassName = org.h2.jdbcx.JdbcDataSource dataSourceClassName = org.h2.jdbcx.JdbcDataSource
dataSource.url = "jdbc:h2:file:blah" dataSource.url = "jdbc:h2:file:blah"