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) + } +}