Skip to content

feat: added firebase module #2954

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

Open
wants to merge 25 commits into
base: main
Choose a base branch
from
Open

feat: added firebase module #2954

wants to merge 25 commits into from

Conversation

xytis
Copy link

@xytis xytis commented Jan 30, 2025

What does this PR do?

This module allows running firebase emulator as a testcontainer.

It does currently depend on a forked docker image, residing under my ownership. There is no official firebase emulator docker image to my knowledge.

Why is it important?

We use firebase, thought someone else might be using firebase too.

@xytis xytis requested a review from a team as a code owner January 30, 2025 18:46
Copy link

netlify bot commented Jan 30, 2025

Deploy Preview for testcontainers-go ready!

Name Link
🔨 Latest commit fd6035e
🔍 Latest deploy log https://app.netlify.com/sites/testcontainers-go/deploys/680b7dcc6dca280008bd401e
😎 Deploy Preview https://deploy-preview-2954--testcontainers-go.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify site configuration.

Copy link
Contributor

@stevenh stevenh left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the PR, I've done a initial pass. The majority of comments center around bringing it inline with the latest template for modules in terms of style and correct behaviour e.g. handling non nil container with error.

// WithRoot sets the directory which is copied to the destination container as firebase root
func WithRoot(rootPath string) testcontainers.CustomizeRequestOption {
return func(req *testcontainers.GenericContainerRequest) error {
if !strings.HasSuffix(rootPath, "/firebase") {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: Could you clarify why this requirement is needed?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This has been an issue a year ago. Mounting did not correctly work at that time, and if the source directory did not match the destination directory, mounting would nest the directory instead of mapping it directly.

I see that the issue has been fixed, removing.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, my bad. The issue still exits. I wrote a failing test to exhibit the issue.

Is there something incorrect with the way I copy over the configuration?

Also, I don't think that volume mount would work in this case, because firebase emulator creates a lot of trash data in the root catalog, which in some cases can even make testing flaky (especially if using DATA_DIRECTORY as a fixture).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll try to find some time to look. I had a some in progress work to fix up the copy behaviour which was odd.

If you have a test to exercise this that, would help alongside a description of the desired behaviour and what you're seeing instead.

return fmt.Sprintf("%s:%s", host, port.Port()), nil
}

func (c *FirebaseContainer) UIConnectionString(ctx context.Context) (string, error) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: seems like lots of additional API surface area instead of just exposing ConnectionString, thoughts?

If not then we'd need comments for all the methods.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This one is a bit more tricky.

I would love to change all of these methods, and figure out a better way to configure the launched container.

Currently I ship a catalog which contains static predefined configuration for the emulator. These methods happen to match the port numbers defined in the said file.

But, in the tests that we use, we provide a completely different setup (because we don't want to boot the full emulation suite), and we have to match the port numbers in our config so that all of this works.

So a question from my side would be:
Is there a way to generate file based config during container setup? To my knowledge firebase emulator does not allow passing those values as environment variables.

I think, ideal use example would be this:

	firebaseContainer, err := firebase.Run(ctx, 
	  "ghcr.io/u-health/docker-firebase-emulator:13.29.2",
	  firebase.WithUI(),
	  firebase.WithFirestore(),
	  firebase.WithCache(),
	)

And then, helper methods XXXConnectionString would actually return meaningful errors if the emulator part was not launched.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a way to generate file based config during container setup?

@xytis you can take a look at the redpanda module, where there is a Go template the module is using to build the configuration file.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After some testing on my side, I remembered why I shipped the config as a directory in the first place.

Default (and probably expected) use case for firebase projects, is to run firebase init and then use the scaffolded configuration to interact with the dependencies.

In the following commit, I've changed the behaviour of this module. It now attempts to understand the emulator configuration of your project and then boots the containerised emulator with that info.

At least in my use case, that makes sense, and when I launch the tests, I use the same project-wide config for all of them.

func (c *FirebaseContainer) connectionString(ctx context.Context, portName nat.Port) (string, error) {
host, err := c.Host(ctx)
if err != nil {
return "", err
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: wrap the error so the user knows the cause, more below.

Copy link
Contributor

@stevenh stevenh left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the updates

Copy link
Member

@mdelapenya mdelapenya left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added a few minor comments, we are in the good way.

Thanks for your contribution


### Container Methods

The Firebase container exposes the following methods:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

docs: we probably need to append here the new methods the container expose

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@xytis let's not forget adding here ConnectionString 🙏

E.g. `Run(context.Background(), "ghcr.io/u-health/docker-firebase-emulator:13.29.2")`.

{% include "../features/common_functional_options.md" %}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

docsd: we need to add here all the functional options for the module.

Copy link
Member

@mdelapenya mdelapenya Apr 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@xytis let's not forget adding the WithData option here 🙏

@mdelapenya
Copy link
Member

One question: reading the README in the docker image:

Supports GRPC for firestore and pub/sub.

What are the differences with the existing Google Cloud emulators? We do have them under the gcloud module, and we currently plan to follow the same package structure as in the upcoming azure module (see #3008). Do you think this firebase has enough entity to be a separate module, or should it live alongside the other gcloud modules?

@xytis
Copy link
Author

xytis commented Mar 13, 2025

Last time I checked, gcloud emulators did not provide the full suite of emulations. I will check this again and come back to you.

@mdelapenya
Copy link
Member

@xytis we merged the layout revamp in the gcloud module. Do you think we can move this there?

@xytis
Copy link
Author

xytis commented Apr 3, 2025

Hey,

unfortunately not.

gcloud emulators do not include the following:

  • firebase auth
  • firebase storage (which is an access control layer on top of gcloud storage)
  • firebase pubsub (again, a layer on top, with some helpers)

I am not certain about the rest.

@mdelapenya
Copy link
Member

mdelapenya commented Apr 3, 2025

gcloud emulators do not include the following:

but we're going to use the ghcr.io/u-health/docker-firebase-emulator:13.29.2 Docker image, which has them. So I'd like to know if it's "semantically" correct to locate this code under the gcloud module, so it's distributed with all GCP services, or if on the contrary, it makes more sense to have it as a separate module.

I do not have an opinion on this, so as the author of the image, please feel free to be prescriptive about it. 🙏

@xytis
Copy link
Author

xytis commented Apr 3, 2025

Well, if I was searching for "firebase" emulators, I would not look under "gcloud". Mostly because the tooling and the documentation (both provided by google) are very different for these two products.

I think, having these two separate makes more sense as long as there are development differences between running gcloud and firebase executables.

@mdelapenya
Copy link
Member

LGTM then to continue as a separate module, thanks! 🙏

I can help you out rebasing this PR resolving the conflicts before moving on with the review

@xytis
Copy link
Author

xytis commented Apr 3, 2025

Please do, I am a bit occupied at the moment.

Thanks.

* main: (91 commits)
  chore(deps): bump github/codeql-action from 3.28.13 to 3.28.15 (testcontainers#3097)
  chore(deps): bump golang.org/x/crypto from 0.31.0 to 0.37.0 (testcontainers#3098)
  feat(aerospike): add Aerospike module (testcontainers#3094)
  security(compose): upgrade github.com/docker/compose/v2 to fix security vulnerability (testcontainers#3095)
  feat: add more functional options to the modules API (testcontainers#3070)
  chore(deps): bump golang.org/x/net in /modules/arangodb (testcontainers#3087)
  feat: add arangodb module (testcontainers#3083)
  chore(deps): bump actions/upload-artifact from 4.6.0 to 4.6.2 (testcontainers#3086)
  chore(deps): bump SonarSource/sonarqube-scan-action from 5.0.0 to 5.1.0 (testcontainers#3085)
  feat: add socat container (testcontainers#3071)
  fix(mssql): reduce flakiness in tests (testcontainers#3084)
  chore: bump golangci-lint to v2 (testcontainers#3082)
  chore(gcloud): deprecate old gcp containers, creating subpackages for them (testcontainers#3063)
  fix(mongodb): replica set initialization & connection handling (testcontainers#2984)
  chore(deps): bump docker/setup-docker-action from 4.2.0 to 4.3.0 (testcontainers#3077)
  chore(deps): bump github/codeql-action from 3.28.12 to 3.28.13 (testcontainers#3078)
  chore(deps): bump tj-actions/changed-files from 45.0.4 to 46.0.3 (testcontainers#3076)
  docs: add dependabot configuration (testcontainers#3074)
  chore(deps): replace `golang.org/x/exp/slices` with stdlib (testcontainers#3075)
  fix(dind): use docker image load (testcontainers#3073)
  ...
@mdelapenya
Copy link
Member

@xytis I cannot push to your branch because this PR was submitted from your main branch 😞

I've pushed it to my origin so that you can check and pick up the commits: https://github.com/testcontainers/testcontainers-go/compare/main...mdelapenya:testcontainers-go:firebase-module?expand=1

I still have doubts about the WithData option, probably as a result of being this one my first interaction with Firebase. Could you help me out in understanding its motivation? I'd need some tests in the code to verify it works as expected.

@xytis
Copy link
Author

xytis commented Apr 14, 2025

@mdelapenya merged your changes.

I was forced to use WithData because Firebase emulators expect a specific directory structure and files to exist in the "project root".

I toyed with providing structured config, but I am not using that in my own tests. It was easier to bootstrap a directory with a specific catalog structure, and I just clone that into the container.

Rough docs on this are here: https://firebase.google.com/docs/emulator-suite/install_and_configure

@mdelapenya
Copy link
Member

@xytis lint is complaining. Could you run make lint from the firebase module dir? 🙏


go 1.23.0

require (
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: just before submitting the changes, run go mod tidy from the module dir to make sure all dependencies are in sync with the core. Else we'll receive an extra PR from dependabot 😅

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

go mod tidy returned no changes. Was this expected?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably no changes are missing. I just want to double check we are not missing recent updates in the main branch.

@mdelapenya
Copy link
Member

@xytis for some reason, the lint still fails:

  Error: modules/firebase/modules/firebase/firebase_test.go:13:1: File is not properly formatted (gci)
  
  ^
  Error: modules/firebase/modules/firebase/options.go:7:1: File is not properly formatted (gofumpt)

Once fixed, and the docs are updated (see #2954 (comment) and #2954 (comment)), I think we are good to merge.

mdelapenya
mdelapenya previously approved these changes Apr 23, 2025
Copy link
Member

@mdelapenya mdelapenya left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm approving this PR already, although there are some comments to be addressed before we merge it:

  • documentation adding the WithData options and the ConnectionString container method
  • Steven's suggestion regarding wrapping errors.

Other than that, LGTM, thanks so much for your patience during the review, we do appreciate your hard work with Testcontainers and the Firebase emulator. 🙇

func (c *Container) ConnectionString(ctx context.Context, portName nat.Port) (string, error) {
host, err := c.Host(ctx)
if err != nil {
return "", err
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return "", err
return "", fmt.Errorf("host: %w", err)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@xytis friendly ping! 🙏

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll get to this once I have some time :)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm cutting a release today. Do you mind if, as this PR was sent from your main branch and I cannot contribute to it, I close this PR and submit another PR with your commits and my suggestions on top? Then I can incorporate this new module in today's release

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, sure. Do as you wish :)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Honestly, I am fine if you took the code and rebased into whatever location you need it to be :)

Just note that the issue which was blocking this module is still here:

I can not copy directories into container, if the source dir name and the dest dir name are not matching.

There is a test/example for that, but I placed an inverse condition for some reason.

I'll revert it now, so it would be clear why this is lagging for so long.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed, that limitation exists at the moment. My question now would be: why would the users of the firebase module like to provide a wrong dir name? I can see two types of users:

  1. developers of the firebase emulator, who would need different failure modes while developing it
  2. developers using the emulator, who would not need that particular failure mode.

I guess developers in group 1 would love to have multiple root dirs with different use cases. As a workaround I can think of a previous step that copies those dirs to a (t.TempDir() + "/firebase"), and then copy that temporary dir to the container. We can add now an issue to testcontainers to resolve the issue as soon as possible and remove that workaround right after it's released.

Of course, as a firebase noob, I'm probably missing other concerns you may have clear. Please let me know what you think about this 🙏

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The only reason why someone would have issues with directory name is when you have different setups for different tests.

In that case, the workaround seems to be a good solution.

Copy link
Member

@mdelapenya mdelapenya Apr 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's do this: let the functional option do that automatically, storing the temp root dir in the container so that we add a new Terminate function that: 1) removes the temp dir, and 2) calls container.Terminate. Please honor the Terminate signature

@stevenh @xytis thoughts?

Co-authored-by: Manuel de la Peña <[email protected]>
Copy link
Contributor

@stevenh stevenh left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a quick pass as I'm busy today.

Port uint16 `json:"port,omitempty"`
} `json:"tasks,omitempty"`
}
type partialFirebaseConfig struct {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: add a blank line between types, suspect it will fail linting without.

ctx := context.Background()

firebaseContainer, err := firebase.Run(ctx, "ghcr.io/u-health/docker-firebase-emulator:13.29.2",
firebase.WithRoot(filepath.Join("testdata", "firebase")),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

info: using literal "testdata/firebase" works fine on all platforms as even though windows uses \ it also accepts /.

Comment on lines +49 to +52
host := hostF.String()
if host != "0.0.0.0" {
return nil, fmt.Errorf("config specified %s emulator host on non public ip: %s", name, host)
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: this seems to limit host to 0.0.0.0, if so why have the field? that said I'm not sure this check is needed, do you have an example of why it is?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the emulator config (which we found in the user provided directory) uses host value other than "0.0.0.0", it will launch that emulator listening on docker internal IP. So no traffic will be able to enter the emulator, leaving the users very much confused.

portF := emulator.FieldByName("Port")
websocketPortF := emulator.FieldByName("WebsocketPort")

if hostF != (reflect.Value{}) && !hostF.IsZero() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: use IsValid to simplify test, more below

Suggested change
if hostF != (reflect.Value{}) && !hostF.IsZero() {
if hostF.isValid() && !hostF.IsZero() {

if emulator.Kind() != reflect.Struct {
continue
}
name := v.Type().Field(i).Name
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: extract type lookup out of the loop as its invariant.

Comment on lines +100 to +108
bytes, err := io.ReadAll(cfg)
if err != nil {
return nil, fmt.Errorf("read firebase.json: %w", err)
}

var parsed partialFirebaseConfig
if err := json.Unmarshal(bytes, &parsed); err != nil {
return nil, fmt.Errorf("parse firebase.json: %w", err)
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: use a stream reader to be more memory efficient, its a small benefit but good practice.

if err != nil {
return nil, fmt.Errorf("gather ports: %w", err)
}
req.ExposedPorts = expectedExposedPorts
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: should we wait for all ports instead of the fragile log wait?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had the same idea, but now I don't remember why I reverted back to logs.

// What would be a solution here? Previously I just added a check that the root must
// end in "/firebase"... I could do the same.
testcontainers.CleanupContainer(t, ctr)
require.NoError(t, err)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bug: this should fail, as I understand it this is down to how our copy works or doesn't. I have an unfinished PR in this area here.

I would change this to check for err as expected and mark the test as skip for now.

Co-authored-by: Steven Hartland <[email protected]>
@mdelapenya
Copy link
Member

For reference, Google released an official image: firebase/firebase-tools#1644 (comment)

@xytis
Copy link
Author

xytis commented May 7, 2025

Ah, cool. I'll adapt to that container once I have some free time.

@mdelapenya
Copy link
Member

Fantastic! thanks for your time @xytis , we appreciate it!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants