Skip to content
Open
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
29b1bba
feat: added firebase module
xytis May 23, 2024
6a88123
chore: bump default container version
xytis Jan 30, 2025
4105b0a
fix: rewrote the module based on comments
xytis Jan 31, 2025
cc2bfa4
fix: another pass at comments
xytis Feb 2, 2025
7c24032
fix: ports are uint
xytis Feb 3, 2025
bc68bb3
Merge branch 'main' into u-health/main
mdelapenya Apr 14, 2025
bc33438
chore: bump testcontainers-go to latest
mdelapenya Apr 14, 2025
1a72008
chore: move to testdata
mdelapenya Apr 14, 2025
c2b8e2b
chore: use custom error when root is needed
mdelapenya Apr 14, 2025
ba0bdc0
fix: handle error in test
mdelapenya Apr 14, 2025
80298e5
chore: use new functional options
mdelapenya Apr 14, 2025
4e271db
fix: no need to pass labels, as they are added in the core
mdelapenya Apr 14, 2025
34409eb
chore: proper error message
mdelapenya Apr 14, 2025
fb2c0aa
chore: move options to its file
mdelapenya Apr 14, 2025
88cb16d
chore: simplify concat
mdelapenya Apr 14, 2025
aa4c29c
chore: add whilelines to separate concerns
mdelapenya Apr 14, 2025
458cc7c
docs: document the new options
mdelapenya Apr 14, 2025
ae27c74
merge: pull request #1 from mdelapenya/firebase-module
xytis Apr 14, 2025
ae49941
fix: lint pass
xytis Apr 23, 2025
cff37d9
fix: another lint pass
xytis Apr 23, 2025
9fe4988
Update modules/firebase/ports.go
xytis Apr 25, 2025
3ed1839
Update modules/firebase/ports.go
xytis Apr 25, 2025
d578817
Merge branch 'main' into main
xytis Apr 25, 2025
72c1f45
chore: reverted example test to failing state, so we could fix the ro…
xytis Apr 25, 2025
fd6035e
Update modules/firebase/ports.go
xytis Apr 25, 2025
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
6 changes: 5 additions & 1 deletion .vscode/.testcontainers-go.code-workspace
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,10 @@
"name": "module / etcd",
"path": "../modules/etcd"
},
{
"name": "module / firebase",
"path": "../modules/firebase"
},
{
"name": "module / gcloud",
"path": "../modules/gcloud"
Expand Down Expand Up @@ -238,4 +242,4 @@
"path": "../modulegen"
}
]
}
}
70 changes: 70 additions & 0 deletions docs/modules/firebase.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# Firebase

Not available until the next release of testcontainers-go <a href="https://github.com/testcontainers/testcontainers-go"><span class="tc-version">:material-tag: main</span></a>

## Introduction

The Testcontainers module for Firebase.

## Adding this module to your project dependencies

Please run the following command to add the Firebase module to your Go dependencies:

```
go get github.com/testcontainers/testcontainers-go/modules/firebase
```

## Usage example

<!--codeinclude-->
[Creating a Firebase container](../../modules/firebase/examples_test.go) inside_block:ExampleRun
<!--/codeinclude-->

## Module Reference

### Run function

- Not available until the next release of testcontainers-go <a href="https://github.com/testcontainers/testcontainers-go"><span class="tc-version">:material-tag: main</span></a>

The Firebase module exposes one entrypoint function to create the Firebase container, and this function receives three parameters:

```golang
func Run(ctx context.Context, img string, opts ...testcontainers.ContainerCustomizer) (*FirebaseContainer, error)
```

- `context.Context`, the Go context.
- `string`, the Docker image to use.
- `testcontainers.ContainerCustomizer`, a variadic argument for passing options.

### Container Options

When starting the Firebase container, you can pass options in a variadic way to configure it.

#### Image

If you need to set a different Firebase Docker image, you can set a valid Docker image as the second argument in the `Run` function.
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 🙏

#### WithRoot

- Not available until the next release of testcontainers-go <a href="https://github.com/testcontainers/testcontainers-go"><span class="tc-version">:material-tag: main</span></a>

If you need to set a different Firebase root directory, you can use the `WithRoot` option.
E.g. `firebase.Run(context.Background(), "ghcr.io/u-health/docker-firebase-emulator:13.29.2", firebase.WithRoot("testdata/firebase"))`.

!!! warning
The root directory must be a valid Firebase project directory, including the `firebase.json` file.

#### WithCache

- Not available until the next release of testcontainers-go <a href="https://github.com/testcontainers/testcontainers-go"><span class="tc-version">:material-tag: main</span></a>

If you need to enable the Firebase cache, you can use the `WithCache` option, which enables the binary cache based on the Testcontainers SessionID (meaningful only when multiple tests are used).

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

### 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 🙏

1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ nav:
- modules/dynamodb.md
- modules/elasticsearch.md
- modules/etcd.md
- modules/firebase.md
- modules/gcloud.md
- modules/grafana-lgtm.md
- modules/inbucket.md
Expand Down
5 changes: 5 additions & 0 deletions modules/firebase/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
include ../../commons-test.mk

.PHONY: test
test:
$(MAKE) test-firebase
87 changes: 87 additions & 0 deletions modules/firebase/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package firebase

// Based on https://github.com/firebase/firebase-tools/blob/master/src/firebaseConfig.ts

type emulatorsConfig struct {
SingleProjectMode bool `json:"singleProjectMode,omitempty"`

Auth struct {
Host string `json:"host,omitempty"`
Port uint16 `json:"port,omitempty"`
} `json:"auth,omitempty"`

Database struct {
Host string `json:"host,omitempty"`
Port uint16 `json:"port,omitempty"`
} `json:"database,omitempty"`

Firestore struct {
Host string `json:"host,omitempty"`
Port uint16 `json:"port,omitempty"`
WebsocketPort uint16 `json:"websocketPort,omitempty"`
} `json:"firestore,omitempty"`

Functions struct {
Host string `json:"host,omitempty"`
Port uint16 `json:"port,omitempty"`
} `json:"functions,omitempty"`

Hosting struct {
Host string `json:"host,omitempty"`
Port uint16 `json:"port,omitempty"`
} `json:"hosting,omitempty"`

AppHosting struct {
Host string `json:"host,omitempty"`
Port uint16 `json:"port,omitempty"`
StartCommand string `json:"startCommand,omitempty"`
RootDirectory string `json:"rootDirectory,omitempty"`
} `json:"apphosting,omitempty"`

PubSub struct {
Host string `json:"host,omitempty"`
Port uint16 `json:"port,omitempty"`
} `json:"pubsub,omitempty"`

Storage struct {
Host string `json:"host,omitempty"`
Port uint16 `json:"port,omitempty"`
} `json:"storage,omitempty"`

Logging struct {
Host string `json:"host,omitempty"`
Port uint16 `json:"port,omitempty"`
} `json:"logging,omitempty"`

Hub struct {
Host string `json:"host,omitempty"`
Port uint16 `json:"port,omitempty"`
} `json:"hub,omitempty"`

UI struct {
Enabled bool `json:"enabled,omitempty"`
Host string `json:"host,omitempty"`
Port uint16 `json:"port,omitempty"`
} `json:"ui,omitempty"`

EventArc struct {
Host string `json:"host,omitempty"`
Port uint16 `json:"port,omitempty"`
} `json:"eventarc,omitempty"`

DataConnect struct {
Host string `json:"host,omitempty"`
Port uint16 `json:"port,omitempty"`
PostgresHost string `json:"postgresHost,omitempty"`
PostgresPort uint16 `json:"postgresPort,omitempty"`
DataDir string `json:"dataDir,omitempty"`
} `json:"dataconnect,omitempty"`

Tasks struct {
Host string `json:"host,omitempty"`
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.

Emulators emulatorsConfig `json:"emulators,omitempty"`
}
40 changes: 40 additions & 0 deletions modules/firebase/examples_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package firebase_test

import (
"context"
"fmt"
"log"
"path/filepath"

"github.com/testcontainers/testcontainers-go"
"github.com/testcontainers/testcontainers-go/modules/firebase"
)

func ExampleRun() {
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 /.

firebase.WithCache(),
)
defer func() {
if err := testcontainers.TerminateContainer(firebaseContainer); err != nil {
log.Printf("failed to terminate container: %s", err)
}
}()
if err != nil {
log.Printf("failed to start container: %s", err)
return
}

state, err := firebaseContainer.State(ctx)
if err != nil {
log.Printf("failed to get container state: %s", err)
return
}

fmt.Println(state.Running)

// Output:
// true
}
127 changes: 127 additions & 0 deletions modules/firebase/firebase.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
package firebase

import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"os"
"path"
"reflect"
"slices"

"github.com/testcontainers/testcontainers-go"
"github.com/testcontainers/testcontainers-go/wait"
)

// Container represents the Firebase container type used in the module
type Container struct {
testcontainers.Container
}

const rootFilePath = "/srv/firebase"

// ErrRootNotProvided is returned when the root path is not provided
var ErrRootNotProvided = errors.New("firebase root not provided (WithRoot is required)")

func gatherPorts(config partialFirebaseConfig) ([]string, error) {
var ports []string

v := reflect.ValueOf(config.Emulators)
for i := 0; i < v.NumField(); i++ {
emulator := v.Field(i)
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.


enabledF := emulator.FieldByName("Enabled")
if enabledF != (reflect.Value{}) && !enabledF.Bool() {
continue
}

hostF := emulator.FieldByName("Host")
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() {

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)
}
Comment on lines +49 to +52
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.

}
if portF != (reflect.Value{}) && !portF.IsZero() {
port := fmt.Sprintf("%d/tcp", portF.Uint())
ports = append(ports, port)
}
if websocketPortF != (reflect.Value{}) && !websocketPortF.IsZero() {
port := fmt.Sprintf("%d/tcp", websocketPortF.Uint())
ports = append(ports, port)
}
}

return ports, nil
}

// Run creates an instance of the Firebase container type
func Run(ctx context.Context, img string, opts ...testcontainers.ContainerCustomizer) (*Container, error) {
req := testcontainers.GenericContainerRequest{
ContainerRequest: testcontainers.ContainerRequest{
Image: img,
Env: map[string]string{},
WaitingFor: wait.ForLog("All emulators ready! It is now safe to connect your app."),
},
Started: true,
}

for _, opt := range opts {
if err := opt.Customize(&req); err != nil {
return nil, fmt.Errorf("customize: %w", err)
}
}

// Check if user supplied root:
rootPathIdx := slices.IndexFunc(req.Files, func(file testcontainers.ContainerFile) bool {
return file.ContainerFilePath == rootFilePath
})
if rootPathIdx == -1 {
return nil, ErrRootNotProvided
}

// Parse expected emulators from the root:
userRoot := req.Files[rootPathIdx].HostFilePath
cfg, err := os.Open(path.Join(userRoot, "firebase.json"))
if err != nil {
return nil, fmt.Errorf("open firebase.json: %w", err)
}
defer cfg.Close()

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)
}
Comment on lines +100 to +108
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.


expectedExposedPorts, err := gatherPorts(parsed)
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.


container, err := testcontainers.GenericContainer(ctx, req)
var c *Container
if container != nil {
c = &Container{Container: container}
}

if err != nil {
return c, fmt.Errorf("generic container: %w", err)
}

return c, nil
}
Loading
Loading