3

Issue

When deploying a Ktor Kotlin application to AppEngine per the Ktor tutorial, the Firestore server authentication is not working, thus data is not being written to the specified Firestore database.

Data is written as expected to Firestore both when the app is run directly in the IntelliJ IDE as well as when it is run with ktor's implementation via the gradle appengineRun command.

There are two sets of AppEngine/Firebase projects for both a staging and production environment. Prior to deploying with the gradle appengineDeploy command the correct SDK configuration been activated and verified via the command gcloud config configurations list.

enter image description here

The strange part is that a few of the apps deployed with these strategies did write to Firestore, however upon deploying the app again Firestore did not show new data being written to it.

Implementation

Ktor Setup

I have the standard ktor required files. I also have an old MANIFEST.MF file from an older implementation. Could that be causing issues?

src/main/resources/application.conf

ktor {
  application {
  modules = [ Initialization.main ]
  }
}

src/main/resources/webapp/WEB-INF/

appengine-web.xml

<?xml version="1.0" encoding="utf-8"?>
<appengine-web-app xmlns="http://appengine.google.com/ns/1.0">
    <threadsafe>true</threadsafe>
    <runtime>java8</runtime>
</appengine-web-app>

web.xml

<?xml version="1.0" encoding="ISO-8859-1" ?>
<web-app xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" version="3.0">
<servlet>
    <display-name>KtorServlet</display-name>
    <servlet-name>KtorServlet</servlet-name>
    <servlet-class>io.ktor.server.servlet.ServletApplicationEngine</servlet-class>
    <!-- path to application.conf file, required -->
    <init-param>
        <param-name>io.ktor.config</param-name>
        <param-value>application.conf</param-value>
    </init-param>
</servlet>
<servlet-mapping>
    <servlet-name>KtorServlet</servlet-name>
    <url-pattern>/</url-pattern>
</servlet-mapping>

logging.properties

.level = INFO

src/main/META-INF/MANIFEST>MF

Manifest-Version: 1.0
Main-Class: Initialization

Dependencies

For authentication strategies outlined below #1-3 the Firebase Admin library is used: compile 'com.google.firebase:firebase-admin:6.5.0'

For authentication strategy #4 the Google Cloud Firestore library is used: compile 'com.google.cloud:google-cloud-firestore:0.58.0-beta'

build.gradle

group 'coinverse'
version '1.0-SNAPSHOT'

buildscript {
    ext.kotlin_version = '1.2.61'
    ext.junitJupiterVersion  = '5.0.3'
    ext.ktor_version = '0.9.4'
    ext.appengine_version = '1.9.60'
    ext.appengine_plugin_version = '1.3.4'

repositories {
    mavenCentral()
    jcenter()
}
dependencies {
    classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
    classpath 'org.junit.platform:junit-platform-gradle-plugin:1.0.3'
    classpath "com.google.cloud.tools:appengine-gradle-plugin:$appengine_plugin_version"
    }
}

apply plugin: 'java'
apply plugin: 'kotlin'
apply plugin: 'war'
apply plugin: 'com.google.cloud.tools.appengine'

sourceSets {
    main.kotlin.srcDirs = [ 'src/main/kotlin' ]
}

sourceCompatibility = 1.8

repositories {
mavenCentral()
jcenter()
maven { url "https://kotlin.bintray.com/ktor" }
}

dependencies {
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
    testCompile group: 'junit', name: 'junit', version: '4.12'
    testCompile("org.junit.jupiter:junit-jupiter-api:${junitJupiterVersion}")
        testRuntime("org.junit.jupiter:junit-jupiter-engine:${junitJupiterVersion}")
        testCompile("org.assertj:assertj-core:3.10.0")
        testCompileOnly('org.apiguardian:apiguardian-api:1.0.0')
        compile 'com.squareup.retrofit2:retrofit:2.3.0'
        compile 'com.squareup.retrofit2:converter-gson:2.3.0'
        compile 'com.squareup.retrofit2:adapter-rxjava:2.3.0'
        compile 'io.reactivex.rxjava2:rxjava:2.2.0'
        compile 'com.google.cloud:google-cloud-firestore:0.58.0-beta'
        // Or compile 'com.google.cloud:google-cloud-firestore:0.58.0-beta'
        compile 'com.google.firebase:firebase-admin:6.5.0'
        compile "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
        compile "io.ktor:ktor-server-servlet:$ktor_version"
        compile "io.ktor:ktor-html-builder:$ktor_version"
        providedCompile "com.google.appengine:appengine:$appengine_version"
}

