From fc665accbcc9f0e06ffec17bcf2e7ad82dfc56d7 Mon Sep 17 00:00:00 2001 From: Nikola Irinchev Date: Mon, 28 Mar 2022 13:03:35 +0200 Subject: [PATCH] Add jobs to deploy and cleanup baas on cloud-dev (#309) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Kasper Overgård Nielsen --- .github/workflows/pr.yml | 162 ++++++++++ .github/workflows/realm-dart-linux.yml | 60 ---- .github/workflows/realm-flutter-ios.yml | 4 +- .github/workflows/realm-flutter-linux.yml | 47 --- .github/workflows/realm-flutter-macos.yml | 2 +- CHANGELOG.md | 31 +- bin/README.md | 14 +- flutter/realm_flutter/pubspec.yaml | 3 +- flutter/realm_flutter/tests/pubspec.yaml | 3 +- lib/src/cli/deployapps/baas_client.dart | 305 ++++++++++++++++++ .../cli/deployapps/deployapps_command.dart | 78 +++++ lib/src/cli/deployapps/options.dart | 47 +++ lib/src/cli/deployapps/options.g.dart | 48 +++ lib/src/cli/main.dart | 2 + pubspec.yaml | 3 +- test/configuration_test.dart | 2 +- test/list_test.dart | 2 +- test/realm_object_test.dart | 2 +- test/realm_test.dart | 2 +- test/results_test.dart | 2 +- test/test.dart | 25 +- 21 files changed, 706 insertions(+), 138 deletions(-) create mode 100644 .github/workflows/pr.yml delete mode 100644 .github/workflows/realm-dart-linux.yml delete mode 100644 .github/workflows/realm-flutter-linux.yml create mode 100644 lib/src/cli/deployapps/baas_client.dart create mode 100644 lib/src/cli/deployapps/deployapps_command.dart create mode 100644 lib/src/cli/deployapps/options.dart create mode 100644 lib/src/cli/deployapps/options.g.dart diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml new file mode 100644 index 000000000..d17cf058d --- /dev/null +++ b/.github/workflows/pr.yml @@ -0,0 +1,162 @@ +name: PR Build + +on: + push: + branches: + - master + tags: + - '[0-9]+.[0-9]+.[0-9]+**' # matches tags like number(s).number(s).number(s)(any) for ex: 1.0.0 and also 1.0.0+beta + pull_request: +env: + REALM_CI: true + +jobs: + baas-linux: + runs-on: ubuntu-latest + name: BaaS Linux + outputs: + clusterName: ${{ steps.deploy-mdb-apps.outputs.clusterName }} + timeout-minutes: 20 + steps: + - name: Checkout + uses: actions/checkout@v2 + with: + submodules: false + - uses: realm/ci-actions/mdb-realm/deployApps@fac1d6958f03d71de743305ce3ab27594efbe7b7 + id: deploy-mdb-apps + with: + projectId: ${{ secrets.ATLAS_PROJECT_ID }} + apiKey: ${{ secrets.ATLAS_PUBLIC_API_KEY }} + privateApiKey: ${{ secrets.ATLAS_PRIVATE_API_KEY }} + differentiator: dart-linux + - name : Setup Dart SDK + uses: dart-lang/setup-dart@main + with: + sdk: stable + - name: Deploy Apps + run: | + dart run realm_dart deploy-apps \ + --baas-url https://realm-dev.mongodb.com \ + --atlas-cluster ${{ steps.deploy-mdb-apps.outputs.clusterName }} \ + --api-key ${{ secrets.ATLAS_PUBLIC_API_KEY }} \ + --private-api-key ${{ secrets.ATLAS_PRIVATE_API_KEY }} \ + --project-id ${{ secrets.ATLAS_PROJECT_ID }} + build-linux: + runs-on: ubuntu-latest + name: Build Linux + steps: + - name: Checkout + uses: actions/checkout@v2 + with: + submodules: 'recursive' + - name: Setup Ninja + uses: seanmiddleditch/gha-setup-ninja@master + - name: Build Realm Dart for Linux + run: ./scripts/build-linux.sh + - name: Store artifacts + uses: actions/upload-artifact@v2 + with: + name: librealm-linux + path: binary/** + retention-days: 1 + + tests-linux: + runs-on: ubuntu-latest + name: Tests Linux + env: + BAAS_URL: https://realm-dev.mongodb.com + BAAS_CLUSTER: ${{ needs.baas-linux.outputs.clusterName }} + BAAS_API_KEY: ${{ secrets.ATLAS_PUBLIC_API_KEY }} + BAAS_PRIVATE_API_KEY: ${{ secrets.ATLAS_PRIVATE_API_KEY }} + BAAS_PROJECT_ID: ${{ secrets.ATLAS_PROJECT_ID}} + needs: + - baas-linux + - build-linux + steps: + - name: Checkout + uses: actions/checkout@v2 + with: + submodules: false + - name: Fetch artifacts + uses: actions/download-artifact@v2 + with: + name: librealm-linux + path: binary + - name : Setup Dart SDK + uses: dart-lang/setup-dart@main + with: + sdk: stable + - name: Install dependencies + run: dart pub get + - name: Run tests + run: | + dart test -r expanded -j 1 --test-randomize-ordering-seed random + + # TODO: these should go away once we have a proper release workflow + - name: Archive binary + if: ${{ success() && github.event_name == 'push' && github.ref_type == 'tag' }} + run: | + dart run realm_dart archive --source-dir $(pwd)/binary/linux --output-file $ARCHIVE_PATH + - name: Release artifacts + if: ${{ success() && github.event_name == 'push' && github.ref_type == 'tag' }} + uses: ncipollo/release-action@v1 + with: + allowUpdates: true + artifactErrorsFailBuild: true + draft: true + omitNameDuringUpdate: true + prerelease: false + omitPrereleaseDuringUpdate: true + artifacts: ${{ env.ARCHIVE_PATH }} + body: "ADD RELEASE NOTES" + omitBodyDuringUpdate: true + token: ${{ secrets.GITHUB_TOKEN }} + + flutter-linux: + runs-on: ubuntu-latest + name: Flutter Tests Linux + needs: + - build-linux + steps: + - name: Checkout + uses: actions/checkout@v2 + with: + submodules: false + - name: Setup GTK + run: | + sudo apt-get update -y + sudo apt-get install -y libgtk-3-dev xvfb + - name: Setup Ninja + uses: seanmiddleditch/gha-setup-ninja@master + - name: Fetch artifacts + uses: actions/download-artifact@v2 + with: + name: librealm-linux + path: binary + - name: Setup Flutter + uses: subosito/flutter-action@v2 + with: + channel: 'stable' + - name: Enable Flutter Desktop support + run: flutter config --enable-linux-desktop + - name: Install dependencies + run: flutter pub get + - name: Run tests + run: xvfb-run flutter drive -d linux --target=test_driver/app.dart --suppress-analytics --dart-entrypoint-args="" #--verbose #-a="Some test name" + working-directory: ./flutter/realm_flutter/tests + + + cleanup-linux: + runs-on: ubuntu-latest + name: Cleanup Linux + needs: + - tests-linux + if: always() + timeout-minutes: 5 + steps: + - uses: realm/ci-actions/mdb-realm/cleanup@fac1d6958f03d71de743305ce3ab27594efbe7b7 + with: + projectId: ${{ secrets.ATLAS_PROJECT_ID}} + apiKey: ${{ secrets.ATLAS_PUBLIC_API_KEY}} + privateApiKey: ${{ secrets.ATLAS_PRIVATE_API_KEY }} + differentiator: dart-linux diff --git a/.github/workflows/realm-dart-linux.yml b/.github/workflows/realm-dart-linux.yml deleted file mode 100644 index c8fb5d226..000000000 --- a/.github/workflows/realm-dart-linux.yml +++ /dev/null @@ -1,60 +0,0 @@ -name: Realm Dart for Linux - -on: - push: - branches: - - master - tags: - - '[0-9]+.[0-9]+.[0-9]+**' # matches tags like number(s).number(s).number(s)(any) for ex: 1.0.0 and also 1.0.0+beta - pull_request: - -jobs: - CI: - runs-on: ubuntu-latest - env: - REALM_CI: true - - steps: - - name: Checkout - uses: actions/checkout@v2 - with: - submodules: 'recursive' - - - name: Setup Ninja - uses: seanmiddleditch/gha-setup-ninja@master - - - name: Build Realm Dart for Linux - run: ./scripts/build-linux.sh - - - name : Setup Dart SDK - uses: dart-lang/setup-dart@main - with: - sdk: stable - - - name: Install dependencies - run: dart pub get - - - name: Run tests - run: | - dart test -r expanded -j 1 --test-randomize-ordering-seed random - echo "ARCHIVE_PATH=$(pwd)/binary/linux.tar.gz" >> $GITHUB_ENV - - - name: Archive binary - if: ${{ success() && github.event_name == 'push' && github.ref_type == 'tag' }} - run: | - dart run realm_dart archive --source-dir $(pwd)/binary/linux --output-file $ARCHIVE_PATH - - - name: Release artifacts - if: ${{ success() && github.event_name == 'push' && github.ref_type == 'tag' }} - uses: ncipollo/release-action@v1 - with: - allowUpdates: true - artifactErrorsFailBuild: true - draft: true - omitNameDuringUpdate: true - prerelease: false - omitPrereleaseDuringUpdate: true - artifacts: ${{ env.ARCHIVE_PATH }} - body: "ADD RELEASE NOTES" - omitBodyDuringUpdate: true - token: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/realm-flutter-ios.yml b/.github/workflows/realm-flutter-ios.yml index faac106a3..845dea4c7 100644 --- a/.github/workflows/realm-flutter-ios.yml +++ b/.github/workflows/realm-flutter-ios.yml @@ -48,7 +48,7 @@ jobs: uses: subosito/flutter-action@v2 with: channel: 'stable' - + - name: Install dependencies run: flutter pub get @@ -87,5 +87,5 @@ jobs: artifacts: ${{ env.ARCHIVE_PATH }} body: "ADD RELEASE NOTES" omitBodyDuringUpdate: true - token: ${{ secrets.GITHUB_TOKEN }} + token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/realm-flutter-linux.yml b/.github/workflows/realm-flutter-linux.yml deleted file mode 100644 index 5116106d9..000000000 --- a/.github/workflows/realm-flutter-linux.yml +++ /dev/null @@ -1,47 +0,0 @@ -name: Realm Flutter for Linux - -on: - push: - branches: - - master - tags: - - '[0-9]+.[0-9]+.[0-9]+**' # matches tags like number(s).number(s).number(s)(any) for ex: 1.0.0 and also 1.0.0+beta - pull_request: - -jobs: - CI: - runs-on: ubuntu-latest - env: - REALM_CI: true - - steps: - - name: Checkout - uses: actions/checkout@v2 - with: - submodules: 'recursive' - - - name: Setup GTK - run: | - sudo apt-get update -y - sudo apt-get install -y libgtk-3-dev xvfb - - - name: Setup Ninja - uses: seanmiddleditch/gha-setup-ninja@master - - - name: Build Realm Flutter for Linux - run: ./scripts/build-linux.sh - - - name: Setup Flutter - uses: subosito/flutter-action@v2 - with: - channel: 'stable' - - - name: Enable Flutter Desktop support - run: flutter config --enable-linux-desktop - - - name: Install dependencies - run: flutter pub get - - - name: Run tests - run: xvfb-run flutter drive -d linux --target=test_driver/app.dart --suppress-analytics --dart-entrypoint-args="" #--verbose #-a="Some test name" - working-directory: ./flutter/realm_flutter/tests diff --git a/.github/workflows/realm-flutter-macos.yml b/.github/workflows/realm-flutter-macos.yml index e882a093e..b5ded4885 100644 --- a/.github/workflows/realm-flutter-macos.yml +++ b/.github/workflows/realm-flutter-macos.yml @@ -37,4 +37,4 @@ jobs: - name: Run tests run: flutter drive -d macos --target=test_driver/app.dart --suppress-analytics --dart-entrypoint-args="" #--verbose #-a="Some test name" working-directory: ./flutter/realm_flutter/tests - + diff --git a/CHANGELOG.md b/CHANGELOG.md index 79779b3cb..d9b176ff8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,8 +4,17 @@ x.x.x Release notes (yyyy-MM-dd) **This project is in the Alpha stage. All API's might change without warning and no guarantees are given about stability. Do not use it in production.** ### Enhancements -Support result value from write transaction callbacks ([#294](https://github.com/realm/realm-dart/pull/294/)) +* Support result value from write transaction callbacks ([#294](https://github.com/realm/realm-dart/pull/294/)) +### Fixed +* None + +### Compatibility +* Dart ^2.15 on Windows, MacOS and Linux + +### Internal +* Added a command to deploy a MongoDB Realm app to `realm_dart`. Usage: `dart run realm_dart deploy-apps`. By default it will deploy apps to `http://localhost:9090` which is the endpoint of the local docker image. If `--atlas-cluster` is provided, it will authenticate, create an application and link the provided cluster to it. (PR [#309](https://github.com/realm/realm-dart/pull/309)) +* Unit tests will now attempt to lookup and create if necessary MongoDB applications (similarly to the above mentioned command). See `test.dart/setupBaas()` for the environment variables that control the Url and Atlas Cluster that will be used. If the `BAAS_URL` environment variable is not set, no apps will be imported and sync tests will not run. (PR [#309](https://github.com/realm/realm-dart/pull/309)) 0.2.1+alpha Release notes (2022-03-20) ============================================================== @@ -14,7 +23,7 @@ Support result value from write transaction callbacks ([#294](https://github.com ### Enhancements * Support change notifications on query results. ([#208](https://github.com/realm/realm-dart/pull/208)) - + Every `RealmResults` object now has a `changes` method returning a `Stream>` which can be listened to. ```dart @@ -79,7 +88,7 @@ Support result value from write transaction callbacks ([#294](https://github.com * Added support for opening in-memory realms. ([#280](https://github.com/realm/realm-dart/pull/280)) * Primary key fields no longer required to be `final` in data model classes ([#240](https://github.com/realm/realm-dart/pull/240)) - Previously primary key fields needed to be `final`. + Previously primary key fields needed to be `final`. ```dart @RealmModel() @@ -102,7 +111,7 @@ Support result value from write transaction callbacks ([#294](https://github.com * List fields no longer required to be `final` in data model classes. ([#253](https://github.com/realm/realm-dart/pull/253)) - Previously list fields needed to be `final`. + Previously list fields needed to be `final`. ```dart @RealmModel() @@ -136,7 +145,7 @@ Support result value from write transaction callbacks ([#294](https://github.com **This project is in the Alpha stage. All API's might change without warning and no guarantees are given about stability. Do not use it in production.** -### Enhancements +### Enhancements * Completely rewritten from the ground up with sound null safety and using Dart FFI ### Compatibility @@ -145,9 +154,9 @@ Support result value from write transaction callbacks ([#294](https://github.com 0.2.0-alpha.2 Release notes (2022-01-29) ============================================================== -Notes: This release is a prerelease version. All API's might change without warning and no guarantees are given about stability. +Notes: This release is a prerelease version. All API's might change without warning and no guarantees are given about stability. -### Enhancements +### Enhancements * Completеly rewritten from the ground up with sound null safety and using Dart FFI ### Fixed @@ -159,9 +168,9 @@ Notes: This release is a prerelease version. All API's might change without warn 0.2.0-alpha.1 Release notes (2022-01-29) ============================================================== -Notes: This release is a prerelease version. All API's might change without warning and no guarantees are given about stability. +Notes: This release is a prerelease version. All API's might change without warning and no guarantees are given about stability. -### Enhancements +### Enhancements * Completеly rewritten from the ground up with sound null safety and using Dart FFI ### Fixed @@ -176,9 +185,9 @@ Notes: This release is a prerelease version. All API's might change without warn 0.2.0-alpha Release notes (2022-01-27) ============================================================== -Notes: This release is a prerelease version. All API's might change without warning and no guarantees are given about stability. +Notes: This release is a prerelease version. All API's might change without warning and no guarantees are given about stability. -### Enhancements +### Enhancements * Completеly rewritten from the ground up with sound null safety and using Dart FFI ### Compatibility diff --git a/bin/README.md b/bin/README.md index cadb536c3..45e26943a 100644 --- a/bin/README.md +++ b/bin/README.md @@ -1,10 +1,10 @@ -Dart packages should distribute their prebuilt native extension binaries (if any) in the lib directory of the package. -This is where Dart VM looks for the binary when an application loads the Dart package. -On Windows Dart VM have a bug which looks for the package with incorrect path. -For Realm Dart this is something like C:\C:\\realm_dart\lib\src\realm_dart.dartrealm_dart_extension.dll. -The name of the native extension binary and the device letter is incorreth path on Windows. +Dart packages should distribute their prebuilt native extension binaries (if any) in the lib directory of the package. +This is where Dart VM looks for the binary when an application loads the Dart package. +On Windows Dart VM have a bug which looks for the package with incorrect path. +For Realm Dart this is something like C:\C:\\realm_dart\lib\src\realm_dart.dartrealm_dart_extension.dll. +The name of the native extension binary and the device letter is incorrect path on Windows. To workaround this bug Realm Dart package has a script in its binary directory which copies the native extension to the correct place on Windows -When the Dart VM bug is fixed Realm Dart package should move the native extension dll to its lib directory and this script will not be needed. +When the Dart VM bug is fixed Realm Dart package should move the native extension dll to its lib directory and this script will not be needed. -In order to use the Realm Dart package application developers should execute `pub run realm_dart install` from the root directory of their Dart application. \ No newline at end of file +In order to use the Realm Dart package application developers should execute `pub run realm_dart install` from the root directory of their Dart application. \ No newline at end of file diff --git a/flutter/realm_flutter/pubspec.yaml b/flutter/realm_flutter/pubspec.yaml index eaf216173..4f06f64eb 100644 --- a/flutter/realm_flutter/pubspec.yaml +++ b/flutter/realm_flutter/pubspec.yaml @@ -28,7 +28,7 @@ dependencies: pub_semver: ^2.1.0 realm_common: path: ../../common - realm_generator: + realm_generator: path: ../../generator tar: ^0.5.4 build_runner: ^2.1.0 @@ -39,6 +39,7 @@ dev_dependencies: build_cli: ^2.1.3 json_serializable: ^6.1.0 lints: ^1.0.1 + http: ^0.13.4 flutter: plugin: diff --git a/flutter/realm_flutter/tests/pubspec.yaml b/flutter/realm_flutter/tests/pubspec.yaml index 648629ca4..9aa733a92 100644 --- a/flutter/realm_flutter/tests/pubspec.yaml +++ b/flutter/realm_flutter/tests/pubspec.yaml @@ -26,9 +26,10 @@ dev_dependencies: flutter_lints: ^1.0.4 build_runner: ^2.1.2 test: ^1.20.1 + http: ^0.13.4 # Remove once flutter_driver from sdk is updated -dependency_overrides: +dependency_overrides: test_api: ^0.4.9 flutter: diff --git a/lib/src/cli/deployapps/baas_client.dart b/lib/src/cli/deployapps/baas_client.dart new file mode 100644 index 000000000..0ea01bfa0 --- /dev/null +++ b/lib/src/cli/deployapps/baas_client.dart @@ -0,0 +1,305 @@ +//////////////////////////////////////////////////////////////////////////////// +// +// Copyright 2022 Realm Inc. +// +// 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. +// +//////////////////////////////////////////////////////////////////////////////// + +import 'package:http/http.dart' as http; +import 'dart:convert'; + +class BaasClient { + static const String _confirmFuncSource = '''exports = ({ token, tokenId, username }) => { + // process the confirm token, tokenId and username + if (username.includes("realm_tests_do_autoverify")) { + return { status: 'success' } + } + // do not confirm the user + return { status: 'fail' }; + };'''; + + static const String _resetFuncSource = '''exports = ({ token, tokenId, username, password }) => { + // process the reset token, tokenId, username and password + if (password.includes("realm_tests_do_reset")) { + return { status: 'success' }; + } + // will not reset the password + return { status: 'fail' }; + };'''; + + final String _baseUrl; + final String? _clusterName; + final String _appSuffix; + final Map _headers; + + late String _groupId; + + BaasClient._(String baseUrl, [this._clusterName]) + : _baseUrl = '$baseUrl/api/admin/v3.0', + _headers = {'Accept': 'application/json'}, + _appSuffix = '-$_clusterName'; + + /// A client that imports apps in a MongoDB Realm docker image. See https://github.com/realm/ci/tree/master/realm/docker/mongodb-realm + /// for instructions on how to set it up. + /// @nodoc + static Future docker(String baseUrl) async { + final result = BaasClient._(baseUrl); + + await result._authenticate('local-userpass', '{ "username": "unique_user@domain.com", "password": "password" }'); + + dynamic groupDoc = await result._get('auth/profile'); + result._groupId = (groupDoc['roles'] as List)[0]['group_id'] as String; + + print('Current GroupID ${result._groupId}'); + + return result; + } + + /// A client that imports apps to an MongoDB Realm environment (typically realm-dev or realm-qa). + /// @nodoc + static Future atlas(String baseUrl, String cluster, String apiKey, String privateApiKey, String groupId) async { + final BaasClient result = BaasClient._(baseUrl, cluster); + + await result._authenticate('mongodb-cloud', '{ "username": "$apiKey", "apiKey": "$privateApiKey" }'); + + result._groupId = groupId; + + return result; + } + + /// Tries to look up the applications for the specified cluster. For [docker] client, returns all apps, + /// for [atlas] one, it will return only apps with suffix equal to the cluster name. If no apps exist, + /// then it will create the test applications and return them. + /// @nodoc + Future> getOrCreateApps() async { + final result = {}; + var apps = await _getApps(); + if (apps.isNotEmpty) { + for (final app in apps) { + result[app.name] = app; + } + } else { + final defaultApp = await _createApp('flexible'); + + result[defaultApp.name] = defaultApp; + + // Add more types of apps as we add more tests here. + } + + return result; + } + + Future> _getApps() async { + final apps = await _get('groups/$_groupId/apps') as List; + return apps + .map((dynamic doc) { + final name = doc['name'] as String; + if (!name.endsWith(_appSuffix)) { + return null; + } + + final appName = name.substring(0, name.length - _appSuffix.length); + return BaasApp(doc['_id'] as String, doc['client_app_id'] as String, appName); + }) + .where((doc) => doc != null) + .map((doc) => doc!) + .toList(); + } + + Future _createApp(String name) async { + print('Creating app $name'); + + final dynamic doc = await _post('groups/$_groupId/apps', '{ "name": "$name$_appSuffix" }'); + final appId = doc['_id'] as String; + final clientAppId = doc['client_app_id'] as String; + + final app = BaasApp(appId, clientAppId, name); + + final confirmFuncId = await _createFunction(app, 'confirmFunc', _confirmFuncSource); + final resetFuncId = await _createFunction(app, 'resetFunc', _resetFuncSource); + + enableProvider(app, 'anon-user'); + enableProvider(app, 'local-userpass', '''{ + "autoConfirm": false, + "confirmEmailSubject": "", + "confirmationFunctionName": "confirmFunc", + "confirmationFunctionId": "$confirmFuncId", + "emailConfirmationUrl": "http://localhost/confirmEmail", + "resetFunctionName": "resetFunc", + "resetFunctionId": "$resetFuncId", + "resetPasswordSubject": "", + "resetPasswordUrl": "http://localhost/resetPassword", + "runConfirmationFunction": true, + "runResetFunction": true + }'''); + + await _createMongoDBService(app, '''{ + "flexible_sync": { + "state": "enabled", + "database_name": "flexible_sync_data", + "queryable_fields_names": ["TODO"], + "permissions": { + "rules": {}, + "defaultRoles": [ + { + "name": "all", + "applyWhen": {}, + "read": true, + "write": true + } + ] + } + } + }'''); + + await _put('groups/$_groupId/apps/$app/sync/config', '{ "development_mode_enabled": true }'); + + return app; + } + + Future enableProvider(BaasApp app, String type, [String config = '{}']) async { + print('Enabling provider $type for ${app.name}'); + + final url = 'groups/$_groupId/apps/$app/auth_providers'; + if (type == 'api-key') { + final providers = await _get(url) as List; + final apiKeyProviderId = providers.singleWhere((dynamic doc) => doc['type'] == 'api-key')['_id'] as String; + + await _put('$url/$apiKeyProviderId/enable', '{}'); + } else { + await _post(url, '''{ + "name": "$type", + "type": "$type", + "disabled": false, + "config": $config + }'''); + } + } + + Future _authenticate(String provider, String credentials) async { + dynamic response = await _post('auth/providers/$provider/login', credentials); + + _headers['Authorization'] = "Bearer ${response['access_token']}"; + } + + Future _createFunction(BaasApp app, String name, String source) async { + print('Creating function $name for ${app.name}...'); + + final dynamic response = await _post('groups/$_groupId/apps/$app/functions', '''{ + "name": "$name", + "source": ${jsonEncode(source)}, + "private": false, + "can_evaluate": {} + }'''); + + return response['_id'] as String; + } + + Future _createMongoDBService(BaasApp app, String syncConfig) async { + final serviceName = _clusterName == null ? 'mongodb' : 'mongodb-atlas'; + final mongoConfig = _clusterName == null ? '{ "uri": "mongodb://localhost:26000" }' : '{ "clusterName": "$_clusterName" }'; + final mongoServiceId = await _createService(app, 'BackingDB', serviceName, mongoConfig); + + // The cluster linking must be separated from enabling sync because Atlas + // takes a few seconds to provision a user for BaaS, meaning enabling sync + // will fail if we attempt to do it with the same request. It's nondeterministic + // how long it'll take, so we must retry for a while. + var attempt = 0; + while (true) { + try { + await _patch('groups/$_groupId/apps/$app/services/$mongoServiceId/config', syncConfig); + break; + } catch (err) { + if (attempt++ < 24) { + print('Failed to update service after ${attempt * 5} seconds. Will keep retrying ...'); + + await Future.delayed(const Duration(seconds: 5)); + } else { + rethrow; + } + } + } + + return mongoServiceId; + } + + Future _createService(BaasApp app, String name, String type, String config) async { + print('Creating service $name for ${app.name}'); + + final dynamic response = await _post('groups/$_groupId/apps/$app/services', '''{ + "name": "$name", + "type": "$type", + "config": $config + }'''); + + return response['_id'] as String; + } + + Map _getHeaders([Map? additionalHeaders]) { + if (additionalHeaders == null) { + return _headers; + } + + additionalHeaders.addAll(_headers); + return additionalHeaders; + } + + Uri _getUri(String relativePath) { + return Uri.parse('$_baseUrl/$relativePath'); + } + + Future _post(String relativePath, String payload) async { + var response = await http.post(_getUri(relativePath), headers: _getHeaders({'Content-Type': 'application/json'}), body: payload); + return _decodeResponse(response, payload); + } + + Future _get(String relativePath) async { + var response = await http.get(_getUri(relativePath), headers: _getHeaders()); + return _decodeResponse(response); + } + + Future _put(String relativePath, String payload) async { + var response = await http.put(_getUri(relativePath), headers: _getHeaders({'Content-Type': 'application/json'}), body: payload); + return _decodeResponse(response, payload); + } + + Future _patch(String relativePath, String payload) async { + var response = await http.patch(_getUri(relativePath), headers: _getHeaders({'Content-Type': 'application/json'}), body: payload); + return _decodeResponse(response, payload); + } + + dynamic _decodeResponse(http.Response response, [String? payload]) { + if (response.statusCode > 399 || response.statusCode < 200) { + throw Exception('Failed to ${response.request?.method} ${response.request?.url}: ${response.statusCode} ${response.body}. Body: $payload'); + } + + if (response.body.isEmpty) { + return {}; + } + return jsonDecode(response.body); + } +} + +class BaasApp { + final String appId; + final String clientAppId; + final String name; + + BaasApp(this.appId, this.clientAppId, this.name); + + @override + String toString() { + return appId; + } +} diff --git a/lib/src/cli/deployapps/deployapps_command.dart b/lib/src/cli/deployapps/deployapps_command.dart new file mode 100644 index 000000000..ebdecb9da --- /dev/null +++ b/lib/src/cli/deployapps/deployapps_command.dart @@ -0,0 +1,78 @@ +//////////////////////////////////////////////////////////////////////////////// +// +// Copyright 2022 Realm Inc. +// +// 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. +// +//////////////////////////////////////////////////////////////////////////////// + +import 'dart:async'; +import 'dart:io'; + +import 'package:args/command_runner.dart'; + +import 'options.dart'; +import 'baas_client.dart'; + +class DeployAppsCommand extends Command { + @override + final String description = 'Deploys test applications to a MongoDB Realm server.'; + + @override + final String name = 'deploy-apps'; + + @override + bool get hidden => true; + + late Options options; + + DeployAppsCommand() { + populateOptionsParser(argParser); + } + + @override + FutureOr? run() async { + options = parseOptionsResult(argResults!); + + if (options.atlasCluster != null) { + if (options.apiKey == null) { + abort('--api-key must be supplied when --atlas-cluster is not set'); + } + + if (options.privateApiKey == null) { + abort('--private-api-key must be supplied when --atlas-cluster is not set'); + } + + if (options.projectId == null) { + abort('--project-id must be supplied when --atlas-cluster is not set'); + } + } + + final client = await (options.atlasCluster == null + ? BaasClient.docker(options.baasUrl) + : BaasClient.atlas(options.baasUrl, options.atlasCluster!, options.apiKey!, options.privateApiKey!, options.projectId!)); + + final apps = await client.getOrCreateApps(); + + print('App import is complete. There are: ${apps.length} apps on the server:'); + apps.forEach((_, value) { + print(" App '${value.name}': '${value.clientAppId}'"); + }); + } + + void abort(String error) { + print(error); + print(usage); + exit(64); //usage error + } +} diff --git a/lib/src/cli/deployapps/options.dart b/lib/src/cli/deployapps/options.dart new file mode 100644 index 000000000..7df2e88c1 --- /dev/null +++ b/lib/src/cli/deployapps/options.dart @@ -0,0 +1,47 @@ +//////////////////////////////////////////////////////////////////////////////// +// +// Copyright 2022 Realm Inc. +// +// 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. +// +//////////////////////////////////////////////////////////////////////////////// + +import 'package:build_cli_annotations/build_cli_annotations.dart'; + +part 'options.g.dart'; + +@CliOptions() +class Options { + @CliOption(help: 'Url for MongoDB Realm.', defaultsTo: 'http://localhost:9090') + final String baasUrl; + + @CliOption(help: 'Atlas Cluster to link in the application.') + final String? atlasCluster; + + @CliOption(help: 'Atlas API key to use for the import. Only used if atlas-cluster is specified.') + final String? apiKey; + + @CliOption(help: 'The private Atlas API key to use for the import. Only used if atlas-cluster is specified.') + final String? privateApiKey; + + @CliOption(help: 'The Atlas project id to use for the import. Only used if atlas-cluster is specified.') + final String? projectId; + + Options(this.baasUrl, {this.atlasCluster, this.apiKey, this.privateApiKey, this.projectId}); +} + +String get usage => _$parserForOptions.usage; + +ArgParser populateOptionsParser(ArgParser p) => _$populateOptionsParser(p); + +Options parseOptionsResult(ArgResults results) => _$parseOptionsResult(results); diff --git a/lib/src/cli/deployapps/options.g.dart b/lib/src/cli/deployapps/options.g.dart new file mode 100644 index 000000000..6a10e9101 --- /dev/null +++ b/lib/src/cli/deployapps/options.g.dart @@ -0,0 +1,48 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'options.dart'; + +// ************************************************************************** +// CliGenerator +// ************************************************************************** + +Options _$parseOptionsResult(ArgResults result) => Options( + result['baas-url'] as String, + atlasCluster: result['atlas-cluster'] as String?, + apiKey: result['api-key'] as String?, + privateApiKey: result['private-api-key'] as String?, + projectId: result['project-id'] as String?, + ); + +ArgParser _$populateOptionsParser(ArgParser parser) => parser + ..addOption( + 'baas-url', + help: 'Url for MongoDB Realm.', + defaultsTo: 'http://localhost:9090', + ) + ..addOption( + 'atlas-cluster', + help: 'Atlas Cluster to link in the application.', + ) + ..addOption( + 'api-key', + help: + 'Atlas API key to use for the import. Only used if atlas-cluster is specified.', + ) + ..addOption( + 'private-api-key', + help: + 'The private Atlas API key to use for the import. Only used if atlas-cluster is specified.', + ) + ..addOption( + 'project-id', + help: + 'The Atlas project id to use for the import. Only used if atlas-cluster is specified.', + ); + +final _$parserForOptions = _$populateOptionsParser(ArgParser()); + +Options parseOptions(List args) { + final result = _$parserForOptions.parse(args); + return _$parseOptionsResult(result); +} diff --git a/lib/src/cli/main.dart b/lib/src/cli/main.dart index 080015b47..bb8d48b8f 100644 --- a/lib/src/cli/main.dart +++ b/lib/src/cli/main.dart @@ -25,6 +25,7 @@ import 'install/install_command.dart'; import 'metrics/metrics_command.dart'; import 'archive/archive_command.dart'; import 'extract/extract_command.dart'; +import 'deployapps/deployapps_command.dart'; void main(List arguments) { CommandRunner("dart run realm|realm_dart", 'Realm commands for working with Realm Flutter & Dart SDKs.') @@ -33,6 +34,7 @@ void main(List arguments) { ..addCommand(InstallCommand()) ..addCommand(ArchiveCommand()) ..addCommand(ExtractCommand()) + ..addCommand(DeployAppsCommand()) ..run(arguments).catchError((Object error) { if (error is UsageException) { print(error); diff --git a/pubspec.yaml b/pubspec.yaml index dbe6124af..6fa4cae8f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -25,7 +25,7 @@ dependencies: pub_semver: ^2.1.0 realm_common: path: ./common - realm_generator: + realm_generator: path: ./generator tar: ^0.5.4 build_runner: ^2.1.0 @@ -35,6 +35,7 @@ dev_dependencies: json_serializable: ^6.1.0 lints: ^1.0.1 test: ^1.14.3 + http: ^0.13.4 dependency_overrides: analyzer: 3.3.1 diff --git a/test/configuration_test.dart b/test/configuration_test.dart index 491741690..03df0aded 100644 --- a/test/configuration_test.dart +++ b/test/configuration_test.dart @@ -25,7 +25,7 @@ import 'test.dart'; Future main([List? args]) async { print("Current PID $pid"); - setupTests(args); + await setupTests(args); test('Configuration can be created', () { Configuration([Car.schema]); diff --git a/test/list_test.dart b/test/list_test.dart index 3841e666d..e80ca06bc 100644 --- a/test/list_test.dart +++ b/test/list_test.dart @@ -26,7 +26,7 @@ import 'test.dart'; Future main([List? args]) async { print("Current PID $pid"); - setupTests(args); + await setupTests(args); test('Lists add object with a list property', () { var config = Configuration([Team.schema, Person.schema]); diff --git a/test/realm_object_test.dart b/test/realm_object_test.dart index 9a932757a..25a1f436d 100644 --- a/test/realm_object_test.dart +++ b/test/realm_object_test.dart @@ -27,7 +27,7 @@ import 'test.dart'; Future main([List? args]) async { print("Current PID $pid"); - setupTests(args); + await setupTests(args); test('RealmObject get property', () { var config = Configuration([Car.schema]); diff --git a/test/realm_test.dart b/test/realm_test.dart index fc8119e28..bab255a1d 100644 --- a/test/realm_test.dart +++ b/test/realm_test.dart @@ -27,7 +27,7 @@ import 'test.dart'; Future main([List? args]) async { print("Current PID $pid"); - setupTests(args); + await setupTests(args); test('Realm can be created', () { var config = Configuration([Car.schema]); diff --git a/test/results_test.dart b/test/results_test.dart index 855494e87..54d53536d 100644 --- a/test/results_test.dart +++ b/test/results_test.dart @@ -26,7 +26,7 @@ import 'test.dart'; Future main([List? args]) async { print("Current PID $pid"); - setupTests(args); + await setupTests(args); test('Results all should not return null', () { var config = Configuration([Car.schema]); diff --git a/test/test.dart b/test/test.dart index 3f56b4495..a74b8d064 100644 --- a/test/test.dart +++ b/test/test.dart @@ -22,6 +22,7 @@ import 'package:path/path.dart' as _path; import 'package:test/test.dart' hide test; import 'package:test/test.dart' as testing; import '../lib/realm.dart'; +import '../lib/src/cli/deployapps/baas_client.dart'; part 'test.g.dart'; @@ -73,6 +74,8 @@ class _School { } String? testName; +Map baasApps = {}; + //Overrides test method so we can filter tests void test(String? name, dynamic Function() testFunction, {dynamic skip}) { if (testName != null && !name!.contains(testName!)) { @@ -92,9 +95,11 @@ void xtest(String? name, dynamic Function() testFunction) { testing.test(name, testFunction, skip: "Test is disabled"); } -void setupTests(List? args) { +Future setupTests(List? args) async { parseTestNameFromArguments(args); + await setupBaas(); + setUp(() { String path = "${generateRandomString(10)}.realm"; if (Platform.isAndroid || Platform.isIOS) { @@ -154,7 +159,23 @@ void parseTestNameFromArguments(List? arguments) { } int nameArgIndex = arguments.indexOf("--name"); - if (nameArgIndex >= 0 && arguments.length > 1) { + if (nameArgIndex >= 0 && arguments.length > nameArgIndex) { testName = arguments[nameArgIndex + 1]; } } + +Future setupBaas() async { + final baasUrl = Platform.environment['BAAS_URL']; + if (baasUrl == null) { + return; + } + + final cluster = Platform.environment['BAAS_CLUSTER']; + final apiKey = Platform.environment['BAAS_API_KEY']; + final privateApiKey = Platform.environment['BAAS_PRIVATE_API_KEY']; + final projectId = Platform.environment['BAAS_PROJECT_ID']; + + final client = await (cluster == null ? BaasClient.docker(baasUrl) : BaasClient.atlas(baasUrl, cluster, apiKey!, privateApiKey!, projectId!)); + + baasApps.addAll(await client.getOrCreateApps()); +}