Search code examples
configenvironmentktor

How to access user defined environment variable in Ktor


I am using ktor as a web server. Users call a Kotlin Clikt command to start the server, passing to it a variable that is needed later when the backend processes certain REST requests.

The clikt class then starts the server takes a single parameter and this parameter is passed when starting the embedded server. That code is here:

class StartServer : CliktCommand(help = "Starts PHGv2 BrAPI Server") {

    private val myLogger = LogManager.getLogger(StartServer::class.java)
    val dbPath by option(help = "Full path to folder where TileDB datasets are stored.   ")
        .default("")
        .validate {
            require(it.isNotBlank()) {
                "--db-path must not be blank"
            }
        }

    override fun run() {
        setupDebugLogging()

        // Verify the uri is valid.  We only care about the hvcf dataset,
        // so check that one explicitly
        val hvcfExists = verifyURI(dbPath,"hvcf_dataset")
        if (!hvcfExists) {
            myLogger.error("hvcf_dataset does not exist in $dbPath.  Exiting.")
            return
        }

        // Create an Args list to pass to the server
        // This tells the endpoint code where the datasets are located.
        val dbUri = "-P:TILEDB_URI=${dbPath}"
        val args = arrayOf(dbUri)

        // commandLineEnvironment reads the application.config file
        // https://ktor.io/docs/configuration.html#hocon-file
        embeddedServer(Netty, commandLineEnvironment(args)).start(wait = true)
    }
}

Later, when servicing a query that needs this configuration variable, I have this code:

private val config = HoconApplicationConfig(ConfigFactory.load())

//val tiledbURI = environment.config.property("TILEDB_URI").getString()
val tiledbURI = config.property("TILEDB_URI").getString()

object SamplesService {

    private val myLogger = LogManager.getLogger(SamplesService::class.java)

    // Cached map of all taxa. Key is genoid mapped to Sample object
    private val taxa: Map<String, Sample> by lazy {
        taxaMap("${tiledbURI}/hvcf_dataset")
    }
     ....

The value for tiledbURI is always null (but the code compiles). If I follow the examples from the the documentation, it shows grabbing the values from environment:

val tiledbURI = environment.config.property("TILEDB_URI").getString()

But "environment" is not known and will not compile. Is there a different import that is needed? My related imports are:

import com.typesafe.config.ConfigFactory
import io.ktor.server.config.*

Am I missing an import? Or does this variable only exist to start the server, and they are not stored in the config file for access further down?

UPDATE/EDIT:

The hcon config file is this:

callsPageSize=10
variantsPageSize=100

# For connecting to tiledb from ktor.  Users should un-comment
# and edit the TILEDB_URI variable to point to their tiledb folder
# When running junit tests,  replace ""/Users/lcj34" in the example below
# with the  path to your home directory.  The path should end with a /
# For other use cases, replace the path with the path to the tiledb folder
#TILEDB_URI="/Users/lcj34/temp/phgv2Tests/tempDir/testTileDBURI/"

# Server metadata params  You will need to fill these out to match your setup
contactEmail = "[email protected]"
documentationURL = "https://github.com/maize-genetics/phg_v2"
location = "Ithaca NY"
organizationName = "Institute for Genetic Diversity at Cornell University"
organizationURL = "https://www.maizegenetics.net/"
serverDescription = "Server to connect to the Maize PHG Tiledb through BrAPI calls."
serverName = "Maize  PHGv2"

ktor {
    deployment {
        port = 8080
        watch = [  build ]
    }
    application {
        modules = [ net.maizegenetics.phgv2.brapi.ApplicationKt.module ]
    }
}

Note on the commented out TILEDB_URI variable. Users that run our application do not have easy access to the config file as it is bundled in a fat jar. The comment related to updating the TILEDB_URI variable in the config file is mostly for developer junit testing.

What we need is for the user to be able to pass us a value that we can set for this parameter.


Solution

  • I was able to get this to work by making the following changes. Basically, I pass the user parameter through to the ktor routing code. The files shown above are changed as follows:

    class StartServer : CliktCommand(help = "Starts PHGv2 BrAPI Server") {
    
        private val myLogger = LogManager.getLogger(StartServer::class.java)
        val dbPath by option(help = "Full path to folder where TileDB datasets are stored.   ")
            .default("")
            .validate {
                require(it.isNotBlank()) {
                    "--db-path must not be blank"
                }
            }
    
        override fun run() {
            setupDebugLogging()
    
            // Verify the uri is valid.  We only care about the hvcf dataset,
            // so check that one explicitly
            val hvcfExists = verifyURI(dbPath,"hvcf_dataset")
            if (!hvcfExists) {
                myLogger.error("hvcf_dataset does not exist in $dbPath.  Exiting.")
                return
            }
    
            // Create an Args list to pass to the server
            // This tells the endpoint code where the datasets are located.
            val dbUri = "-P:TILEDB_URI=${dbPath}"
            //val dbUri = "TILEDB_URI=${dbPath}"
            val args = arrayOf(dbUri)
    
            val server = embeddedServer(Netty,port=8080) {
                module(args)
            }
            server.start(true)
    
        }
    
    }
    

    this is then processed in Application.kt via:

    fun Application.module(args:Array<String>) {
    
        install(DefaultHeaders)
        install(CallLogging)
    
        install(ContentNegotiation) {
            json(Json {
                prettyPrint = false
                isLenient = true
                encodeDefaults = true
            })
        }
    
    
        // Setup routing.  Individual endpoints create Kotlin Route extensions
        // to handle processing REST requests.
    
        routing {
            // this method routes brapi/v2/
            // Within apiRoute(), specific endpoint calls are handled
            apiRoute(args)
        }
    
    }
    

    We continue to pass this argument until it reaches the function that actually processes the data:

    fun Route.samples(args:Array<String>) {
    
        val samplesService = SamplesService
        val tiledbURI = args[0].substringAfter("TILEDB_URI=").substringBefore(" ")
    
        route("/samples") {
    
            get("") {
                call.respond(
                    SampleListResponse(
                        Metadata(),
                        SampleListResponseResult(samplesService.lcjAllTaxaNames(tiledbURI).toTypedArray())
                        
                    )
                )
            }
    
        }
    }
    

    If there is a better way to pass the argument value, please let me know, but this works for now.