kotlin.experimental.coroutines = 'enable'

compileKotlin {
    kotlinOptions.jvmTarget = "1.8"
}
compileTestKotlin {
    kotlinOptions.jvmTarget = "1.8"
}

task run(dependsOn: appengineRun)

appengine {
    deploy {
        version = 'price-staging-1021653pm'
        stopPreviousVersion = false
    }
}

Initializing Firebase Strategies

1. Initialize on Google Cloud Platform

This method is promising as credentials are managed automatically.

// Use the application default credentials
GoogleCredentials credentials = GoogleCredentials.getApplicationDefault();
FirebaseOptions options = new FirebaseOptions.Builder()
    .setCredentials(credentials)
    .setProjectId(projectId)
    .build();
FirebaseApp.initializeApp(options);

Firestore db = FirestoreClient.getFirestore();

2. Initialize on your own server

I've confirmed in GCPs IAM & admin > Service accounts that the key id's match with the Json object being used to authenticate.

I'm using this strategy successfully in another Firestore connected app deployed to AppEngine. The working app is built as a .Jar and deployed directly to AppEngine without using ktor, but rather with the steps outlined here.

// Use a service account
InputStream serviceAccount = new FileInputStream("path/to/serviceAccount.json");
GoogleCredentials credentials = GoogleCredentials.fromStream(serviceAccount);
FirebaseOptions options = new FirebaseOptions.Builder()
    .setCredentials(credentials)
    .build();
FirebaseApp.initializeApp(options);

Firestore db = FirestoreClient.getFirestore();

In my working .Jar built app I'm passing in the Json object programmatically to avoid issues with the file not being found. I tried the same programmatic implementation for this ktor application. It worked with gradle appengineRun but not when deployed.

val credentials = GoogleCredentials.fromStream(Gson().toJson(FirebaseCredentials(
            "service_account",
            "project-name",
            "asdfghjkl",
            "keyStringHere",
            "firebase-adminsdk-dhr30@project-name.iam.gserviceaccount.com",
            "1234567890",
            "https://accounts.google.com/o/oauth2/auth",
            "https://oauth2.googleapis.com/token",
            "https://www.googleapis.com/oauth2/v1/certs",
           "https://www.googleapis.com/robot/v1/metadata/x509/firebase-adminsdk-dhr30%40project-name-staging.iam.gserviceaccount.com"
    )).byteInputStream())
    val options = FirebaseOptions.Builder()
            .setCredentials(credentials)
            .setDatabaseUrl("https://project-name-staging.firebaseio.com")
            .build()

    FirebaseApp.initializeApp(options)

3. Initialize on your own server (Firebase console setup)

The only difference between #2 is this setup adds .setDatabaseUrl("https://yourProjectName.firebaseio.com").

enter image description here

4. Initialize cloud Firestore

FirestoreOptions firestoreOptions =
FirestoreOptions.getDefaultInstance().toBuilder()
    .setProjectId(projectId)
    .build();
Firestore db = firestoreOptions.getService();

Accessing Firestore Object

For #1-3 the Firebase app is initialized right away in the application's main() method. Then, the Firestore object is accessed from an object.

FirebaseClient.Kt

object FirebaseClient {
    val firestore: Firestore
    init {
        firestore = FirestoreClient.getFirestore()
    }
}

For #4 the Firestore object is created in the Kotlin object's init{...} and stored in the object as a value.

FirebaseClient.Kt

object FirebaseClient {
    val firestore: Firestore

    init {
        val firestoreOptions = FirestoreOptions.getDefaultInstance().toBuilder()
            .setTimestampsInSnapshotsEnabled(true)
            .setProjectId("project-name")
            .build()
        firestore = firestoreOptions.service
    }
}

Writing to Firestore

FirebaseClient.firestore.collection(someCollection).document(someDocument).collection(anotherCollection).add(someObject)

4

1 回答 1

1

After utilizing Firebase authentication for a different project I discovered this is not an issue with Firebase authentication but rather with the application's main method. Therefore the various implementations above of Firebase authentication will work as expected when deployed to AppEngine.

Solution

I was expecting the application's main method to run once the app is deployed to AppEngine, similar to how an application's main method is called when ran in IntelliJ. However I realized main is only called once the app's hosted route is called.

ie: https://[yourProjectName].appspot.com

I've created a new StackOverflow post in order to determine how to run a Ktor app's main method automatically once deployed.

于 2018-12-01T21:08:18.447 回答