-
-
Notifications
You must be signed in to change notification settings - Fork 584
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
base: main
Are you sure you want to change the base?
Changes from 20 commits
29b1bba
6a88123
4105b0a
cc2bfa4
7c24032
bc68bb3
bc33438
1a72008
c2b8e2b
ba0bdc0
80298e5
4e271db
34409eb
fb2c0aa
88cb16d
aa4c29c
458cc7c
ae27c74
ae49941
cff37d9
9fe4988
3ed1839
d578817
72c1f45
fd6035e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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" %} | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @xytis let's not forget adding here |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
include ../../commons-test.mk | ||
|
||
.PHONY: test | ||
test: | ||
$(MAKE) test-firebase |
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 { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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"` | ||
} |
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")), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
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 | ||
} |
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 | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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() { | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. suggestion: use IsValid to simplify test, more below
Suggested change
|
||||||
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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||||||
} |
Uh oh!
There was an error while loading. Please reload this page.