Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/firebase emulator config switch #707

Merged
Changes from 1 commit
Commits
Show all changes
113 commits
Select commit Hold shift + click to select a range
f548b1f
Implementation of the Firebase Emulator DevService
jfbenckhuijsen Oct 28, 2024
c8e670a
Added firebase integration testing
jfbenckhuijsen Nov 1, 2024
0a9e223
Switch on or off the Firestore and Pubsub dev services based on a con…
jfbenckhuijsen Nov 1, 2024
2e6e862
Merge branch 'feature/emulator-credentials' into feature/firebase-emu…
jfbenckhuijsen Nov 1, 2024
e39fded
Merge branch 'feature/firebase-emulator' into feature/firebase-emulat…
jfbenckhuijsen Nov 1, 2024
3e34037
Create test to use both Firestore and pubsub
jfbenckhuijsen Nov 1, 2024
fe2266e
Merge branch 'feature/firebase-emulator' into feature/firebase-emulat…
jfbenckhuijsen Nov 1, 2024
c25de1c
Merge remote-tracking branch 'main/main' into feature/firebase-emulat…
jfbenckhuijsen Nov 2, 2024
7da9e29
Merge remote-tracking branch 'main/main' into feature/firebase-emulator
jfbenckhuijsen Nov 2, 2024
70f9eb8
Formatting fixes
jfbenckhuijsen Nov 2, 2024
81841f8
Formatting fixes
jfbenckhuijsen Nov 2, 2024
62122cc
Version bump for new modules
jfbenckhuijsen Nov 2, 2024
e2222dd
Docs regenerated
jfbenckhuijsen Nov 2, 2024
f17e6d3
Merge branch 'feature/firebase-emulator' into feature/firebase-emulat…
jfbenckhuijsen Nov 2, 2024
06726ad
Docs regenerated
jfbenckhuijsen Nov 2, 2024
81f4d99
Formatting fixes
jfbenckhuijsen Nov 2, 2024
ba22251
Use emulator credentials by default in case the config property is set.
jfbenckhuijsen Nov 2, 2024
9d2ab29
Merge branch 'feature/firebase-emulator' into feature/firebase-emulat…
jfbenckhuijsen Nov 2, 2024
530dce0
Generated docs and formatted sources
jfbenckhuijsen Nov 2, 2024
e1db024
Dont start pubsub and firestore container if disabled
jfbenckhuijsen Nov 2, 2024
5c1dc54
Moved firebase library versions to bom (as it is shared with the new …
jfbenckhuijsen Nov 13, 2024
58d8693
Moved firebase emulator container to separate class, which is totally…
jfbenckhuijsen Nov 13, 2024
771e9a7
Documentation fixes, using the generated configuration table.
jfbenckhuijsen Nov 13, 2024
d039a2d
Fix writing the emulator data on exit
jfbenckhuijsen Nov 13, 2024
36a6ccc
Split the logic to build the FirebaseEmulatorContainer configuration …
jfbenckhuijsen Nov 13, 2024
a0f1b48
Removed obsolete check
jfbenckhuijsen Nov 13, 2024
9a48b15
Formatting changes
jfbenckhuijsen Nov 13, 2024
5084b9f
Fix property for firestore
jfbenckhuijsen Nov 13, 2024
535d1fa
Renamed enum name
jfbenckhuijsen Nov 14, 2024
cf1c783
Fix writing container logs
jfbenckhuijsen Nov 14, 2024
78d8308
Formatting fix
jfbenckhuijsen Nov 14, 2024
9bc7679
Output debug data for output files
jfbenckhuijsen Nov 14, 2024
90bfcec
Documentation regenerated
jfbenckhuijsen Nov 14, 2024
16ca50a
Debug export on build server
jfbenckhuijsen Nov 14, 2024
42971d5
Debug export on build server
jfbenckhuijsen Nov 14, 2024
020f398
Debug export on build server
jfbenckhuijsen Nov 14, 2024
235c691
Debug export on build server
jfbenckhuijsen Nov 14, 2024
ab2ff11
Debug export on build server
jfbenckhuijsen Nov 14, 2024
e18d12f
Add specifying the user and groupid
jfbenckhuijsen Nov 14, 2024
c430c57
Combine docker run commands and fix order of commands to speed up doc…
jfbenckhuijsen Nov 14, 2024
beef407
Combine docker run commands and fix order of commands to speed up doc…
jfbenckhuijsen Nov 14, 2024
382ee26
Fix file permissions
jfbenckhuijsen Nov 14, 2024
a77608d
Try run as gitlab user
jfbenckhuijsen Nov 14, 2024
994b330
Cleanup debug code
jfbenckhuijsen Nov 14, 2024
e25d7b0
Fix documentation
jfbenckhuijsen Nov 14, 2024
f3c3518
Formatting fix
jfbenckhuijsen Nov 14, 2024
db28e35
Regenerated documentation
jfbenckhuijsen Nov 14, 2024
6d49dde
Update firebase-admin/runtime/src/main/java/io/quarkiverse/googleclou…
jfbenckhuijsen Nov 21, 2024
a9af679
Update firebase/README.md
jfbenckhuijsen Nov 21, 2024
874bab4
Added missing test scope
jfbenckhuijsen Nov 21, 2024
96c2a30
Removed obsolete properties
jfbenckhuijsen Nov 21, 2024
f7c6cf3
Update firebase/deployment/src/main/java/io/quarkiverse/googlecloudse…
jfbenckhuijsen Nov 21, 2024
0c80a67
Fix comment to match default
jfbenckhuijsen Nov 21, 2024
9cbeb5a
Renamed module to firebase-devservices
jfbenckhuijsen Nov 21, 2024
30281e1
Renamed module to firebase-devservices
jfbenckhuijsen Nov 21, 2024
c7f02ca
Merge remote-tracking branch 'origin/feature/firebase-emulator-config…
jfbenckhuijsen Nov 21, 2024
b1d5362
Merge remote-tracking branch 'refs/remotes/main/main' into feature/fi…
jfbenckhuijsen Nov 21, 2024
461172d
Moved files to new directory
jfbenckhuijsen Nov 21, 2024
c8d8b2d
New name for firebase devservices, new doc files generated
jfbenckhuijsen Nov 21, 2024
021d9cc
New name for firebase devservices, new doc files generated
jfbenckhuijsen Nov 21, 2024
83dea99
Formatting fix
jfbenckhuijsen Nov 21, 2024
a3e8a06
Docs regenerated
jfbenckhuijsen Nov 21, 2024
79dd652
Added firebase schema
jfbenckhuijsen Nov 15, 2024
b45041e
Changed schema ports number -> integer
jfbenckhuijsen Nov 15, 2024
cb826d6
Merge fixes
jfbenckhuijsen Nov 15, 2024
f935201
Added config options for the rules and indexes files
jfbenckhuijsen Nov 21, 2024
9c8fa6e
Firebase container moved to separate package
jfbenckhuijsen Nov 21, 2024
a712b1b
Firebase container moved to separate package
jfbenckhuijsen Nov 21, 2024
eb031f7
Added tests and completed setup to include custom firebase.json, Fire…
jfbenckhuijsen Nov 30, 2024
e9f2219
Update docs/modules/ROOT/pages/firebase.adoc
jfbenckhuijsen Dec 12, 2024
6ff127c
Update firebase-devservices/runtime/src/main/resources/META-INF/quark…
jfbenckhuijsen Dec 12, 2024
2b82260
Update docs/modules/ROOT/pages/firebase.adoc
jfbenckhuijsen Dec 12, 2024
0b90afc
Update docs/modules/ROOT/pages/firebase.adoc
jfbenckhuijsen Dec 12, 2024
baab7ed
Fix documentation page name and include in navigation
jfbenckhuijsen Dec 12, 2024
a16bb55
Update firebase.adoc
jfbenckhuijsen Dec 15, 2024
ad647a6
Refactored config setup
jfbenckhuijsen Dec 15, 2024
1b6264f
Merge branch 'feature/firebase-emulator-config-switch-rules' into fea…
jfbenckhuijsen Dec 15, 2024
0be5dad
Refactored to separate package
jfbenckhuijsen Dec 15, 2024
59b926c
Import changes from testcontainers-firebase module
jfbenckhuijsen Dec 15, 2024
01fb947
Update docs/modules/ROOT/pages/firebase.adoc
jfbenckhuijsen Dec 15, 2024
b5f81b0
Update docs/modules/ROOT/pages/firebase.adoc
jfbenckhuijsen Dec 15, 2024
9f08a6a
Merge remote-tracking branch 'origin/feature/firebase-emulator-config…
jfbenckhuijsen Dec 15, 2024
ee2dc6a
Update configuration
jfbenckhuijsen Dec 15, 2024
365401c
Fixes from copying from main module
jfbenckhuijsen Dec 15, 2024
3743dec
Refactored config setup and connected to new builder for FirebaseEmul…
jfbenckhuijsen Dec 15, 2024
4277e40
Fix properties based on changed config setup
jfbenckhuijsen Dec 15, 2024
9e529e3
Formatting fixes
jfbenckhuijsen Dec 15, 2024
dda79bf
Removed obsolete test
jfbenckhuijsen Dec 15, 2024
be35c0e
Fix storage unit test
jfbenckhuijsen Dec 16, 2024
f7da5f3
Merge remote-tracking branch 'main/main' into feature/firebase-emulat…
jfbenckhuijsen Dec 16, 2024
e9de6dc
Merge in main
jfbenckhuijsen Dec 16, 2024
4592f11
Fix config mapping to match mapping in quarkus.extension.yaml
jfbenckhuijsen Dec 17, 2024
06957c1
Formatting fixed
jfbenckhuijsen Dec 17, 2024
a2547bd
Documentation regenerated
jfbenckhuijsen Dec 17, 2024
5111190
Removed dependency on firebase-devservices from firebase-admin. No ne…
jfbenckhuijsen Dec 17, 2024
798a0db
Fixed extension definition
jfbenckhuijsen Dec 17, 2024
874996e
Update docs with getting started info
jfbenckhuijsen Dec 17, 2024
9228063
If no emulators are defined via configuration, make sure to fallback …
jfbenckhuijsen Dec 17, 2024
5e10315
Fix maven name of the devservices projects
jfbenckhuijsen Dec 17, 2024
2a7415a
Add firebase devservices as dependency
jfbenckhuijsen Dec 17, 2024
f9f6adf
Remove unneeded config option
jfbenckhuijsen Dec 17, 2024
249afe4
Enable firebase auth
jfbenckhuijsen Dec 17, 2024
64d081c
Fix doc (typo)
jfbenckhuijsen Dec 17, 2024
868e41e
Fix missing pubsub emulator parsing
jfbenckhuijsen Dec 17, 2024
eeb5b9b
Improve reading of firebase.json file and added documentation
jfbenckhuijsen Dec 18, 2024
f353ec0
Run npm install for test dependencies for functions
jfbenckhuijsen Dec 20, 2024
bf90792
Use specific project id
jfbenckhuijsen Dec 20, 2024
39608c8
Fix path to NPM package.json
jfbenckhuijsen Dec 20, 2024
8be975c
Move config files into the test directory
jfbenckhuijsen Dec 20, 2024
338451a
Create temporary files in target (build) directory
jfbenckhuijsen Dec 20, 2024
9c855f6
Move schema to META-INF
jfbenckhuijsen Dec 20, 2024
921ff66
Fix formatting
jfbenckhuijsen Dec 20, 2024
bc2c5a0
Fix formatting
jfbenckhuijsen Dec 20, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Added tests and completed setup to include custom firebase.json, Fire…
…store and Storage files.
jfbenckhuijsen committed Nov 30, 2024
commit eb031f7426b271f879244202ef7cb30705e4ced5
Original file line number Diff line number Diff line change
@@ -425,6 +425,40 @@ endif::add-copy-button-to-env-var[]
|int
|

a|icon:lock[title=Fixed at build time] [[quarkus-google-cloud-firebase-devservices_quarkus-google-cloud-firestore-devservice-rules-file]] [.property-path]##link:#quarkus-google-cloud-firebase-devservices_quarkus-google-cloud-firestore-devservice-rules-file[`quarkus.google.cloud.firestore.devservice.rules-file`]##

[.description]
--
Path to the firestore.rules file.


ifdef::add-copy-button-to-env-var[]
Environment variable: env_var_with_copy_button:+++QUARKUS_GOOGLE_CLOUD_FIRESTORE_DEVSERVICE_RULES_FILE+++[]
endif::add-copy-button-to-env-var[]
ifndef::add-copy-button-to-env-var[]
Environment variable: `+++QUARKUS_GOOGLE_CLOUD_FIRESTORE_DEVSERVICE_RULES_FILE+++`
endif::add-copy-button-to-env-var[]
--
|string
|

a|icon:lock[title=Fixed at build time] [[quarkus-google-cloud-firebase-devservices_quarkus-google-cloud-firestore-devservice-indexes-file]] [.property-path]##link:#quarkus-google-cloud-firebase-devservices_quarkus-google-cloud-firestore-devservice-indexes-file[`quarkus.google.cloud.firestore.devservice.indexes-file`]##

[.description]
--
Path to the firestore.indexes.json file.


ifdef::add-copy-button-to-env-var[]
Environment variable: env_var_with_copy_button:+++QUARKUS_GOOGLE_CLOUD_FIRESTORE_DEVSERVICE_INDEXES_FILE+++[]
endif::add-copy-button-to-env-var[]
ifndef::add-copy-button-to-env-var[]
Environment variable: `+++QUARKUS_GOOGLE_CLOUD_FIRESTORE_DEVSERVICE_INDEXES_FILE+++`
endif::add-copy-button-to-env-var[]
--
|string
|

a|icon:lock[title=Fixed at build time] [[quarkus-google-cloud-firebase-devservices_quarkus-google-cloud-functions-devservice-enabled]] [.property-path]##link:#quarkus-google-cloud-firebase-devservices_quarkus-google-cloud-functions-devservice-enabled[`quarkus.google.cloud.functions.devservice.enabled`]##

[.description]
@@ -527,5 +561,22 @@ endif::add-copy-button-to-env-var[]
|int
|

a|icon:lock[title=Fixed at build time] [[quarkus-google-cloud-firebase-devservices_quarkus-google-cloud-storage-devservice-rules-file]] [.property-path]##link:#quarkus-google-cloud-firebase-devservices_quarkus-google-cloud-storage-devservice-rules-file[`quarkus.google.cloud.storage.devservice.rules-file`]##

[.description]
--
Path to the storage.rules file.


ifdef::add-copy-button-to-env-var[]
Environment variable: env_var_with_copy_button:+++QUARKUS_GOOGLE_CLOUD_STORAGE_DEVSERVICE_RULES_FILE+++[]
endif::add-copy-button-to-env-var[]
ifndef::add-copy-button-to-env-var[]
Environment variable: `+++QUARKUS_GOOGLE_CLOUD_STORAGE_DEVSERVICE_RULES_FILE+++`
endif::add-copy-button-to-env-var[]
--
|string
|

|===

Original file line number Diff line number Diff line change
@@ -425,6 +425,40 @@ endif::add-copy-button-to-env-var[]
|int
|

a|icon:lock[title=Fixed at build time] [[quarkus-google-cloud-firebase-devservices_quarkus-google-cloud-firestore-devservice-rules-file]] [.property-path]##link:#quarkus-google-cloud-firebase-devservices_quarkus-google-cloud-firestore-devservice-rules-file[`quarkus.google.cloud.firestore.devservice.rules-file`]##

[.description]
--
Path to the firestore.rules file.


ifdef::add-copy-button-to-env-var[]
Environment variable: env_var_with_copy_button:+++QUARKUS_GOOGLE_CLOUD_FIRESTORE_DEVSERVICE_RULES_FILE+++[]
endif::add-copy-button-to-env-var[]
ifndef::add-copy-button-to-env-var[]
Environment variable: `+++QUARKUS_GOOGLE_CLOUD_FIRESTORE_DEVSERVICE_RULES_FILE+++`
endif::add-copy-button-to-env-var[]
--
|string
|

a|icon:lock[title=Fixed at build time] [[quarkus-google-cloud-firebase-devservices_quarkus-google-cloud-firestore-devservice-indexes-file]] [.property-path]##link:#quarkus-google-cloud-firebase-devservices_quarkus-google-cloud-firestore-devservice-indexes-file[`quarkus.google.cloud.firestore.devservice.indexes-file`]##

[.description]
--
Path to the firestore.indexes.json file.


ifdef::add-copy-button-to-env-var[]
Environment variable: env_var_with_copy_button:+++QUARKUS_GOOGLE_CLOUD_FIRESTORE_DEVSERVICE_INDEXES_FILE+++[]
endif::add-copy-button-to-env-var[]
ifndef::add-copy-button-to-env-var[]
Environment variable: `+++QUARKUS_GOOGLE_CLOUD_FIRESTORE_DEVSERVICE_INDEXES_FILE+++`
endif::add-copy-button-to-env-var[]
--
|string
|

a|icon:lock[title=Fixed at build time] [[quarkus-google-cloud-firebase-devservices_quarkus-google-cloud-functions-devservice-enabled]] [.property-path]##link:#quarkus-google-cloud-firebase-devservices_quarkus-google-cloud-functions-devservice-enabled[`quarkus.google.cloud.functions.devservice.enabled`]##

[.description]
@@ -527,5 +561,22 @@ endif::add-copy-button-to-env-var[]
|int
|

a|icon:lock[title=Fixed at build time] [[quarkus-google-cloud-firebase-devservices_quarkus-google-cloud-storage-devservice-rules-file]] [.property-path]##link:#quarkus-google-cloud-firebase-devservices_quarkus-google-cloud-storage-devservice-rules-file[`quarkus.google.cloud.storage.devservice.rules-file`]##

[.description]
--
Path to the storage.rules file.


ifdef::add-copy-button-to-env-var[]
Environment variable: env_var_with_copy_button:+++QUARKUS_GOOGLE_CLOUD_STORAGE_DEVSERVICE_RULES_FILE+++[]
endif::add-copy-button-to-env-var[]
ifndef::add-copy-button-to-env-var[]
Environment variable: `+++QUARKUS_GOOGLE_CLOUD_STORAGE_DEVSERVICE_RULES_FILE+++`
endif::add-copy-button-to-env-var[]
--
|string
|

|===

26 changes: 26 additions & 0 deletions firebase-devservices/deployment/firebase.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"emulators": {
"firestore": {
"host": "0.0.0.0",
"port": 7002,
"websocketPort": 7003
},
"storage": {
"host": "0.0.0.0",
"port": 7005
},
"ui": {
"host": "0.0.0.0",
"enabled": true,
"port": 7009
},
"singleProjectMode": true
},
"firestore": {
"rules": "firestore.rules",
"indexes": "firestore.indexes.json"
},
"storage": {
"rules": "storage.rules"
}
}
39 changes: 39 additions & 0 deletions firebase-devservices/deployment/firestore.indexes.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
{
"indexes": [],
"fieldOverrides": [
{
"collectionGroup": "Aanmelding",
"fieldPath": "datum",
"ttl": false,
"indexes": [
{
"order": "ASCENDING",
"queryScope": "COLLECTION_GROUP"
}
]
},
{
"collectionGroup": "events",
"fieldPath": "transactionId",
"ttl": false,
"indexes": [
{
"order": "ASCENDING",
"queryScope": "COLLECTION"
},
{
"order": "DESCENDING",
"queryScope": "COLLECTION"
},
{
"arrayConfig": "CONTAINS",
"queryScope": "COLLECTION"
},
{
"order": "ASCENDING",
"queryScope": "COLLECTION_GROUP"
}
]
}
]
}
9 changes: 9 additions & 0 deletions firebase-devservices/deployment/firestore.rules
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /data/{document} {
allow read: if request.auth != null && request.auth.uid == resource.data.ownerId;
allow write: if request.auth != null && request.auth.uid == resource.data.ownerId;
}
}
}
Original file line number Diff line number Diff line change
@@ -268,7 +268,7 @@ interface Storage {
*/
StorageDevService devservice();

interface StorageDevService extends GenericDevService{
interface StorageDevService extends GenericDevService {

/**
* Path to the storage.rules file.
Original file line number Diff line number Diff line change
@@ -4,9 +4,9 @@
import java.util.*;
import java.util.stream.Collectors;

import io.quarkiverse.googlecloudservices.firebase.deployment.testcontainers.FirebaseEmulatorContainer;
import org.jboss.logging.Logger;

import io.quarkiverse.googlecloudservices.firebase.deployment.testcontainers.FirebaseEmulatorContainer;
import io.quarkus.deployment.IsNormal;
import io.quarkus.deployment.annotations.BuildStep;
import io.quarkus.deployment.annotations.BuildSteps;
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
package io.quarkiverse.googlecloudservices.firebase.deployment;

import io.quarkiverse.googlecloudservices.firebase.deployment.testcontainers.FirebaseEmulatorContainer;

import java.io.File;
import java.nio.file.Path;
import java.util.Map;
import java.util.stream.Collectors;

import io.quarkiverse.googlecloudservices.firebase.deployment.testcontainers.FirebaseEmulatorContainer;

/**
* This class translates the Quarkus Firebase extension configuration to the {@link FirebaseEmulatorContainer}
* configuration.
@@ -33,17 +33,15 @@ public FirebaseEmulatorContainer.EmulatorConfig build() {
devService.customFirebaseJson().map(File::new).map(File::toPath),
devService.javaToolOptions(),
devService.emulatorData().map(File::new).map(File::toPath),
new FirebaseEmulatorContainer.HostingConfig(
config.firebase().hosting().hostingPath().map(FirebaseEmulatorConfigBuilder::asPath)
),
new FirebaseEmulatorContainer.StorageConfig(
config.storage().devservice().rulesFile().map(FirebaseEmulatorConfigBuilder::asPath)
),
new FirebaseEmulatorContainer.FirestoreConfig(
config.firestore().devservice().rulesFile().map(FirebaseEmulatorConfigBuilder::asPath),
config.firestore().devservice().indexesFile().map(FirebaseEmulatorConfigBuilder::asPath)
),
exposedEmulators(devServices(config)));
new FirebaseEmulatorContainer.FirebaseConfig(
new FirebaseEmulatorContainer.HostingConfig(
config.firebase().hosting().hostingPath().map(FirebaseEmulatorConfigBuilder::asPath)),
new FirebaseEmulatorContainer.StorageConfig(
config.storage().devservice().rulesFile().map(FirebaseEmulatorConfigBuilder::asPath)),
new FirebaseEmulatorContainer.FirestoreConfig(
config.firestore().devservice().rulesFile().map(FirebaseEmulatorConfigBuilder::asPath),
config.firestore().devservice().indexesFile().map(FirebaseEmulatorConfigBuilder::asPath)),
exposedEmulators(devServices(config))));
}

private static Path asPath(String path) {
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
package io.quarkiverse.googlecloudservices.firebase.deployment.testcontainers;

import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.nio.file.Path;
import java.util.*;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collectors;

import org.testcontainers.shaded.com.fasterxml.jackson.databind.ObjectMapper;

import io.quarkiverse.googlecloudservices.firebase.deployment.firebase.json.Emulators;
import io.quarkiverse.googlecloudservices.firebase.deployment.firebase.json.FirebaseConfig;

public class CustomFirebaseConfigReader {

private final ObjectMapper objectMapper = new ObjectMapper();

public FirebaseEmulatorContainer.FirebaseConfig readFromFirebase(Path customFirebaseJson) throws IOException {
var root = readCustomFirebaseJson(customFirebaseJson);

return new FirebaseEmulatorContainer.FirebaseConfig(
readHosting(root.getHosting()),
readStorage(root.getStorage()),
readFirestore(root.getFirestore()),
readEmulators(root.getEmulators()));
}

private record EmulatorMergeStrategy<T>(
FirebaseEmulatorContainer.Emulator emulator,
Supplier<T> configObjectSupplier,
Function<T, Supplier<Integer>> portSupplier) {
}

private Map<FirebaseEmulatorContainer.Emulator, FirebaseEmulatorContainer.ExposedPort> readEmulators(Emulators em) {
var mergeStrategies = new EmulatorMergeStrategy<?>[] {
new EmulatorMergeStrategy<>(
FirebaseEmulatorContainer.Emulator.AUTHENTICATION,
em::getAuth,
a -> a::getPort),
new EmulatorMergeStrategy<>(
FirebaseEmulatorContainer.Emulator.EMULATOR_SUITE_UI,
em::getUi,
u -> u::getPort),
new EmulatorMergeStrategy<>(
FirebaseEmulatorContainer.Emulator.EMULATOR_HUB,
em::getHub,
h -> h::getPort),
new EmulatorMergeStrategy<>(
FirebaseEmulatorContainer.Emulator.LOGGING,
em::getLogging,
l -> l::getPort),
new EmulatorMergeStrategy<>(
FirebaseEmulatorContainer.Emulator.CLOUD_FUNCTIONS,
em::getFunctions,
f -> f::getPort),
new EmulatorMergeStrategy<>(
FirebaseEmulatorContainer.Emulator.EVENT_ARC,
em::getEventarc,
e -> e::getPort),
new EmulatorMergeStrategy<>(
FirebaseEmulatorContainer.Emulator.REALTIME_DATABASE,
em::getDatabase,
d -> d::getPort),
new EmulatorMergeStrategy<>(
FirebaseEmulatorContainer.Emulator.CLOUD_FIRESTORE,
em::getFirestore,
d -> d::getPort),
new EmulatorMergeStrategy<>(
FirebaseEmulatorContainer.Emulator.CLOUD_STORAGE,
em::getStorage,
s -> s::getPort),
new EmulatorMergeStrategy<>(
FirebaseEmulatorContainer.Emulator.FIREBASE_HOSTING,
em::getHosting,
h -> h::getPort)
};

var map = Arrays.stream(mergeStrategies)
.map(this::mergeEmulator)
.filter(e -> !Objects.isNull(e))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));

if (em.getFirestore() != null && em.getFirestore().getWebsocketPort() != null) {
map = new HashMap<>(map);

map.put(
FirebaseEmulatorContainer.Emulator.CLOUD_FIRESTORE_WS,
new FirebaseEmulatorContainer.ExposedPort(em.getFirestore().getWebsocketPort()));

map = Map.copyOf(map);
}

return map;
}

private <T> Map.Entry<FirebaseEmulatorContainer.Emulator, FirebaseEmulatorContainer.ExposedPort> mergeEmulator(
EmulatorMergeStrategy<T> emulatorMergeStrategy) {

var configObject = emulatorMergeStrategy.configObjectSupplier.get();
if (configObject != null) {
var port = emulatorMergeStrategy.portSupplier.apply(configObject).get();
return Map.entry(emulatorMergeStrategy.emulator, new FirebaseEmulatorContainer.ExposedPort(port));
} else {
return null;
}
}

private FirebaseEmulatorContainer.FirestoreConfig readFirestore(Object firestore) {
if (firestore instanceof Map) {
@SuppressWarnings("unchecked")
Map<String, String> firestoreMap = (Map<String, String>) firestore;

var rulesFile = Optional
.ofNullable(firestoreMap.get("rules"))
.map(this::resolvePath);
var indexesFile = Optional
.ofNullable(firestoreMap.get("indexes"))
.map(this::resolvePath);

return new FirebaseEmulatorContainer.FirestoreConfig(
rulesFile,
indexesFile);
} else {
return new FirebaseEmulatorContainer.FirestoreConfig(
Optional.empty(),
Optional.empty());
}
}

private FirebaseEmulatorContainer.HostingConfig readHosting(Object hosting) {
if (hosting instanceof Map) {
@SuppressWarnings("unchecked")
Map<String, String> hostingMap = (Map<String, String>) hosting;

var publicDir = Optional
.ofNullable(hostingMap.get("public"))
.map(this::resolvePath);

return new FirebaseEmulatorContainer.HostingConfig(
publicDir);
} else {
return new FirebaseEmulatorContainer.HostingConfig(
Optional.empty());
}
}

private FirebaseEmulatorContainer.StorageConfig readStorage(Object storage) {
if (storage instanceof Map) {
@SuppressWarnings("unchecked")
Map<String, String> storageMap = (Map<String, String>) storage;

var rulesFile = Optional
.ofNullable(storageMap.get("rules"))
.map(this::resolvePath);

return new FirebaseEmulatorContainer.StorageConfig(
rulesFile);
} else {
return new FirebaseEmulatorContainer.StorageConfig(
Optional.empty());
}
}

private Path resolvePath(String filename) {
return new File(filename).toPath();
}

private FirebaseConfig readCustomFirebaseJson(Path customFirebaseJson) throws IOException {
var customFirebaseFile = customFirebaseJson.toFile();
var customFirebaseStream = new BufferedInputStream(new FileInputStream(customFirebaseFile));

return objectMapper.readerFor(FirebaseConfig.class)
.readValue(customFirebaseStream);
}

}
Original file line number Diff line number Diff line change
@@ -179,6 +179,20 @@ public record FirestoreConfig(
Optional<Path> indexesFile) {
}

/**
*
* @param hostingConfig The firebase hosting configuration
* @param storageConfig The storage configuration
* @param firestoreConfig The firestore configuration
* @param services The exposed services configuration
*/
public record FirebaseConfig(
HostingConfig hostingConfig,
StorageConfig storageConfig,
FirestoreConfig firestoreConfig,
Map<Emulator, ExposedPort> services) {
}

/**
* Describes the Firebase emulator configuration.
*
@@ -189,10 +203,7 @@ public record FirestoreConfig(
* @param customFirebaseJson The path to a custom firebase
* @param javaToolOptions The options to pass to the java based emulators
* @param emulatorData The path to the directory where to store the emulator data
* @param hostingConfig The firebase hosting configuration
* @param storageConfig The storage configuration
* @param firestoreConfig The firestore configuration
* @param services The exposed services configuration
* @param firebaseConfig The firebase configuration
*/
public record EmulatorConfig(
DockerConfig dockerConfig,
@@ -202,34 +213,31 @@ public record EmulatorConfig(
Optional<Path> customFirebaseJson,
Optional<String> javaToolOptions,
Optional<Path> emulatorData,
HostingConfig hostingConfig,
StorageConfig storageConfig,
FirestoreConfig firestoreConfig,
Map<Emulator, ExposedPort> services) {
FirebaseConfig firebaseConfig) {
}

private final Map<Emulator, ExposedPort> services;

/**
* Creates a new Firebase Emulator container
*
* @param firebaseConfig The generic configuration of the firebase emulators
* @param emulatorConfig The generic configuration of the firebase emulators
*/
public FirebaseEmulatorContainer(EmulatorConfig firebaseConfig) {
super(new FirebaseDockerBuilder(firebaseConfig).build());
public FirebaseEmulatorContainer(EmulatorConfig emulatorConfig) {
super(new FirebaseDockerBuilder(emulatorConfig).build());

firebaseConfig.emulatorData().ifPresent(path -> {
emulatorConfig.emulatorData().ifPresent(path -> {
// https://firebase.google.com/docs/emulator-suite/install_and_configure#export_and_import_emulator_data
// Mount the volume to the specified path
this.withFileSystemBind(path.toString(), EMULATOR_DATA_PATH, BindMode.READ_WRITE);
});

firebaseConfig.hostingConfig().hostingContentDir().ifPresent(hostingPath -> {
emulatorConfig.firebaseConfig().hostingConfig().hostingContentDir().ifPresent(hostingPath -> {
// Mount volume for static hosting content
this.withFileSystemBind(hostingPath.toString(), FIREBASE_HOSTING_PATH, BindMode.READ_ONLY);
});

this.services = firebaseConfig.services;
this.services = emulatorConfig.firebaseConfig().services;
}

private static class FirebaseDockerBuilder {
@@ -243,14 +251,14 @@ private static class FirebaseDockerBuilder {

private final ImageFromDockerfile result;

private final EmulatorConfig firebaseConfig;
private final EmulatorConfig emulatorConfig;
private final Map<Emulator, ExposedPort> devServices;

private DockerfileBuilder dockerBuilder;

public FirebaseDockerBuilder(EmulatorConfig firebaseConfig) {
this.devServices = firebaseConfig.services;
this.firebaseConfig = firebaseConfig;
public FirebaseDockerBuilder(EmulatorConfig emulatorConfig) {
this.devServices = emulatorConfig.firebaseConfig().services;
this.emulatorConfig = emulatorConfig;

this.result = new ImageFromDockerfile("localhost/testcontainers/firebase", false)
.withDockerfileFromBuilder(builder -> this.dockerBuilder = builder);
@@ -275,7 +283,7 @@ public ImageFromDockerfile build() {
}

private void validateConfiguration() {
if (isEmulatorEnabled(Emulator.AUTHENTICATION) && firebaseConfig.projectId().isEmpty()) {
if (isEmulatorEnabled(Emulator.AUTHENTICATION) && emulatorConfig.projectId().isEmpty()) {
throw new IllegalStateException("Can't create Firebase Auth emulator. Google Project id is required");
}

@@ -302,14 +310,14 @@ private void validateConfiguration() {
}

private void configureBaseImage() {
dockerBuilder.from(firebaseConfig.dockerConfig().imageName());
dockerBuilder.from(emulatorConfig.dockerConfig().imageName());
}

private void initialSetup() {
dockerBuilder
.run("apk --no-cache add openjdk11-jre bash curl openssl gettext nano nginx sudo && " +
"npm cache clean --force && " +
"npm i -g firebase-tools@" + firebaseConfig.firebaseVersion() + " && " +
"npm i -g firebase-tools@" + emulatorConfig.firebaseVersion() + " && " +
"deluser nginx && delgroup abuild && delgroup ping && " +
"mkdir -p " + FIREBASE_ROOT + " && " +
"mkdir -p " + FIREBASE_HOSTING_PATH + " && " +
@@ -338,17 +346,17 @@ private String downloadEmulatorCommand(Emulator emulator, String downloadId) {
}

private void authenticateToFirebase() {
firebaseConfig.token().ifPresent(token -> dockerBuilder.env("FIREBASE_TOKEN", token));
emulatorConfig.token().ifPresent(token -> dockerBuilder.env("FIREBASE_TOKEN", token));
}

private void setupJavaToolOptions() {
firebaseConfig.javaToolOptions().ifPresent(toolOptions -> dockerBuilder.env("JAVA_TOOL_OPTIONS", toolOptions));
emulatorConfig.javaToolOptions().ifPresent(toolOptions -> dockerBuilder.env("JAVA_TOOL_OPTIONS", toolOptions));
}

private void addFirebaseJson() {
dockerBuilder.workDir(FIREBASE_ROOT);

firebaseConfig.customFirebaseJson().ifPresentOrElse(
emulatorConfig.customFirebaseJson().ifPresentOrElse(
this::includeCustomFirebaseJson,
this::generateFirebaseJson);

@@ -362,122 +370,54 @@ private void includeCustomFirebaseJson(Path customFilePath) {
}

private void includeFirestoreFiles() {
firebaseConfig.firestoreConfig.rulesFile.ifPresent(rulesFile -> {
emulatorConfig.firebaseConfig().firestoreConfig.rulesFile.ifPresent(rulesFile -> {
this.dockerBuilder.add("firestore.rules", FIREBASE_ROOT + "/firestore.rules");
this.result.withFileFromPath("firestore.rules", rulesFile);
});

firebaseConfig.firestoreConfig.indexesFile.ifPresent(indexesFile -> {
emulatorConfig.firebaseConfig().firestoreConfig.indexesFile.ifPresent(indexesFile -> {
this.dockerBuilder.add("firestore.indexes.json", FIREBASE_ROOT + "/firestore.indexes.json");
this.result.withFileFromPath("firestore.indexes.json", indexesFile);
});
}

private void includeStorageFiles() {
firebaseConfig.storageConfig.rulesFile.ifPresent(rulesFile -> {
emulatorConfig.firebaseConfig().storageConfig.rulesFile.ifPresent(rulesFile -> {
this.dockerBuilder.add("storage.rules", FIREBASE_ROOT + "/storage.rules");
this.result.withFileFromPath("storage.rules", rulesFile);
});
}

private void generateFirebaseJson() {
var firebaseJsonBuilder = new FirebaseJsonBuilder(this.firebaseConfig);
String firebaseJson = null;
var firebaseJsonBuilder = new FirebaseJsonBuilder(this.emulatorConfig);
String firebaseJson;
try {
firebaseJson = firebaseJsonBuilder.buildFirebaseConfig();
} catch (IOException e) {
throw new IllegalStateException("Failed to generate firebase.json file", e);
}

// StringBuilder firebaseJson = new StringBuilder();
//
// firebaseJson.append("{\n");
// firebaseJson.append("\t\"emulators\": {\n");
//
// var emulatorsJson = this.devServices
// .entrySet()
// .stream()
// .filter(service -> service.getKey().configProperty != null)
// .map((service -> {
// var emulator = service.getKey();
//
// var port = Optional.ofNullable(service.getValue().fixedPort())
// .orElse(emulator.internalPort);
//
// String additionalConfig = "";
// if (emulator.equals(Emulator.CLOUD_FIRESTORE)) {
// var wsService = this.devServices.get(Emulator.CLOUD_FIRESTORE_WS);
// if (wsService != null) {
// var wsPort = Optional.ofNullable(wsService.fixedPort)
// .orElse(Emulator.CLOUD_FIRESTORE.internalPort);
// additionalConfig = "\t\t\t\"websocketPort\": " + wsPort + ",\n";
// }
// }
//
// return "\t\t\"" + emulator.configProperty + "\": {\n" +
// "\t\t\t\"port\": " + port + ",\n" +
// additionalConfig +
// "\t\t\t\"host\": \"0.0.0.0\"\n" +
// "\t\t}";
// }))
// .collect(Collectors.joining(",\n"));
// firebaseJson.append(emulatorsJson).append("\n");
// firebaseJson.append("\t}\n");
//
// if (isEmulatorEnabled(Emulator.CLOUD_FIRESTORE)) {
// var firestoreJson = ",\"firestore\": {";
//
// var files = Stream.of(
// firebaseConfig.firestoreConfig()
// .rulesFile
// .map(rulesFile -> "\t\"rules\": \"" + rulesFile + "\""),
// firebaseConfig.firestoreConfig()
// .indexesFile
// .map(indexesFile -> "\t\"indexes\": \"" + indexesFile + "\"")
// )
// .filter(Optional::isPresent)
// .map(Optional::get)
// .collect(Collectors.joining(",\n"));
//
// firestoreJson += files;
// firestoreJson += "}\n";
// firebaseJson.append(firestoreJson);
// }
//
// if (isEmulatorEnabled(Emulator.CLOUD_STORAGE)) {
// var storageJson = ",\"storage\": {";
//
// storageJson += firebaseConfig.storageConfig.rulesFile().map(rulesFile ->
// "\n\t\"rules\": \"" + rulesFile + "\"\n"
// );
//
// storageJson += "}\n";
// firebaseJson.append(storageJson);
// }
//
// firebaseJson.append("}\n");

this.result.withFileFromString("firebase.json", firebaseJson.toString());
this.result.withFileFromString("firebase.json", firebaseJson);
}

private void setupDataImportExport() {
firebaseConfig.emulatorData().ifPresent(emulator -> this.dockerBuilder.volume(EMULATOR_DATA_PATH));
emulatorConfig.emulatorData().ifPresent(emulator -> this.dockerBuilder.volume(EMULATOR_DATA_PATH));
}

private void setupHosting() {
// Specify public directory if hosting is enabled
if (firebaseConfig.hostingConfig().hostingContentDir().isPresent()) {
if (emulatorConfig.firebaseConfig().hostingConfig().hostingContentDir().isPresent()) {
this.dockerBuilder.volume(FIREBASE_HOSTING_PATH);
}
}

private void setupUserAndGroup() {
loicmathieu marked this conversation as resolved.
Show resolved Hide resolved
var commands = new ArrayList<String>();

firebaseConfig.dockerConfig.groupId().ifPresent(group -> commands.add("addgroup -g " + group + " runner"));
emulatorConfig.dockerConfig.groupId().ifPresent(group -> commands.add("addgroup -g " + group + " runner"));

firebaseConfig.dockerConfig.userId().ifPresent(user -> {
var groupName = firebaseConfig.dockerConfig().groupId().map(i -> "runner").orElse("node");
emulatorConfig.dockerConfig.userId().ifPresent(user -> {
var groupName = emulatorConfig.dockerConfig().groupId().map(i -> "runner").orElse("node");
commands.add("adduser -u " + user + " -G " + groupName + " -D -h /srv/firebase runner");
});

@@ -494,26 +434,26 @@ private void setupUserAndGroup() {
}

private int dockerUser() {
return firebaseConfig.dockerConfig().userId().orElse(1000);
return emulatorConfig.dockerConfig().userId().orElse(1000);
}

private int dockerGroup() {
return firebaseConfig.dockerConfig().groupId().orElse(1000);
return emulatorConfig.dockerConfig().groupId().orElse(1000);
}

private void runExecutable() {
List<String> arguments = new ArrayList<>();

arguments.add("emulators:start");

firebaseConfig.projectId()
emulatorConfig.projectId()
.map(id -> "--project")
.ifPresent(arguments::add);

firebaseConfig.projectId()
emulatorConfig.projectId()
.ifPresent(arguments::add);

firebaseConfig
emulatorConfig
.emulatorData()
.map(path -> "--import")
.ifPresent(arguments::add);
@@ -522,12 +462,12 @@ private void runExecutable() {
* We write the data to a subdirectory of the mount point. The firebase emulator tries to remove and
* recreate the mount-point directory, which will obviously fail. By using a subdirectory, export succeeds.
*/
firebaseConfig
emulatorConfig
.emulatorData()
.map(path -> EMULATOR_EXPORT_PATH)
.ifPresent(arguments::add);

firebaseConfig
emulatorConfig
.emulatorData()
.map(path -> "--export-on-exit")
.ifPresent(arguments::add);
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
package io.quarkiverse.googlecloudservices.firebase.deployment.testcontainers;

import java.io.IOException;
import java.io.StringWriter;
import java.io.*;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.Optional;
import java.util.function.Consumer;

@@ -17,15 +17,24 @@ public class FirebaseJsonBuilder {

private static final String ALL_IP = "0.0.0.0";

private final FirebaseConfig root;
private final ObjectMapper objectMapper = new ObjectMapper();
private final FirebaseEmulatorContainer.EmulatorConfig emulatorConfig;
private final FirebaseConfig root;

public FirebaseJsonBuilder(FirebaseEmulatorContainer.EmulatorConfig emulatorConfig) {
this.emulatorConfig = emulatorConfig;
this.root = new FirebaseConfig();
}

public String buildFirebaseConfig() throws IOException {
generateFirebaseConfig();

StringWriter writer = new StringWriter();
objectMapper.writeValue(writer, root);
return writer.toString();
}

private void generateFirebaseConfig() {
// private Object database;
// private Object dataconnect;
configureEmulator();
@@ -35,11 +44,6 @@ public String buildFirebaseConfig() throws IOException {
// private Object hosting;
// private Remoteconfig remoteconfig;
configureStorage();

StringWriter writer = new StringWriter();
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.writeValue(writer, root);
return writer.toString();
}

private void configureEmulator() {
@@ -134,7 +138,7 @@ private void configureEmulator() {

private void withEmulator(FirebaseEmulatorContainer.Emulator emulator, Consumer<Integer> handler) {
if (isEmulatorEnabled(emulator)) {
var exposedPort = emulatorConfig.services().get(emulator);
var exposedPort = emulatorConfig.firebaseConfig().services().get(emulator);
var port = Optional.ofNullable(exposedPort.fixedPort())
.orElse(emulator.internalPort);

@@ -144,29 +148,29 @@ private void withEmulator(FirebaseEmulatorContainer.Emulator emulator, Consumer<

private void configureFirestore() {
if (isEmulatorEnabled(FirebaseEmulatorContainer.Emulator.CLOUD_FIRESTORE)) {
var firestore = new Firestore();
var firestore = new HashMap<String, String>(); // Generated sources can't handle anyOf yet
root.setFirestore(firestore);

emulatorConfig.firestoreConfig().rulesFile().ifPresent(rules -> {
emulatorConfig.firebaseConfig().firestoreConfig().rulesFile().ifPresent(rules -> {
var rulesFile = fileRelativeToCustomJsonOrDefault(rules, "firestore.rules");
// TODO: Add rules file
firestore.put("rules", rulesFile);
});

emulatorConfig.firestoreConfig().indexesFile().ifPresent(index -> {
emulatorConfig.firebaseConfig().firestoreConfig().indexesFile().ifPresent(index -> {
var indexFile = fileRelativeToCustomJsonOrDefault(index, "firestore.indexes.json");
// TODO add index file
firestore.put("indexes", indexFile);
});
}
}

private void configureStorage() {
if (isEmulatorEnabled(FirebaseEmulatorContainer.Emulator.CLOUD_STORAGE)) {
var storage = new Storage();
root.setStorage(storage);
emulatorConfig.firebaseConfig().storageConfig().rulesFile().ifPresent(rules -> {
var storage = new HashMap<String, String>(); // Generated sources can't handle anyOf yet
root.setStorage(storage);

emulatorConfig.storageConfig().rulesFile().ifPresent(rules -> {
var rulesFile = fileRelativeToCustomJsonOrDefault(rules, "storage.rules");
// TODO add rules file
storage.put("rules", rulesFile);
});
}
}
@@ -182,7 +186,7 @@ private String relativePath(Path firebaseJson, Path otherFile) {
}

private boolean isEmulatorEnabled(FirebaseEmulatorContainer.Emulator emulator) {
return this.emulatorConfig.services().containsKey(emulator);
return this.emulatorConfig.firebaseConfig().services().containsKey(emulator);
}

}
jfbenckhuijsen marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -544,7 +544,6 @@
},
"port": {
"type": [
"string",
"integer"
]
}
Original file line number Diff line number Diff line change
@@ -6,10 +6,11 @@
import java.util.Map;
import java.util.Optional;

import io.quarkiverse.googlecloudservices.firebase.deployment.testcontainers.FirebaseEmulatorContainer;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import io.quarkiverse.googlecloudservices.firebase.deployment.testcontainers.FirebaseEmulatorContainer;

class FirebaseEmulatorConfigBuilderTest {

private FirebaseEmulatorConfigBuilder configBuilder;
@@ -63,8 +64,7 @@ void setUp() {
new TestStorageDevService(
true,
Optional.empty(),
Optional.of("storage.rules"))
));
Optional.of("storage.rules"))));
configBuilder = new FirebaseEmulatorConfigBuilder(config);
}

@@ -80,10 +80,11 @@ void testBuild() {
assertPathEndsWith("firebase.json", emulatorConfig.customFirebaseJson().orElse(null));
assertEquals("-Xmx", emulatorConfig.javaToolOptions().orElse(null));
assertPathEndsWith("data", emulatorConfig.emulatorData().orElse(null));
assertPathEndsWith("public", emulatorConfig.hostingConfig().hostingContentDir().orElse(null));
assertPathEndsWith("storage.rules", emulatorConfig.storageConfig().rulesFile().orElse(null));
assertPathEndsWith("firestore.rules", emulatorConfig.firestoreConfig().rulesFile().orElse(null));
assertPathEndsWith("firestore.indexes.json", emulatorConfig.firestoreConfig().indexesFile().orElse(null));
assertPathEndsWith("public", emulatorConfig.firebaseConfig().hostingConfig().hostingContentDir().orElse(null));
assertPathEndsWith("storage.rules", emulatorConfig.firebaseConfig().storageConfig().rulesFile().orElse(null));
assertPathEndsWith("firestore.rules", emulatorConfig.firebaseConfig().firestoreConfig().rulesFile().orElse(null));
assertPathEndsWith("firestore.indexes.json",
emulatorConfig.firebaseConfig().firestoreConfig().indexesFile().orElse(null));

}

@@ -97,7 +98,7 @@ void testExposedEmulators() {
FirebaseEmulatorContainer.EmulatorConfig emulatorConfig = configBuilder.build();

Map<FirebaseEmulatorContainer.Emulator, FirebaseEmulatorContainer.ExposedPort> exposedPorts = emulatorConfig
.services();
.firebaseConfig().services();

assertEquals(10, exposedPorts.size());
assertEquals(6000, exposedPorts.get(FirebaseEmulatorContainer.Emulator.EMULATOR_SUITE_UI).fixedPort());
@@ -191,8 +192,7 @@ record TestStorage(
record TestStorageDevService(
boolean enabled,
Optional<Integer> emulatorPort,
Optional<String> rulesFile
) implements FirebaseDevServiceConfig.Storage.StorageDevService {
Optional<String> rulesFile) implements FirebaseDevServiceConfig.Storage.StorageDevService {

}

Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package io.quarkiverse.googlecloudservices.firebase.deployment;

import static org.junit.jupiter.api.Assertions.assertTrue;

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.util.Optional;

import org.junit.jupiter.api.Test;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;

import com.google.firebase.FirebaseOptions;

import io.quarkiverse.googlecloudservices.firebase.deployment.testcontainers.CustomFirebaseConfigReader;
import io.quarkiverse.googlecloudservices.firebase.deployment.testcontainers.FirebaseEmulatorContainer;

@Testcontainers
public class FirebaseEmulatorContainerCustomConfigTest {

private static final File tempEmulatorDataDir;

static {
try {
// Create a temporary directory for emulator data
tempEmulatorDataDir = Files.createTempDirectory("firebase-emulator-data").toFile();
firebaseContainer = new TestableFirebaseEmulatorContainer(
new FirebaseEmulatorContainer.EmulatorConfig(
new FirebaseEmulatorContainer.DockerConfig(
"node:23-alpine",
TestableFirebaseEmulatorContainer.user,
TestableFirebaseEmulatorContainer.group),
"latest", // Firebase version
Optional.of("demo-test-project"),
Optional.empty(),
Optional.of(new File("firebase.json").toPath()),
Optional.empty(),
Optional.of(tempEmulatorDataDir.toPath()),
new CustomFirebaseConfigReader().readFromFirebase(new File("firebase.json").toPath())),
"FirebaseEmulatorContainerCustomConfigTest") {

@Override
protected void createFirebaseOptions(FirebaseOptions.Builder builder) {
}
};

} catch (IOException e) {
throw new IllegalStateException(e);
}
}

@Container
private static final FirebaseEmulatorContainer firebaseContainer;

@Test
public void testFirestoreRulesAndIndexes() throws InterruptedException, IOException {
// Verify the firebase.json file exists in the container
String firebaseJsonCheck = firebaseContainer.execInContainer("cat", "/srv/firebase/firebase.json").getStdout();
assertTrue(firebaseJsonCheck.contains("\"emulators\""), "Expected firebase.json to be present in the container");

// Verify the firestore.rules file exists in the container
String firestoreRulesCheck = firebaseContainer.execInContainer("cat", "/srv/firebase/firestore.rules").getStdout();
assertTrue(firestoreRulesCheck.contains("service cloud.firestore"),
"Expected firestore.rules to be present in the container");
}

@Test
public void testStorageRules() throws IOException, InterruptedException {
// Verify the storage.rules file exists in the container
String storageRulesCheck = firebaseContainer.execInContainer("cat", "/srv/firebase/storage.rules").getStdout();
assertTrue(storageRulesCheck.contains("service firebase.storage"),
"Expected storage.rules to be present in the container");
}

}
Original file line number Diff line number Diff line change
@@ -13,11 +13,10 @@
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;

import io.quarkiverse.googlecloudservices.firebase.deployment.testcontainers.FirebaseEmulatorContainer;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.testcontainers.containers.output.OutputFrame;
import org.testcontainers.junit.jupiter.Testcontainers;

import com.google.api.core.ApiFuture;
@@ -31,7 +30,6 @@
import com.google.cloud.pubsub.v1.TopicAdminSettings;
import com.google.cloud.storage.*;
import com.google.cloud.storage.Blob;
import com.google.firebase.FirebaseApp;
import com.google.firebase.FirebaseOptions;
import com.google.firebase.auth.FirebaseAuth;
import com.google.firebase.auth.FirebaseAuthException;
@@ -44,19 +42,33 @@

import io.grpc.ManagedChannel;
import io.grpc.ManagedChannelBuilder;
import io.quarkiverse.googlecloudservices.firebase.deployment.testcontainers.FirebaseEmulatorContainer;
import io.quarkiverse.googlecloudservices.firebase.deployment.testcontainers.FirebaseEmulatorContainer.Emulator;
import io.quarkiverse.googlecloudservices.firebase.deployment.testcontainers.FirebaseEmulatorContainer.EmulatorConfig;
import io.quarkiverse.googlecloudservices.firebase.deployment.testcontainers.FirebaseEmulatorContainer.ExposedPort;

@Testcontainers
public class FirebaseEmulatorContainerIntegrationTest {

private static final FirebaseEmulatorContainer firebaseContainer;
private static final File tempEmulatorDataDir;
private static final File tempHostingContentDir;

private static final String emulatorHost;
private static final FirebaseApp app;
static {
try {
// Create a temporary directory for emulator data
tempEmulatorDataDir = Files.createTempDirectory("firebase-emulator-data").toFile();
tempHostingContentDir = Files.createTempDirectory("firebase-hosting-content").toFile();

// Create a static HTML file in the hosting directory
File indexFile = new File(tempHostingContentDir, "index.html");
try (FileWriter writer = new FileWriter(indexFile)) {
writer.write("<html><body><h1>Hello, Firebase Hosting!</h1></body></html>");
}

} catch (IOException e) {
throw new IllegalStateException(e);
}
}

private static final Map<Emulator, ExposedPort> SERVICES = Map.of(
Emulator.AUTHENTICATION, new ExposedPort(6000),
@@ -72,97 +84,45 @@ Emulator.EMULATOR_SUITE_UI, new ExposedPort(6009),
Emulator.EMULATOR_HUB, new ExposedPort(6010),
Emulator.LOGGING, new ExposedPort(6011));

static {
try {
/*
* We determine the current group and user using an env variable. This is set by the GitHub Actions runner.
* The user and group are used to set the user/group for the user in the docker container run by
* TestContainers for the Firebase Emulators. This way, the data exported by the Firebase Emulators
* can be read from the build.
*/
var user = Optional
.ofNullable(System.getenv("CURRENT_USER"))
.map(Integer::valueOf);
var group = Optional
.ofNullable(System.getenv("CURRENT_GROUP"))
.map(Integer::valueOf);

System.out.println("Running as user " + user + " and group " + group);

// Create a temporary directory for emulator data
tempEmulatorDataDir = Files.createTempDirectory("firebase-emulator-data").toFile();
tempHostingContentDir = Files.createTempDirectory("firebase-hosting-content").toFile();

// Create a static HTML file in the hosting directory
File indexFile = new File(tempHostingContentDir, "index.html");
try (FileWriter writer = new FileWriter(indexFile)) {
writer.write("<html><body><h1>Hello, Firebase Hosting!</h1></body></html>");
}

EmulatorConfig config = new EmulatorConfig(
private static final TestableFirebaseEmulatorContainer firebaseContainer = new TestableFirebaseEmulatorContainer(
new EmulatorConfig(
new FirebaseEmulatorContainer.DockerConfig(
"node:23-alpine",
user,
group),
TestableFirebaseEmulatorContainer.user,
TestableFirebaseEmulatorContainer.group),
"latest", // Firebase version
Optional.of("demo-test-project"),
Optional.empty(),
Optional.empty(),
Optional.empty(),
Optional.of(tempEmulatorDataDir.toPath()),
new FirebaseEmulatorContainer.HostingConfig(
Optional.of(tempHostingContentDir.toPath())
),
new FirebaseEmulatorContainer.StorageConfig(
Optional.empty()
),
new FirebaseEmulatorContainer.FirestoreConfig(
Optional.empty(),
Optional.empty()
),
SERVICES);

firebaseContainer = new FirebaseEmulatorContainer(config);
firebaseContainer.start();

firebaseContainer.followOutput(FirebaseEmulatorContainerIntegrationTest::writeToStdOut,
OutputFrame.OutputType.STDOUT);
firebaseContainer.followOutput(FirebaseEmulatorContainerIntegrationTest::writeToStdErr,
OutputFrame.OutputType.STDERR);

emulatorHost = firebaseContainer.getHost();

int dbPort = firebaseContainer.emulatorPort(Emulator.REALTIME_DATABASE);

var firebaseBuilder = FirebaseOptions.builder()
.setProjectId("demo-test-project")
.setCredentials(new EmulatorCredentials())
.setDatabaseUrl("http://" + emulatorHost + ":" + dbPort + "?ns=demo-test-project");

FirebaseOptions options = firebaseBuilder.build();
app = FirebaseApp.initializeApp(options);
} catch (IOException e) {
throw new IllegalStateException(e);
new FirebaseEmulatorContainer.FirebaseConfig(
new FirebaseEmulatorContainer.HostingConfig(
Optional.of(tempHostingContentDir.toPath())),
new FirebaseEmulatorContainer.StorageConfig(
Optional.empty()),
new FirebaseEmulatorContainer.FirestoreConfig(
Optional.empty(),
Optional.empty()),
SERVICES)),
"FirebaseEmulatorContainerIntegrationTest") {
@Override
protected void createFirebaseOptions(FirebaseOptions.Builder builder) {
var emulatorHost = firebaseContainer.getHost();
var dbPort = firebaseContainer.emulatorPort(Emulator.REALTIME_DATABASE);

builder.setDatabaseUrl("http://" + emulatorHost + ":" + dbPort + "?ns=demo-test-project");
}
}
};

private static void writeToStdOut(OutputFrame frame) {
writeOutputFrame(frame, System.out);
}

private static void writeToStdErr(OutputFrame frame) {
writeOutputFrame(frame, System.err);
}

private static void writeOutputFrame(OutputFrame frame, PrintStream output) {
output.println(frame.getUtf8StringWithoutLineEnding());
@BeforeAll
public static void setup() {
firebaseContainer.start();
}

@AfterAll
public static void tearDown() {
if (firebaseContainer != null) {
firebaseContainer.stop();
}
firebaseContainer.stop();

validateEmulatorDataWritten();

@@ -209,7 +169,7 @@ public void testFirebaseHostingEmulatorConnection() throws Exception {
int hostingPort = firebaseContainer.emulatorPort(Emulator.FIREBASE_HOSTING);

// Construct URL for the hosted file
URL url = new URL("http://" + emulatorHost + ":" + hostingPort + "/index.html");
URL url = new URL("http://" + firebaseContainer.getHost() + ":" + hostingPort + "/index.html");

// Fetch content from the URL
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
@@ -230,10 +190,10 @@ public void testFirebaseAuthenticationEmulatorConnection() throws FirebaseAuthEx
int authPort = firebaseContainer.emulatorPort(Emulator.AUTHENTICATION);

// Set the environment variable for the Firebase Authentication emulator
FirebaseProcessEnvironment.setenv("FIREBASE_AUTH_EMULATOR_HOST", emulatorHost + ":" + authPort);
FirebaseProcessEnvironment.setenv("FIREBASE_AUTH_EMULATOR_HOST", firebaseContainer.getHost() + ":" + authPort);

// Initialize FirebaseOptions without setting the auth emulator host directly
FirebaseAuth auth = FirebaseAuth.getInstance(app);
FirebaseAuth auth = FirebaseAuth.getInstance(firebaseContainer.getApp());

// Create a test user and verify it
UserRecord.CreateRequest request = new UserRecord.CreateRequest()
@@ -254,7 +214,7 @@ public void testFirestoreEmulatorConnection() throws ExecutionException, Interru

FirestoreOptions options = FirestoreOptions.newBuilder()
.setProjectId("demo-test-project")
.setEmulatorHost(emulatorHost + ":" + firestorePort)
.setEmulatorHost(firebaseContainer.getHost() + ":" + firestorePort)
.setCredentials(new EmulatorCredentials())
.build();
Firestore firestore = options.getService();
@@ -269,7 +229,7 @@ public void testFirestoreEmulatorConnection() throws ExecutionException, Interru

@Test
public void testRealtimeDatabaseEmulatorConnection() throws ExecutionException, InterruptedException {
DatabaseReference ref = FirebaseDatabase.getInstance(app).getReference("testData");
DatabaseReference ref = FirebaseDatabase.getInstance(firebaseContainer.getApp()).getReference("testData");

// Write data to the database
ref.setValueAsync("testValue").get();
@@ -302,7 +262,7 @@ public void testPubSubEmulatorConnection() throws Exception {
int pubSubPort = firebaseContainer.emulatorPort(Emulator.PUB_SUB);

// Set up a gRPC channel to the Pub/Sub emulator
ManagedChannel channel = ManagedChannelBuilder.forAddress(emulatorHost, pubSubPort)
ManagedChannel channel = ManagedChannelBuilder.forAddress(firebaseContainer.getHost(), pubSubPort)
.usePlaintext()
.build();

@@ -340,7 +300,7 @@ public void testStorageEmulatorConnection() {
int storagePort = firebaseContainer.emulatorPort(Emulator.CLOUD_STORAGE);

Storage storage = StorageOptions.newBuilder()
.setHost("http://" + emulatorHost + ":" + storagePort)
.setHost("http://" + firebaseContainer.getHost() + ":" + storagePort)
.setProjectId("demo-test-project")
.setCredentials(NoCredentials.getInstance())
.build().getService();
@@ -367,7 +327,7 @@ public void testEmulatorUIReachable() throws Exception {
int uiPort = firebaseContainer.emulatorPort(Emulator.EMULATOR_SUITE_UI);

// Construct the URL for the Emulator UI root (where index.html would be served)
URL url = new URL("http://" + emulatorHost + ":" + uiPort + "/");
URL url = new URL("http://" + firebaseContainer.getHost() + ":" + uiPort + "/");

// Open a connection and send an HTTP GET request
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
@@ -387,7 +347,7 @@ public void testEmulatorHub() throws Exception {
int uiPort = firebaseContainer.emulatorPort(Emulator.EMULATOR_HUB);

// Construct the URL for the Emulator UI root (where index.html would be served)
URL url = new URL("http://" + emulatorHost + ":" + uiPort + "/emulators");
URL url = new URL("http://" + firebaseContainer.getHost() + ":" + uiPort + "/emulators");

// Open a connection and send an HTTP GET request
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package io.quarkiverse.googlecloudservices.firebase.deployment;

import java.io.PrintStream;
import java.util.Optional;

import org.testcontainers.containers.output.OutputFrame;

import com.google.firebase.FirebaseApp;
import com.google.firebase.FirebaseOptions;
import com.google.firebase.internal.EmulatorCredentials;

import io.quarkiverse.googlecloudservices.firebase.deployment.testcontainers.FirebaseEmulatorContainer;

/**
* Subclass of {@link FirebaseEmulatorContainer} which has some extra facilities to ease testing. Functionally
* this class is equivalent of its superclass with respect to the testing we need to perform.
*/
public abstract class TestableFirebaseEmulatorContainer extends FirebaseEmulatorContainer {

/*
* We determine the current group and user using an env variable. This is set by the GitHub Actions runner.
* The user and group are used to set the user/group for the user in the docker container run by
* TestContainers for the Firebase Emulators. This way, the data exported by the Firebase Emulators
* can be read from the build.
*/
protected static Optional<Integer> user = Optional
.ofNullable(System.getenv("CURRENT_USER"))
.map(Integer::valueOf);
protected static Optional<Integer> group = Optional
.ofNullable(System.getenv("CURRENT_GROUP"))
.map(Integer::valueOf);

static {
System.out.println("Running as user " + user + " and group " + group);
}

private final String name;
private FirebaseApp app;

/**
* Creates a new Firebase Emulator container
*
* @param firebaseConfig The generic configuration of the firebase emulators
* @param name The name of the firebase app (must be unique across the JVM).
*/
public TestableFirebaseEmulatorContainer(EmulatorConfig firebaseConfig, String name) {
super(firebaseConfig);
this.name = name;
}

@Override
public void start() {
super.start();

followOutput(this::writeToStdOut, OutputFrame.OutputType.STDOUT);
followOutput(this::writeToStdErr, OutputFrame.OutputType.STDERR);

var firebaseBuilder = FirebaseOptions.builder()
.setProjectId("demo-test-project")
.setCredentials(new EmulatorCredentials());

createFirebaseOptions(firebaseBuilder);

FirebaseOptions options = firebaseBuilder.build();
app = FirebaseApp.initializeApp(options, name);
}

protected abstract void createFirebaseOptions(FirebaseOptions.Builder builder);

private void writeToStdOut(OutputFrame frame) {
writeOutputFrame(frame, System.out);
}

private void writeToStdErr(OutputFrame frame) {
writeOutputFrame(frame, System.err);
}

private void writeOutputFrame(OutputFrame frame, PrintStream output) {
output.println(frame.getUtf8StringWithoutLineEnding());
}

public FirebaseApp getApp() {
return app;
}
}
10 changes: 10 additions & 0 deletions firebase-devservices/deployment/storage.rules
jfbenckhuijsen marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
service firebase.storage {
match /b/{bucket}/o {
match /company/{allPaths=**} {
allow read: if true
}
match /building/{allPaths=**} {
allow read: if true
}
}
}