diff --git a/.github/workflows/package-ffi-engine.yml b/.github/workflows/package-ffi-engine.yml
index 6829721c..cd22d2d7 100644
--- a/.github/workflows/package-ffi-engine.yml
+++ b/.github/workflows/package-ffi-engine.yml
@@ -101,7 +101,7 @@ jobs:
key: ${{ runner.os }}-${{ matrix.platform.target}}-cargo-${{ hashFiles('**/Cargo.lock') }}
- name: Set RUSTFLAGS
- if: endsWith(matrix.platform.target, '-musl')
+ if: endsWith(matrix.platform.target, '-musl') || endsWith(matrix.platform.target, '-android')
run: |
echo "RUSTFLAGS=-C target-feature=-crt-static" >> $GITHUB_ENV
@@ -159,3 +159,22 @@ jobs:
tag_name: flipt-engine-ffi-${{ github.event.inputs.tag }}
files: |
flipt-engine-ffi-${{ matrix.platform.name }}${{ startsWith(matrix.platform.name, 'Windows') && '.zip' || '.tar.gz' }}
+
+ notify:
+ runs-on: ubuntu-latest
+ needs: build
+ steps:
+ - name: Generate token
+ id: generate_token
+ uses: tibdex/github-app-token@v2
+ with:
+ app_id: ${{ secrets.FLIPT_RELEASE_BOT_APP_ID }}
+ private_key: ${{ secrets.FLIPT_RELEASE_BOT_APP_PEM }}
+ installation_id: ${{ secrets.FLIPT_RELEASE_BOT_INSTALLATION_ID }}
+
+ - name: Trigger Test Android SDK
+ run: |
+ curl -X POST -H "Authorization: Bearer ${{ steps.generate_token.outputs.token }}" \
+ -H "Accept: application/vnd.github.v3+json" \
+ https://api.github.com/repos/flipt-io/flipt-client-sdks/actions/workflows/test-android-sdk.yml/dispatches \
+ -d '{"ref":"${{ github.head_ref }}", "inputs":{"caller_run_id":"${{ github.run_id }}"}}'
diff --git a/.github/workflows/test-android-sdk.yml b/.github/workflows/test-android-sdk.yml
index c3f17c56..2b569d6b 100644
--- a/.github/workflows/test-android-sdk.yml
+++ b/.github/workflows/test-android-sdk.yml
@@ -1,8 +1,10 @@
name: Test Android SDK
on:
- workflow_run:
- workflows: ["Package FFI Engine"]
- types: [completed]
+ workflow_dispatch:
+ inputs:
+ caller_run_id:
+ type: string
+ required: true
permissions:
contents: write
@@ -12,7 +14,17 @@ jobs:
test:
name: Integration Tests
runs-on: ubuntu-latest
+ timeout-minutes: 10
steps:
+ - name: Enable KVM group perms
+ run: |
+ echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
+ sudo udevadm control --reload-rules
+ sudo udevadm trigger --name-match=kvm
+
+ - name: Setup Gradle
+ uses: gradle/actions/setup-gradle@v3
+
- name: Checkout Sources
uses: actions/checkout@v4
@@ -21,21 +33,6 @@ jobs:
with:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- - name: Set up JDK 17
- uses: actions/setup-java@v4
- with:
- java-version: "17"
- distribution: "temurin"
-
- - name: Setup Android SDK
- uses: android-actions/setup-android@v3
- with:
- api-level: 33
- target: default
- arch: x86_64
- profile: default
- emulator-build: stable
-
- name: Generate token
id: generate_token
uses: tibdex/github-app-token@v2
@@ -49,59 +46,65 @@ jobs:
with:
github-token: ${{ steps.generate_token.outputs.token }}
name: flipt-engine-ffi-Android-x86_64.tar.gz
- path: flipt-engine-ffi-Android-x86_64.tar.gz
+ path: /tmp
+ run-id: ${{ inputs.caller_run_id }}
- name: Download Artifact (arm64)
uses: actions/download-artifact@v4
with:
github-token: ${{ steps.generate_token.outputs.token }}
name: flipt-engine-ffi-Android-arm64.tar.gz
- path: flipt-engine-ffi-Android-arm64.tar.gz
+ path: /tmp
+ run-id: ${{ inputs.caller_run_id }}
- name: Extract Artifacts
run: |
- tar -xzvf flipt-engine-ffi-Android-x86_64.tar.gz -C ./flipt-engine-ffi-Android-x86_64
- tar -xzvf flipt-engine-ffi-Android-arm64.tar.gz -C ./flipt-engine-ffi-Android-arm64
- ls -la ./flipt-engine-ffi-Android-x86_64
- ls -la ./flipt-engine-ffi-Android-arm64
-
- # - name: Run flipt
- # env:
- # FLIPT_STORAGE_TYPE: "local"
- # FLIPT_STORAGE_LOCAL_PATH: "./test/fixtures/testdata"
- # run: flipt&
+ mkdir -p /tmp/flipt-engine-ffi-Android-x86_64
+ mkdir -p /tmp/flipt-engine-ffi-Android-arm64
+ tar -xzvf /tmp/flipt-engine-ffi-Android-x86_64.tar.gz -C /tmp/flipt-engine-ffi-Android-x86_64
+ tar -xzvf /tmp/flipt-engine-ffi-Android-arm64.tar.gz -C /tmp/flipt-engine-ffi-Android-arm64
- # - name: Install System Image
- # run: |
- # echo "Installing system image..."
- # sdkmanager "system-images;android-33;google_apis;x86_64"
-
- # - name: Create Emulator
- # run: |
- # echo "Creating emulator..."
- # echo "no" | avdmanager create avd -n test -k "system-images;android-33;google_apis;x86_64" --device "pixel"
-
- # - name: Start Emulator
- # run: |
- # echo "Starting emulator..."
- # $ANDROID_HOME/emulator/emulator -avd test -no-audio -no-boot-anim -no-window -gpu swiftshader_indirect &
-
- # - name: Wait for Emulator to Boot
- # run: |
- # echo "Waiting for emulator to boot..."
- # adb wait-for-device
- # adb shell getprop init.svc.bootanim | grep -m 1 stopped
-
- # - name: Run Integration Tests
- # env:
- # FLIPT_URL: "http://0.0.0.0:8080"
- # FLIPT_AUTH_TOKEN: "secret"
- # run: |
- # cd ./flipt-client-kotlin-android
- # ./gradlew connectedAndroidTest
+ - name: Move Artifacts
+ run: |
+ mkdir -p flipt-client-kotlin-android/src/main/cpp/libs/x86_64
+ mkdir -p flipt-client-kotlin-android/src/main/cpp/libs/arm64-v8a
+ mkdir -p flipt-client-kotlin-android/src/main/cpp/include
+ mv /tmp/flipt-engine-ffi-Android-x86_64/target/x86_64-linux-android/release/libfliptengine.a flipt-client-kotlin-android/src/main/cpp/libs/x86_64/
+ mv /tmp/flipt-engine-ffi-Android-arm64/target/aarch64-linux-android/release/libfliptengine.a flipt-client-kotlin-android/src/main/cpp/libs/arm64-v8a/
+ cp -r flipt-engine-ffi/include/* flipt-client-kotlin-android/src/main/cpp/include
+
+ - name: Run Flipt
+ env:
+ FLIPT_STORAGE_TYPE: "local"
+ FLIPT_STORAGE_LOCAL_PATH: "./test/fixtures/testdata"
+ FLIPT_AUTHENTICATION_REQUIRED: true
+ FLIPT_AUTHENTICATION_METHODS_TOKEN_ENABLED: true
+ FLIPT_AUTHENTICATION_METHODS_TOKEN_BOOTSTRAP_TOKEN: "secret"
+ run: flipt&
+
+ - name: Wait for Flipt to be ready
+ run: |
+ while ! curl -s http://0.0.0.0:8080/health | grep -q "SERVING"; do
+ echo "Waiting for Flipt to be ready..."
+ sleep 1
+ done
+
+ - name: Run Integration Tests
+ uses: reactivecircus/android-emulator-runner@v2
+ env:
+ FLIPT_URL: "http://10.0.2.2:8080"
+ FLIPT_AUTH_TOKEN: "secret"
+ with:
+ api-level: 33
+ target: default
+ arch: x86_64
+ script: ./gradlew connectedAndroidTest
+ working-directory: ./flipt-client-kotlin-android
+ emulator-options: -no-window -no-snapshot -screen no-touch -noaudio -no-boot-anim -camera-back none
- # - name: Stop Emulator
- # if: always()
- # run: |
- # echo "Stopping emulator..."
- # adb emu kill
+ - name: (Fail-only) Upload the build reports
+ if: failure()
+ uses: actions/upload-artifact@v4
+ with:
+ name: build-reports
+ path: ./flipt-client-kotlin-android/build/reports
diff --git a/.gitignore b/.gitignore
index 2efb1967..4f0ee923 100644
--- a/.gitignore
+++ b/.gitignore
@@ -17,5 +17,4 @@ Cargo.lock
tmp
.envrc
.vscode
-staging
-*.h
\ No newline at end of file
+staging
\ No newline at end of file
diff --git a/flipt-client-kotlin-android/build.gradle b/flipt-client-kotlin-android/build.gradle
index 15f9562d..44e14155 100644
--- a/flipt-client-kotlin-android/build.gradle
+++ b/flipt-client-kotlin-android/build.gradle
@@ -17,18 +17,16 @@ android {
ndk {
abiFilters "x86_64", "arm64-v8a"
}
+ def fliptUrl = System.getenv("FLIPT_URL") ?: ""
+ def fliptAuthToken = System.getenv("FLIPT_AUTH_TOKEN") ?: ""
+ buildConfigField("String", "FLIPT_URL", "\"$fliptUrl\"")
+ buildConfigField("String", "FLIPT_AUTH_TOKEN", "\"$fliptAuthToken\"")
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
- buildConfigField("String", "FLIPT_URL", "\"${System.getenv("FLIPT_URL") ?: ""}\"")
- buildConfigField("String", "FLIPT_AUTH_TOKEN", "\"${System.getenv("FLIPT_AUTH_TOKEN") ?: ""}\"")
- }
- debug {
- buildConfigField("String", "FLIPT_URL", "\"${System.getenv("FLIPT_URL") ?: ""}\"")
- buildConfigField("String", "FLIPT_AUTH_TOKEN", "\"${System.getenv("FLIPT_AUTH_TOKEN") ?: ""}\"")
}
}
compileOptions {
@@ -38,6 +36,7 @@ android {
kotlinOptions {
jvmTarget = '1.8'
+ freeCompilerArgs += '-opt-in=kotlin.RequiresOptIn'
}
externalNativeBuild {
@@ -53,4 +52,4 @@ dependencies {
androidTestImplementation libs.androidx.junit
androidTestImplementation libs.androidx.espresso.core
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.7.20"
-}
\ No newline at end of file
+}
diff --git a/flipt-client-kotlin-android/src/androidTest/AndroidManifest.xml b/flipt-client-kotlin-android/src/androidTest/AndroidManifest.xml
new file mode 100644
index 00000000..871dce41
--- /dev/null
+++ b/flipt-client-kotlin-android/src/androidTest/AndroidManifest.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/flipt-client-kotlin-android/src/androidTest/java/io/flipt/client/TestFliptEvaluationClient.kt b/flipt-client-kotlin-android/src/androidTest/java/io/flipt/client/TestFliptEvaluationClient.kt
index e0fd0015..1c53a699 100644
--- a/flipt-client-kotlin-android/src/androidTest/java/io/flipt/client/TestFliptEvaluationClient.kt
+++ b/flipt-client-kotlin-android/src/androidTest/java/io/flipt/client/TestFliptEvaluationClient.kt
@@ -6,7 +6,6 @@ import org.junit.After
import org.junit.Before
import org.junit.Test
-
class TestFliptEvaluationClient {
private var fliptClient: FliptEvaluationClient? = null
@@ -15,14 +14,16 @@ class TestFliptEvaluationClient {
fun initAll() {
val fliptURL = BuildConfig.FLIPT_URL
val clientToken = BuildConfig.FLIPT_AUTH_TOKEN
-
+ assert("http://10.0.2.2:8080" == fliptURL)
assert(!fliptURL.isEmpty())
assert(!clientToken.isEmpty())
- fliptClient = FliptEvaluationClient.builder()
- .url(url = fliptURL)
- .namespace("default")
- .authentication(ClientTokenAuthentication(clientToken))
- .build()
+ fliptClient =
+ FliptEvaluationClient
+ .builder()
+ .url(url = fliptURL)
+ .namespace("default")
+ .authentication(ClientTokenAuthentication(clientToken))
+ .build()
}
@Test
@@ -31,7 +32,7 @@ class TestFliptEvaluationClient {
val context: MutableMap = HashMap()
context["fizz"] = "buzz"
- val response = fliptClient?.evaluateVariant("flag1", "entity", context)
+ val response = fliptClient?.evaluateVariant("flag1", "entity", context)
assert("flag1" == response?.flagKey)
assert(response?.match ?: false)
@@ -59,11 +60,12 @@ class TestFliptEvaluationClient {
val context: MutableMap = HashMap()
context["fizz"] = "buzz"
- val evalRequests: Array = arrayOf(
- EvaluationRequest("flag1", "entity", context),
- EvaluationRequest("flag_boolean", "entity", context),
- EvaluationRequest("notfound", "entity", context)
- )
+ val evalRequests: Array =
+ arrayOf(
+ EvaluationRequest("flag1", "entity", context),
+ EvaluationRequest("flag_boolean", "entity", context),
+ EvaluationRequest("notfound", "entity", context),
+ )
val response = fliptClient?.evaluateBatch(evalRequests)
@@ -71,7 +73,7 @@ class TestFliptEvaluationClient {
val responses = response?.responses
assert(responses?.get(0)?.variantEvaluationResponse != null)
- val variantResponse = responses?.get(0)?.variantEvaluationResponse
+ val variantResponse = responses?.get(0)?.variantEvaluationResponse
assert("flag1" == variantResponse?.flagKey)
assert(variantResponse?.match ?: false)
assert("MATCH_EVALUATION_REASON" == variantResponse?.reason)
@@ -103,5 +105,4 @@ class TestFliptEvaluationClient {
fun tearDownAll() {
fliptClient?.close()
}
-
}
diff --git a/flipt-client-kotlin-android/src/androidTest/res/xml/network_security_config.xml b/flipt-client-kotlin-android/src/androidTest/res/xml/network_security_config.xml
new file mode 100644
index 00000000..39817683
--- /dev/null
+++ b/flipt-client-kotlin-android/src/androidTest/res/xml/network_security_config.xml
@@ -0,0 +1,7 @@
+
+
+
+ localhost
+ 10.0.2.2
+
+
diff --git a/flipt-client-kotlin-android/src/main/AndroidManifest.xml b/flipt-client-kotlin-android/src/main/AndroidManifest.xml
index e0c2f049..19d2638e 100644
--- a/flipt-client-kotlin-android/src/main/AndroidManifest.xml
+++ b/flipt-client-kotlin-android/src/main/AndroidManifest.xml
@@ -1,6 +1,4 @@
-
-
-
\ No newline at end of file
+
diff --git a/flipt-client-kotlin-android/src/main/cpp/include/flipt_engine.h b/flipt-client-kotlin-android/src/main/cpp/include/flipt_engine.h
new file mode 100644
index 00000000..604169ef
--- /dev/null
+++ b/flipt-client-kotlin-android/src/main/cpp/include/flipt_engine.h
@@ -0,0 +1,54 @@
+#include
+#include
+#include
+#include
+
+/**
+ * # Safety
+ *
+ * This function will initialize an Engine and return a pointer back to the caller.
+ */
+void *initialize_engine(const char *namespace_, const char *opts);
+
+/**
+ * # Safety
+ *
+ * This function will take in a pointer to the engine and return a variant evaluation response.
+ */
+const char *evaluate_variant(void *engine_ptr, const char *evaluation_request);
+
+/**
+ * # Safety
+ *
+ * This function will take in a pointer to the engine and return a boolean evaluation response.
+ */
+const char *evaluate_boolean(void *engine_ptr, const char *evaluation_request);
+
+/**
+ * # Safety
+ *
+ * This function will take in a pointer to the engine and return a batch evaluation response.
+ */
+const char *evaluate_batch(void *engine_ptr, const char *batch_evaluation_request);
+
+/**
+ * # Safety
+ *
+ * This function will take in a pointer to the engine and return a list of flags for the given namespace.
+ */
+const char *list_flags(void *engine_ptr);
+
+/**
+ * # Safety
+ *
+ * This function will free the memory occupied by the engine.
+ */
+void destroy_engine(void *engine_ptr);
+
+/**
+ * # Safety
+ *
+ * This function will take in a pointer to the string and free the memory.
+ * See Rust the safety section in CString::from_raw.
+ */
+void destroy_string(char *ptr);
diff --git a/flipt-client-kotlin-android/src/main/java/io/flipt/client/FliptEvaluationClient.kt b/flipt-client-kotlin-android/src/main/java/io/flipt/client/FliptEvaluationClient.kt
index 623b0694..9070d84b 100644
--- a/flipt-client-kotlin-android/src/main/java/io/flipt/client/FliptEvaluationClient.kt
+++ b/flipt-client-kotlin-android/src/main/java/io/flipt/client/FliptEvaluationClient.kt
@@ -17,10 +17,10 @@ import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import kotlin.time.Duration
-
class FliptEvaluationClient(namespace: String, options: ClientOptions) {
private var engine: Long = 0
private val json = Json {
+ encodeDefaults = false
ignoreUnknownKeys = true
}
diff --git a/flipt-client-kotlin-android/src/main/java/io/flipt/client/models/AuthenticationStrategy.kt b/flipt-client-kotlin-android/src/main/java/io/flipt/client/models/AuthenticationStrategy.kt
index 134da342..2e30e66f 100644
--- a/flipt-client-kotlin-android/src/main/java/io/flipt/client/models/AuthenticationStrategy.kt
+++ b/flipt-client-kotlin-android/src/main/java/io/flipt/client/models/AuthenticationStrategy.kt
@@ -3,23 +3,68 @@ package io.flipt.client.models
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
-import kotlinx.serialization.json.JsonClassDiscriminator
+import kotlinx.serialization.KSerializer
+import kotlinx.serialization.SerializationException
+import kotlinx.serialization.json.JsonEncoder
+import kotlinx.serialization.json.JsonDecoder
+import kotlinx.serialization.json.JsonObject
+import kotlinx.serialization.json.jsonPrimitive
+import kotlinx.serialization.json.JsonPrimitive
+import kotlinx.serialization.encoding.Encoder
+import kotlinx.serialization.encoding.Decoder
+import kotlinx.serialization.descriptors.SerialDescriptor
+import kotlinx.serialization.descriptors.element
+import kotlinx.serialization.descriptors.buildClassSerialDescriptor
@OptIn(ExperimentalSerializationApi::class)
-@Serializable
-@JsonClassDiscriminator("type")
+@Serializable(with = AuthenticationStrategySerializer::class)
sealed class AuthenticationStrategy
@Serializable
-@SerialName("client_token")
data class ClientTokenAuthentication(
@SerialName("client_token")
- val clientToken: String
+ val clientToken: String,
) : AuthenticationStrategy()
@Serializable
-@SerialName("jwt_token")
data class JWTAuthentication(
@SerialName("jwt_token")
- val jwtToken: String
+ val jwtToken: String,
) : AuthenticationStrategy()
+
+object AuthenticationStrategySerializer : KSerializer {
+ override val descriptor: SerialDescriptor = buildClassSerialDescriptor("AuthenticationStrategy") {
+ element("type", isOptional = true) // Optional type for identification if needed
+ element("client_token", isOptional = true)
+ element("jwt_token", isOptional = true)
+ }
+
+ override fun serialize(encoder: Encoder, value: AuthenticationStrategy) {
+ val jsonEncoder = encoder as? JsonEncoder
+ ?: throw SerializationException("This serializer only supports JSON encoding")
+
+ val jsonObject = when (value) {
+ is ClientTokenAuthentication -> JsonObject(mapOf("client_token" to JsonPrimitive(value.clientToken)))
+ is JWTAuthentication -> JsonObject(mapOf("jwt_token" to JsonPrimitive(value.jwtToken)))
+ }
+ jsonEncoder.encodeJsonElement(jsonObject)
+ }
+
+ override fun deserialize(decoder: Decoder): AuthenticationStrategy {
+ val jsonDecoder = decoder as? JsonDecoder
+ ?: throw SerializationException("This serializer only supports JSON decoding")
+
+ val jsonElement = jsonDecoder.decodeJsonElement() as? JsonObject
+ ?: throw SerializationException("Expected JSON object")
+
+ return when {
+ "client_token" in jsonElement -> ClientTokenAuthentication(
+ clientToken = jsonElement["client_token"]!!.jsonPrimitive.content
+ )
+ "jwt_token" in jsonElement -> JWTAuthentication(
+ jwtToken = jsonElement["jwt_token"]!!.jsonPrimitive.content
+ )
+ else -> throw SerializationException("Unknown authentication strategy")
+ }
+ }
+}
diff --git a/flipt-client-kotlin-android/src/test/java/io/flipt/client/TestOptionsEncoding.kt b/flipt-client-kotlin-android/src/test/java/io/flipt/client/TestOptionsEncoding.kt
new file mode 100644
index 00000000..6115ab18
--- /dev/null
+++ b/flipt-client-kotlin-android/src/test/java/io/flipt/client/TestOptionsEncoding.kt
@@ -0,0 +1,27 @@
+package io.flipt.client
+import io.flipt.client.models.AuthenticationStrategy
+import io.flipt.client.models.ClientTokenAuthentication
+import io.flipt.client.models.JWTAuthentication
+import kotlinx.serialization.encodeToString
+import kotlinx.serialization.json.Json
+import org.junit.Assert.assertEquals
+import org.junit.Test
+
+class TestOptionsEncoding {
+ @Test
+ fun testEncodeAuthenticationStrategy() {
+ val json =
+ Json {
+ encodeDefaults = false
+ ignoreUnknownKeys = true
+ }
+ var clientAuth: AuthenticationStrategy = ClientTokenAuthentication("clientToken")
+ var encoded = json.encodeToString(clientAuth)
+ var expected = """{"client_token":"clientToken"}"""
+ assertEquals("output doesn't match expected", expected, encoded)
+ var jwtAuth = JWTAuthentication("jwt")
+ encoded = json.encodeToString(jwtAuth)
+ expected = """{"jwt_token":"jwt"}"""
+ assertEquals("output doesn't match expected", expected, encoded)
+ }
+}