solution: paths relative to config, default is at /etc/dshackle

This commit is contained in:
Igor Artamonov
2019-09-08 22:30:46 -04:00
parent 26d2b0a3a0
commit 7fd27d581a
11 changed files with 207 additions and 33 deletions

View File

@@ -72,7 +72,7 @@ upstreams:
This configures: This configures:
- setups 2 upstreams, one for Ethereum Mainnet and another for Kovan Testnet (both upstreams are configured for Infura) - setups 2 upstreams, one for Ethereum Mainnet and another for Kovan Testnet (both upstreams are configured to use Infura endpoint)
- for Ethereum Mainnet it connects using JSON RPC and Websockets connections, for Kovan just JSON RPC is used - for Ethereum Mainnet it connects using JSON RPC and Websockets connections, for Kovan just JSON RPC is used
- Infura authentication config is omitted for this demo - Infura authentication config is omitted for this demo
- `${INFURA_USER}` will be provided through environment variable - `${INFURA_USER}` will be provided through environment variable
@@ -90,7 +90,7 @@ export INFURA_USER=...
.Run Dshackle .Run Dshackle
[source,bash] [source,bash]
---- ----
docker run -p 2449:2449 -v $(pwd):/config -w /config -e "INFURA_USER=$INFURA_USER" emeraldpay/dshackle docker run -p 2449:2449 -v $(pwd):/etc/dshackle -e "INFURA_USER=$INFURA_USER" emeraldpay/dshackle
---- ----
Now it listen on port 2449 at the localhost and can be connected from any gRPC compatible client. Now it listen on port 2449 at the localhost and can be connected from any gRPC compatible client.

View File

@@ -71,7 +71,7 @@ export INFURA_USER=...
.Run Dshackle .Run Dshackle
[source,bash] [source,bash]
---- ----
docker run -p 2449:2449 -v $(pwd):/config -w /config -e "INFURA_USER=$INFURA_USER" emeraldpay/dshackle docker run -p 2449:2449 -v $(pwd):/etc/dshackle -e "INFURA_USER=$INFURA_USER" emeraldpay/dshackle
---- ----
.Connect and listen for new blocks on Ethereum Mainnet .Connect and listen for new blocks on Ethereum Mainnet

View File

@@ -1,6 +1,10 @@
== Server Configuration == Server Configuration
Dshackle server tries to load its configuration from `/etc/dshackle/dshackle.yaml`, if it can't find a file at that path
it tries to load file `dshackle.yaml` from current working directory. If none of them found server fails to run with an error.
[source,yaml] [source,yaml]
.Example dshackle.yaml configuration:
---- ----
version: v1 version: v1
port: 2449 port: 2449
@@ -16,10 +20,10 @@ upstreams:
config: "upstreams.yaml" config: "upstreams.yaml"
---- ----
Configures following: It configures following:
- server is listening on `0.0.0.0:2449` - server is listening on `0.0.0.0:2449`
- TLS is enabled - TLS is enabled
- server certificate in located at `server.crt` with key for it at `server.p8.key` - server certificate is located at `server.crt` with the key for it at `server.p8.key`
- server requires a client authentication by TLS client certificate signed by `ca.crt` certificate - the server requires a client authentication by TLS client certificate signed by `ca.crt` certificate
- upstreams configuration is configured in file `upstreams.yaml` - upstreams configuration is configured in the file `upstreams.yaml`

View File

@@ -19,22 +19,34 @@ import com.fasterxml.jackson.core.Version
import com.fasterxml.jackson.databind.DeserializationFeature import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.databind.module.SimpleModule import com.fasterxml.jackson.databind.module.SimpleModule
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.beans.factory.annotation.Qualifier import org.springframework.beans.factory.annotation.Qualifier
import org.springframework.context.ApplicationContext
import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration import org.springframework.context.annotation.Configuration
import org.springframework.context.annotation.Import
import org.springframework.core.env.Environment
import org.springframework.scheduling.annotation.EnableAsync import org.springframework.scheduling.annotation.EnableAsync
import org.springframework.scheduling.annotation.EnableScheduling import org.springframework.scheduling.annotation.EnableScheduling
import org.springframework.scheduling.annotation.Scheduled import org.springframework.scheduling.annotation.Scheduled
import reactor.core.scheduler.Scheduler import reactor.core.scheduler.Scheduler
import reactor.core.scheduler.Schedulers import reactor.core.scheduler.Schedulers
import java.io.File
import java.lang.IllegalStateException
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.* import java.util.*
import java.util.concurrent.Executors import java.util.concurrent.Executors
import kotlin.system.exitProcess
@Configuration @Configuration
@EnableScheduling @EnableScheduling
@EnableAsync @EnableAsync
open class Config { open class Config(
@Autowired private val env: Environment
) {
private val log = LoggerFactory.getLogger(Config::class.java)
@Bean @Bean
open fun objectMapper(): ObjectMapper { open fun objectMapper(): ObjectMapper {
@@ -55,4 +67,19 @@ open class Config {
return Schedulers.fromExecutorService(Executors.newFixedThreadPool(16)) return Schedulers.fromExecutorService(Executors.newFixedThreadPool(16))
} }
@Bean
@Qualifier("configDir")
open fun configDir(): File {
val config = env.getProperty("configPath") ?: throw IllegalStateException("Config path is not set")
if (config.trim().isEmpty()) {
throw IllegalStateException("Config path is empty")
}
log.info("Use configuration from: $config")
return File(config).parentFile
}
@Bean
open fun fileResolver(): FileResolver {
return FileResolver(configDir())
}
} }

View File

@@ -0,0 +1,67 @@
/**
* Copyright (c) 2019 ETCDEV GmbH
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.emeraldpay.dshackle
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.config.YamlPropertiesFactoryBean
import org.springframework.core.env.*
import org.springframework.core.io.FileSystemResource
import org.springframework.core.io.Resource
import java.io.File
import java.util.*
const val DEFAULT_CONFIG = "/etc/dshackle/dshackle.yaml"
const val LOCAL_CONFIG = "./dshackle.yaml"
open class DshackleEnvironment: StandardEnvironment() {
companion object {
private val log = LoggerFactory.getLogger(DshackleEnvironment::class.java)
}
override fun customizePropertySources(propertySources: MutablePropertySources) {
super.customizePropertySources(propertySources)
propertySources.addLast(mainConfig())
}
open fun mainConfig(): PropertySource<*> {
var target = File(DEFAULT_CONFIG)
if (!isAcceptedConfig(target)) {
target = File(LOCAL_CONFIG)
if (!isAcceptedConfig(target)) {
log.error("Configuration is not found neither at $DEFAULT_CONFIG nor $LOCAL_CONFIG")
return PropertySource.named("mainConfig")
}
}
target = target.normalize()
log.info("Load configuration from: ${target.absolutePath}")
val loadedProperties = this.loadYaml(FileSystemResource(target))
loadedProperties["configPath"] = target.absolutePath
return PropertiesPropertySource("mainConfig", loadedProperties)
}
protected fun loadYaml(resource: Resource): Properties {
val factory = YamlPropertiesFactoryBean()
factory.setResources(resource)
factory.afterPropertiesSet()
return factory.getObject()!!
}
protected fun isAcceptedConfig(target: File): Boolean {
return target.exists() && target.isFile
}
}

View File

@@ -0,0 +1,32 @@
/**
* Copyright (c) 2019 ETCDEV GmbH
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.emeraldpay.dshackle
import java.io.File
class FileResolver(
private val baseDir: File
) {
fun resolve(path: String): File {
val direct = File(path)
if (direct.isAbsolute) {
return direct
}
return File(baseDir, path)
}
}

View File

@@ -25,7 +25,6 @@ import org.springframework.beans.factory.annotation.Autowired
import org.springframework.core.env.Environment import org.springframework.core.env.Environment
import org.springframework.core.io.ResourceLoader import org.springframework.core.io.ResourceLoader
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
import java.io.File
import javax.annotation.PostConstruct import javax.annotation.PostConstruct
import javax.annotation.PreDestroy import javax.annotation.PreDestroy
@@ -33,7 +32,8 @@ import javax.annotation.PreDestroy
open class GrpcServer( open class GrpcServer(
@Autowired val rpcs: List<io.grpc.BindableService>, @Autowired val rpcs: List<io.grpc.BindableService>,
@Autowired val resourceLoader: ResourceLoader, @Autowired val resourceLoader: ResourceLoader,
@Autowired val env: Environment @Autowired val env: Environment,
@Autowired val fileResolver: FileResolver
) { ) {
private val log = LoggerFactory.getLogger(GrpcServer::class.java) private val log = LoggerFactory.getLogger(GrpcServer::class.java)
@@ -73,13 +73,13 @@ open class GrpcServer(
if (mustBeSecure || (!tlsDisabled && hasServerCertificate)) { if (mustBeSecure || (!tlsDisabled && hasServerCertificate)) {
log.info("Using TLS") log.info("Using TLS")
val sslContextBuilder = GrpcSslContexts.forServer( val sslContextBuilder = GrpcSslContexts.forServer(
File(env.getProperty("tls.server.certificate")!!), fileResolver.resolve(env.getProperty("tls.server.certificate")!!),
File(env.getProperty("tls.server.key")!!) fileResolver.resolve(env.getProperty("tls.server.key")!!)
) )
if (StringUtils.isNotEmpty(env.getProperty("tls.client.ca"))) { if (StringUtils.isNotEmpty(env.getProperty("tls.client.ca"))) {
log.info("Using TLS for client authentication") log.info("Using TLS for client authentication")
sslContextBuilder.trustManager( sslContextBuilder.trustManager(
File(env.getProperty("tls.client.ca")!!) fileResolver.resolve(env.getProperty("tls.client.ca")!!)
) )
if (env.getProperty("tls.client.require", "true") == "true") { if (env.getProperty("tls.client.require", "true") == "true") {
sslContextBuilder.clientAuth(ClientAuth.REQUIRE) sslContextBuilder.clientAuth(ClientAuth.REQUIRE)

View File

@@ -15,19 +15,19 @@
*/ */
package io.emeraldpay.dshackle package io.emeraldpay.dshackle
import org.springframework.beans.factory.config.YamlPropertiesFactoryBean import org.slf4j.LoggerFactory
import org.springframework.boot.SpringApplication import org.springframework.boot.SpringApplication
import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.env.YamlPropertySourceLoader
import org.springframework.context.annotation.Import import org.springframework.context.annotation.Import
import org.springframework.context.annotation.PropertySource
@SpringBootApplication(scanBasePackages = [ "io.emeraldpay.dshackle" ]) @SpringBootApplication(scanBasePackages = [ "io.emeraldpay.dshackle" ])
@PropertySource("file:./dshackle.yaml", ignoreResourceNotFound = true, factory = YamlPropertySourceFactory::class)
@Import(Config::class) @Import(Config::class)
open class Starter open class Starter
private val log = LoggerFactory.getLogger(Starter::class.java)
fun main(args: Array<String>) { fun main(args: Array<String>) {
val app = SpringApplication(Starter::class.java) val app = SpringApplication(Starter::class.java)
app.setEnvironment(DshackleEnvironment())
app.run(*args) app.run(*args)
} }

View File

@@ -16,6 +16,7 @@
package io.emeraldpay.dshackle.upstream package io.emeraldpay.dshackle.upstream
import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.databind.ObjectMapper
import io.emeraldpay.dshackle.FileResolver
import io.emeraldpay.dshackle.config.UpstreamsConfig import io.emeraldpay.dshackle.config.UpstreamsConfig
import io.emeraldpay.dshackle.config.UpstreamsConfigReader import io.emeraldpay.dshackle.config.UpstreamsConfigReader
import io.emeraldpay.dshackle.upstream.ethereum.DirectEthereumApi import io.emeraldpay.dshackle.upstream.ethereum.DirectEthereumApi
@@ -29,22 +30,19 @@ import org.apache.commons.lang3.StringUtils
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Autowired import org.springframework.beans.factory.annotation.Autowired
import org.springframework.core.env.Environment import org.springframework.core.env.Environment
import org.springframework.scheduling.annotation.Scheduled
import org.springframework.stereotype.Repository import org.springframework.stereotype.Repository
import reactor.core.publisher.Flux
import reactor.core.publisher.TopicProcessor
import java.io.File
import java.net.URI import java.net.URI
import java.util.* import java.util.*
import java.util.concurrent.ConcurrentHashMap
import javax.annotation.PostConstruct import javax.annotation.PostConstruct
import kotlin.collections.HashMap import kotlin.collections.HashMap
import kotlin.system.exitProcess
@Repository @Repository
open class ConfiguredUpstreams( open class ConfiguredUpstreams(
@Autowired val env: Environment, @Autowired val env: Environment,
@Autowired private val objectMapper: ObjectMapper, @Autowired private val objectMapper: ObjectMapper,
@Autowired private val currentUpstreams: CurrentUpstreams @Autowired private val currentUpstreams: CurrentUpstreams,
@Autowired private val fileResolver: FileResolver
) { ) {
private val log = LoggerFactory.getLogger(ConfiguredUpstreams::class.java) private val log = LoggerFactory.getLogger(ConfiguredUpstreams::class.java)
@@ -83,13 +81,13 @@ open class ConfiguredUpstreams(
val path = env.getProperty("upstreams.config") val path = env.getProperty("upstreams.config")
if (StringUtils.isEmpty(path)) { if (StringUtils.isEmpty(path)) {
log.error("Path to upstreams is not set (upstreams.config)") log.error("Path to upstreams is not set (upstreams.config)")
System.exit(1) exitProcess(1)
} }
val upstreamConfig = File(path!!) val upstreamConfig = fileResolver.resolve(path!!).normalize()
val ok = upstreamConfig.exists() && upstreamConfig.isFile val ok = upstreamConfig.exists() && upstreamConfig.isFile
if (!ok) { if (!ok) {
log.error("Unable to setup upstreams from ${upstreamConfig.path}") log.error("Unable to setup upstreams from ${upstreamConfig.path}")
System.exit(1) exitProcess(1)
} }
log.info("Read upstream configuration from ${upstreamConfig.path}") log.info("Read upstream configuration from ${upstreamConfig.path}")
val reader = UpstreamsConfigReader() val reader = UpstreamsConfigReader()
@@ -139,7 +137,7 @@ open class ConfiguredUpstreams(
} }
conn.rpc?.tls?.let { tls -> conn.rpc?.tls?.let { tls ->
tls.ca?.let { ca -> tls.ca?.let { ca ->
File(ca).inputStream().use { cert -> rpcTransport.setTrustedCertificate(cert) } fileResolver.resolve(ca).inputStream().use { cert -> rpcTransport.setTrustedCertificate(cert) }
} }
} }
val rpcClient = DefaultRpcClient(rpcTransport) val rpcClient = DefaultRpcClient(rpcTransport)
@@ -181,9 +179,10 @@ open class ConfiguredUpstreams(
val ds = GrpcUpstreams( val ds = GrpcUpstreams(
config.id!!, config.id!!,
endpoint.host!!, endpoint.host!!,
endpoint.port ?: 443, endpoint.port ?: 2449,
objectMapper, objectMapper,
endpoint.auth endpoint.auth,
fileResolver
) )
log.info("Using ALL CHAINS (gRPC) upstream, at ${endpoint.host}:${endpoint.port}") log.info("Using ALL CHAINS (gRPC) upstream, at ${endpoint.host}:${endpoint.port}")
ds.start() ds.start()

View File

@@ -18,6 +18,7 @@ package io.emeraldpay.dshackle.upstream.grpc
import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.databind.ObjectMapper
import io.emeraldpay.api.proto.BlockchainOuterClass import io.emeraldpay.api.proto.BlockchainOuterClass
import io.emeraldpay.api.proto.ReactorBlockchainGrpc import io.emeraldpay.api.proto.ReactorBlockchainGrpc
import io.emeraldpay.dshackle.FileResolver
import io.emeraldpay.dshackle.config.UpstreamsConfig import io.emeraldpay.dshackle.config.UpstreamsConfig
import io.emeraldpay.dshackle.upstream.UpstreamChange import io.emeraldpay.dshackle.upstream.UpstreamChange
import io.emeraldpay.grpc.Chain import io.emeraldpay.grpc.Chain
@@ -29,7 +30,6 @@ import org.apache.commons.lang3.StringUtils
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import reactor.core.Disposable import reactor.core.Disposable
import reactor.core.publisher.Flux import reactor.core.publisher.Flux
import java.io.File
import java.time.Duration import java.time.Duration
import java.util.* import java.util.*
import java.util.concurrent.Executors import java.util.concurrent.Executors
@@ -42,7 +42,8 @@ class GrpcUpstreams(
private val host: String, private val host: String,
private val port: Int, private val port: Int,
private val objectMapper: ObjectMapper, private val objectMapper: ObjectMapper,
private val auth: UpstreamsConfig.TlsAuth? = null private val auth: UpstreamsConfig.TlsAuth? = null,
private val fileResolver: FileResolver
) { ) {
private val log = LoggerFactory.getLogger(GrpcUpstreams::class.java) private val log = LoggerFactory.getLogger(GrpcUpstreams::class.java)
@@ -127,9 +128,11 @@ class GrpcUpstreams(
internal fun withTls(auth: UpstreamsConfig.TlsAuth): SslContext { internal fun withTls(auth: UpstreamsConfig.TlsAuth): SslContext {
val sslContext = SslContextBuilder.forClient() val sslContext = SslContextBuilder.forClient()
.clientAuth(ClientAuth.REQUIRE) .clientAuth(ClientAuth.REQUIRE)
sslContext.trustManager(File(auth.ca!!).inputStream()) sslContext.trustManager(fileResolver.resolve(auth.ca!!).inputStream())
if (StringUtils.isNotEmpty(auth.key) && StringUtils.isNoneEmpty(auth.certificate)) { if (StringUtils.isNotEmpty(auth.key) && StringUtils.isNoneEmpty(auth.certificate)) {
sslContext.keyManager(File(auth.certificate!!).inputStream(), File(auth.key!!).inputStream()) sslContext.keyManager(
fileResolver.resolve(auth.certificate!!).inputStream(),
fileResolver.resolve(auth.key!!).inputStream())
} else { } else {
log.warn("Connect to remote using only CA certificate") log.warn("Connect to remote using only CA certificate")
} }

View File

@@ -0,0 +1,42 @@
/**
* Copyright (c) 2019 ETCDEV GmbH
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.emeraldpay.dshackle
import spock.lang.Specification
class FileResolverSpec extends Specification {
def "Leaves absolute path"() {
setup:
def resolver = new FileResolver(new File("/tmp"))
expect:
resolver.resolve(path).absolutePath == path
where:
path << ["/test.txt", "/etc/dshackle/dshackle.yaml"]
}
def "Start from base dir for related path"() {
setup:
def resolver = new FileResolver(new File("/tmp"))
expect:
resolver.resolve(path).canonicalPath == new File(fullPath).canonicalPath
where:
path | fullPath
"test.txt" | "/tmp/test.txt"
"./test.txt" | "/tmp/test.txt"
"./tls/ca.crt" | "/tmp/tls/ca.crt"
}
}