diff --git a/.vscode/.testcontainers-go.code-workspace b/.vscode/.testcontainers-go.code-workspace
index 128ed2e320..96466a6156 100644
--- a/.vscode/.testcontainers-go.code-workspace
+++ b/.vscode/.testcontainers-go.code-workspace
@@ -85,6 +85,10 @@
"name": "module / etcd",
"path": "../modules/etcd"
},
+ {
+ "name": "module / firebase",
+ "path": "../modules/firebase"
+ },
{
"name": "module / gcloud",
"path": "../modules/gcloud"
@@ -246,4 +250,4 @@
"path": "../modulegen"
}
]
-}
\ No newline at end of file
+}
diff --git a/docs/modules/firebase.md b/docs/modules/firebase.md
new file mode 100644
index 0000000000..5d1d37b7a5
--- /dev/null
+++ b/docs/modules/firebase.md
@@ -0,0 +1,70 @@
+# Firebase
+
+Not available until the next release of testcontainers-go :material-tag: main
+
+## 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
+
+
+[Creating a Firebase container](../../modules/firebase/examples_test.go) inside_block:ExampleRun
+
+
+## Module Reference
+
+### Run function
+
+- Not available until the next release of testcontainers-go :material-tag: main
+
+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" %}
+
+#### WithRoot
+
+- Not available until the next release of testcontainers-go :material-tag: main
+
+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 :material-tag: main
+
+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:
diff --git a/modules/firebase/Makefile b/modules/firebase/Makefile
new file mode 100644
index 0000000000..3618add78f
--- /dev/null
+++ b/modules/firebase/Makefile
@@ -0,0 +1,5 @@
+include ../../commons-test.mk
+
+.PHONY: test
+test:
+ $(MAKE) test-firebase
diff --git a/modules/firebase/config.go b/modules/firebase/config.go
new file mode 100644
index 0000000000..8486c66ce4
--- /dev/null
+++ b/modules/firebase/config.go
@@ -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 {
+ Emulators emulatorsConfig `json:"emulators,omitempty"`
+}
diff --git a/modules/firebase/examples_test.go b/modules/firebase/examples_test.go
new file mode 100644
index 0000000000..6e31660806
--- /dev/null
+++ b/modules/firebase/examples_test.go
@@ -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")),
+ 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
+}
diff --git a/modules/firebase/firebase.go b/modules/firebase/firebase.go
new file mode 100644
index 0000000000..2cb7804f7e
--- /dev/null
+++ b/modules/firebase/firebase.go
@@ -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
+
+ 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() {
+ 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)
+ }
+ }
+ 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)
+ }
+
+ expectedExposedPorts, err := gatherPorts(parsed)
+ if err != nil {
+ return nil, fmt.Errorf("gather ports: %w", err)
+ }
+ req.ExposedPorts = expectedExposedPorts
+
+ 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
+}
diff --git a/modules/firebase/firebase_test.go b/modules/firebase/firebase_test.go
new file mode 100644
index 0000000000..ef18653df2
--- /dev/null
+++ b/modules/firebase/firebase_test.go
@@ -0,0 +1,74 @@
+package firebase_test
+
+import (
+ "context"
+ "path/filepath"
+ "testing"
+ "time"
+
+ "cloud.google.com/go/firestore"
+ "github.com/stretchr/testify/require"
+
+ "github.com/testcontainers/testcontainers-go"
+
+ "github.com/testcontainers/testcontainers-go/modules/firebase"
+)
+
+func TestFirebase(t *testing.T) {
+ ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(3*time.Minute))
+ defer cancel()
+
+ ctr, err := firebase.Run(ctx, "ghcr.io/u-health/docker-firebase-emulator:13.29.2",
+ firebase.WithRoot(filepath.Join("testdata", "firebase")),
+ )
+ testcontainers.CleanupContainer(t, ctr)
+ require.NoError(t, err)
+
+ // Ports are linked to the example config in firebase/firebase.json
+
+ firestoreURL, err := ctr.ConnectionString(ctx, "8080/tcp")
+ require.NoError(t, err)
+ require.NotEmpty(t, firestoreURL)
+
+ t.Setenv("FIRESTORE_EMULATOR_HOST", firestoreURL)
+ c, err := firestore.NewClient(ctx, firestore.DetectProjectID)
+ require.NoError(t, err)
+ defer c.Close()
+
+ w, err := c.Collection("example").Doc("one").Set(ctx, map[string]any{
+ "foo": "bar",
+ })
+ require.NoError(t, err)
+ require.NotNil(t, w)
+
+ snap, err := c.Collection("example").Doc("one").Get(ctx)
+ require.NoError(t, err)
+ var out map[string]string
+ err = snap.DataTo(&out)
+ require.NoError(t, err)
+ require.Equal(t, "bar", out["foo"])
+}
+
+func TestFirebaseBadDirectory(t *testing.T) {
+ ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(3*time.Minute))
+ defer cancel()
+
+ ctr, err := firebase.Run(ctx, "ghcr.io/u-health/docker-firebase-emulator:13.29.2",
+ firebase.WithRoot(filepath.Join("testdata", "failure")),
+ )
+ // In this case, the file gets copied over at /srv/failure (instead of /srv/firebase)
+ // and this stops working.
+ // 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)
+}
+
+func TestFirebaseRequiresRoot(t *testing.T) {
+ ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(3*time.Minute))
+ defer cancel()
+
+ ctr, err := firebase.Run(ctx, "ghcr.io/u-health/docker-firebase-emulator:13.29.2")
+ testcontainers.CleanupContainer(t, ctr)
+ require.ErrorIs(t, err, firebase.ErrRootNotProvided)
+}
diff --git a/modules/firebase/go.mod b/modules/firebase/go.mod
new file mode 100644
index 0000000000..8f2098af38
--- /dev/null
+++ b/modules/firebase/go.mod
@@ -0,0 +1,84 @@
+module github.com/testcontainers/testcontainers-go/modules/firebase
+
+go 1.23.0
+
+require (
+ cloud.google.com/go/firestore v1.18.0
+ github.com/docker/go-connections v0.5.0
+ github.com/stretchr/testify v1.10.0
+ github.com/testcontainers/testcontainers-go v0.36.0
+)
+
+require (
+ cloud.google.com/go v0.117.0 // indirect
+ cloud.google.com/go/auth v0.13.0 // indirect
+ cloud.google.com/go/auth/oauth2adapt v0.2.6 // indirect
+ cloud.google.com/go/compute/metadata v0.6.0 // indirect
+ cloud.google.com/go/longrunning v0.6.2 // indirect
+ dario.cat/mergo v1.0.1 // indirect
+ github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
+ github.com/Microsoft/go-winio v0.6.2 // indirect
+ github.com/cenkalti/backoff/v4 v4.2.1 // indirect
+ github.com/containerd/log v0.1.0 // indirect
+ github.com/containerd/platforms v0.2.1 // indirect
+ github.com/cpuguy83/dockercfg v0.3.2 // indirect
+ github.com/davecgh/go-spew v1.1.1 // indirect
+ github.com/distribution/reference v0.6.0 // indirect
+ github.com/docker/docker v28.0.1+incompatible // indirect
+ github.com/docker/go-units v0.5.0 // indirect
+ github.com/ebitengine/purego v0.8.2 // indirect
+ github.com/felixge/httpsnoop v1.0.4 // indirect
+ github.com/go-logr/logr v1.4.2 // indirect
+ github.com/go-logr/stdr v1.2.2 // indirect
+ github.com/go-ole/go-ole v1.2.6 // indirect
+ github.com/gogo/protobuf v1.3.2 // indirect
+ github.com/google/s2a-go v0.1.8 // indirect
+ github.com/google/uuid v1.6.0 // indirect
+ github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect
+ github.com/googleapis/gax-go/v2 v2.14.0 // indirect
+ github.com/klauspost/compress v1.17.4 // indirect
+ github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
+ github.com/magiconair/properties v1.8.10 // indirect
+ github.com/moby/docker-image-spec v1.3.1 // indirect
+ github.com/moby/patternmatcher v0.6.0 // indirect
+ github.com/moby/sys/sequential v0.5.0 // indirect
+ github.com/moby/sys/user v0.1.0 // indirect
+ github.com/moby/sys/userns v0.1.0 // indirect
+ github.com/moby/term v0.5.0 // indirect
+ github.com/morikuni/aec v1.0.0 // indirect
+ github.com/opencontainers/go-digest v1.0.0 // indirect
+ github.com/opencontainers/image-spec v1.1.1 // indirect
+ github.com/pkg/errors v0.9.1 // indirect
+ github.com/pmezard/go-difflib v1.0.0 // indirect
+ github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
+ github.com/rogpeppe/go-internal v1.14.1 // indirect
+ github.com/shirou/gopsutil/v4 v4.25.2 // indirect
+ github.com/sirupsen/logrus v1.9.3 // indirect
+ github.com/tklauser/go-sysconf v0.3.12 // indirect
+ github.com/tklauser/numcpus v0.6.1 // indirect
+ github.com/yusufpapurcu/wmi v1.2.4 // indirect
+ go.opentelemetry.io/auto/sdk v1.1.0 // indirect
+ go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0 // indirect
+ go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 // indirect
+ go.opentelemetry.io/otel v1.35.0 // indirect
+ go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0 // indirect
+ go.opentelemetry.io/otel/metric v1.35.0 // indirect
+ go.opentelemetry.io/otel/trace v1.35.0 // indirect
+ go.opentelemetry.io/proto/otlp v1.0.0 // indirect
+ golang.org/x/crypto v0.37.0 // indirect
+ golang.org/x/net v0.33.0 // indirect
+ golang.org/x/oauth2 v0.24.0 // indirect
+ golang.org/x/sync v0.13.0 // indirect
+ golang.org/x/sys v0.32.0 // indirect
+ golang.org/x/text v0.24.0 // indirect
+ golang.org/x/time v0.8.0 // indirect
+ google.golang.org/api v0.214.0 // indirect
+ google.golang.org/genproto v0.0.0-20241118233622-e639e219e697 // indirect
+ google.golang.org/genproto/googleapis/api v0.0.0-20241118233622-e639e219e697 // indirect
+ google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576 // indirect
+ google.golang.org/grpc v1.67.3 // indirect
+ google.golang.org/protobuf v1.36.5 // indirect
+ gopkg.in/yaml.v3 v3.0.1 // indirect
+)
+
+replace github.com/testcontainers/testcontainers-go => ../..
diff --git a/modules/firebase/go.sum b/modules/firebase/go.sum
new file mode 100644
index 0000000000..e80de547b3
--- /dev/null
+++ b/modules/firebase/go.sum
@@ -0,0 +1,214 @@
+cloud.google.com/go v0.117.0 h1:Z5TNFfQxj7WG2FgOGX1ekC5RiXrYgms6QscOm32M/4s=
+cloud.google.com/go v0.117.0/go.mod h1:ZbwhVTb1DBGt2Iwb3tNO6SEK4q+cplHZmLWH+DelYYc=
+cloud.google.com/go/auth v0.13.0 h1:8Fu8TZy167JkW8Tj3q7dIkr2v4cndv41ouecJx0PAHs=
+cloud.google.com/go/auth v0.13.0/go.mod h1:COOjD9gwfKNKz+IIduatIhYJQIc0mG3H102r/EMxX6Q=
+cloud.google.com/go/auth/oauth2adapt v0.2.6 h1:V6a6XDu2lTwPZWOawrAa9HUK+DB2zfJyTuciBG5hFkU=
+cloud.google.com/go/auth/oauth2adapt v0.2.6/go.mod h1:AlmsELtlEBnaNTL7jCj8VQFLy6mbZv0s4Q7NGBeQ5E8=
+cloud.google.com/go/compute/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4j01OwKxG9I=
+cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg=
+cloud.google.com/go/firestore v1.18.0 h1:cuydCaLS7Vl2SatAeivXyhbhDEIR8BDmtn4egDhIn2s=
+cloud.google.com/go/firestore v1.18.0/go.mod h1:5ye0v48PhseZBdcl0qbl3uttu7FIEwEYVaWm0UIEOEU=
+cloud.google.com/go/longrunning v0.6.2 h1:xjDfh1pQcWPEvnfjZmwjKQEcHnpz6lHjfy7Fo0MK+hc=
+cloud.google.com/go/longrunning v0.6.2/go.mod h1:k/vIs83RN4bE3YCswdXC5PFfWVILjm3hpEUlSko4PiI=
+dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s=
+dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
+github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 h1:bvDV9vkmnHYOMsOr4WLk+Vo07yKIzd94sVoIqshQ4bU=
+github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8=
+github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8=
+github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
+github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
+github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
+github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM=
+github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
+github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
+github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
+github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A=
+github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw=
+github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA=
+github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc=
+github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=
+github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
+github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
+github.com/docker/docker v28.0.1+incompatible h1:FCHjSRdXhNRFjlHMTv4jUNlIBbTeRjrWfeFuJp7jpo0=
+github.com/docker/docker v28.0.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
+github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
+github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
+github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
+github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
+github.com/ebitengine/purego v0.8.2 h1:jPPGWs2sZ1UgOSgD2bClL0MJIqu58nOmIcBuXr62z1I=
+github.com/ebitengine/purego v0.8.2/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
+github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
+github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
+github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
+github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
+github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
+github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
+github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
+github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
+github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
+github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
+github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
+github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
+github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
+github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
+github.com/google/s2a-go v0.1.8 h1:zZDs9gcbt9ZPLV0ndSyQk6Kacx2g/X+SKYovpnz3SMM=
+github.com/google/s2a-go v0.1.8/go.mod h1:6iNWHTpQ+nfNRN5E00MSdfDwVesa8hhS32PhPO8deJA=
+github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
+github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/googleapis/enterprise-certificate-proxy v0.3.4 h1:XYIDZApgAnrN1c855gTgghdIA6Stxb52D5RnLI1SLyw=
+github.com/googleapis/enterprise-certificate-proxy v0.3.4/go.mod h1:YKe7cfqYXjKGpGvmSg28/fFvhNzinZQm8DGnaburhGA=
+github.com/googleapis/gax-go/v2 v2.14.0 h1:f+jMrjBPl+DL9nI4IQzLUxMq7XrAqFYB7hBPqMNIe8o=
+github.com/googleapis/gax-go/v2 v2.14.0/go.mod h1:lhBCnjdLrWRaPvLWhmc8IS24m9mr07qSYnHncrgo+zk=
+github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 h1:YBftPWNWd4WwGqtY2yeZL2ef8rHAxPBD8KFhJpmcqms=
+github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0/go.mod h1:YN5jB8ie0yfIUg6VvR9Kz84aCaG7AsGZnLjhHbUqwPg=
+github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
+github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
+github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4=
+github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM=
+github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
+github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
+github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
+github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
+github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4=
+github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
+github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE=
+github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
+github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
+github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
+github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk=
+github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc=
+github.com/moby/sys/sequential v0.5.0 h1:OPvI35Lzn9K04PBbCLW0g4LcFAJgHsvXsRyewg5lXtc=
+github.com/moby/sys/sequential v0.5.0/go.mod h1:tH2cOOs5V9MlPiXcQzRC+eEyab644PWKGRYaaV5ZZlo=
+github.com/moby/sys/user v0.1.0 h1:WmZ93f5Ux6het5iituh9x2zAG7NFY9Aqi49jjE1PaQg=
+github.com/moby/sys/user v0.1.0/go.mod h1:fKJhFOnsCN6xZ5gSfbM6zaHGgDJMrqt9/reuj4T7MmU=
+github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g=
+github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28=
+github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
+github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
+github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
+github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
+github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
+github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
+github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
+github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
+github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
+github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw=
+github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
+github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
+github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
+github.com/shirou/gopsutil/v4 v4.25.2 h1:NMscG3l2CqtWFS86kj3vP7soOczqrQYIEhO/pMvvQkk=
+github.com/shirou/gopsutil/v4 v4.25.2/go.mod h1:34gBYJzyqCDT11b6bMHP0XCvWeU3J61XRT7a2EmCRTA=
+github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
+github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
+github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
+github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
+github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU=
+github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=
+github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk=
+github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY=
+github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
+github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
+go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
+go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
+go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
+go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
+go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0 h1:r6I7RJCN86bpD/FQwedZ0vSixDpwuWREjW9oRMsmqDc=
+go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0/go.mod h1:B9yO6b04uB80CzjedvewuqDhxJxi11s7/GtiGa8bAjI=
+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 h1:TT4fX+nBOA/+LUkobKGW1ydGcn+G3vRw9+g5HwCphpk=
+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0/go.mod h1:L7UH0GbB0p47T4Rri3uHjbpCFYrVrwc1I25QhNPiGK8=
+go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ=
+go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0 h1:Mne5On7VWdx7omSrSSZvM4Kw7cS7NQkOOmLcgscI51U=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0/go.mod h1:IPtUMKL4O3tH5y+iXVyAXqpAwMuzC1IrxVS81rummfE=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 h1:IeMeyr1aBvBiPVYihXIaeIZba6b8E1bYp7lbdxK8CQg=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0/go.mod h1:oVdCUtjq9MK9BlS7TtucsQwUcXcymNiEDjgDD2jMtZU=
+go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M=
+go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE=
+go.opentelemetry.io/otel/sdk v1.29.0 h1:vkqKjk7gwhS8VaWb0POZKmIEDimRCMsopNYnriHyryo=
+go.opentelemetry.io/otel/sdk v1.29.0/go.mod h1:pM8Dx5WKnvxLCb+8lG1PRNIDxu9g9b9g59Qr7hfAAok=
+go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs=
+go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc=
+go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I=
+go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
+golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
+golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
+golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
+golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE=
+golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610=
+golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
+golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
+golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o=
+golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
+golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
+golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg=
+golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+google.golang.org/api v0.214.0 h1:h2Gkq07OYi6kusGOaT/9rnNljuXmqPnaig7WGPmKbwA=
+google.golang.org/api v0.214.0/go.mod h1:bYPpLG8AyeMWwDU6NXoB00xC0DFkikVvd5MfwoxjLqE=
+google.golang.org/genproto v0.0.0-20241118233622-e639e219e697 h1:ToEetK57OidYuqD4Q5w+vfEnPvPpuTwedCNVohYJfNk=
+google.golang.org/genproto v0.0.0-20241118233622-e639e219e697/go.mod h1:JJrvXBWRZaFMxBufik1a4RpFw4HhgVtBBWQeQgUj2cc=
+google.golang.org/genproto/googleapis/api v0.0.0-20241118233622-e639e219e697 h1:pgr/4QbFyktUv9CtQ/Fq4gzEE6/Xs7iCXbktaGzLHbQ=
+google.golang.org/genproto/googleapis/api v0.0.0-20241118233622-e639e219e697/go.mod h1:+D9ySVjN8nY8YCVjc5O7PZDIdZporIDY3KaGfJunh88=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576 h1:8ZmaLZE4XWrtU3MyClkYqqtl6Oegr3235h7jxsDyqCY=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576/go.mod h1:5uTbfoYQed2U9p3KIj2/Zzm02PYhndfdmML0qC3q3FU=
+google.golang.org/grpc v1.67.3 h1:OgPcDAFKHnH8X3O4WcO4XUc8GRDeKsKReqbQtiCj7N8=
+google.golang.org/grpc v1.67.3/go.mod h1:YGaHCc6Oap+FzBJTZLBzkGSYt/cvGPFTPxkn7QfSU8s=
+google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
+google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU=
+gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU=
diff --git a/modules/firebase/options.go b/modules/firebase/options.go
new file mode 100644
index 0000000000..e95b5305c2
--- /dev/null
+++ b/modules/firebase/options.go
@@ -0,0 +1,51 @@
+package firebase
+
+import (
+ "github.com/testcontainers/testcontainers-go"
+)
+
+type options struct{}
+
+// Compiler check to ensure that Option implements the testcontainers.ContainerCustomizer interface.
+var _ testcontainers.ContainerCustomizer = (Option)(nil)
+
+// Option is an option for the Firebase container.
+type Option func(*options)
+
+// Customize is a NOOP. It's defined to satisfy the testcontainers.ContainerCustomizer interface.
+func (o Option) Customize(*testcontainers.GenericContainerRequest) error {
+ // NOOP to satisfy interface.
+ return nil
+}
+
+// WithRoot sets the directory which is copied to the destination container as firebase root
+func WithRoot(rootPath string) testcontainers.CustomizeRequestOption {
+ return testcontainers.WithFiles(testcontainers.ContainerFile{
+ HostFilePath: rootPath,
+ ContainerFilePath: rootFilePath,
+ FileMode: 0o775,
+ })
+}
+
+// WithData names the data directory (by default under firebase root), can be used as a way of setting up fixtures.
+// Usage of absolute path will imply that the user knows how to mount external directory into the container.
+func WithData(dataPath string) testcontainers.CustomizeRequestOption {
+ return func(req *testcontainers.GenericContainerRequest) error {
+ req.Env["DATA_DIRECTORY"] = dataPath
+ return nil
+ }
+}
+
+const cacheFilePath = "/root/.cache/firebase"
+
+// WithCache enables firebase binary cache based on session (meaningful only when multiple tests are used)
+func WithCache() testcontainers.CustomizeRequestOption {
+ volumeName := "firestore-cache-" + testcontainers.SessionID()
+
+ return testcontainers.WithMounts(testcontainers.ContainerMount{
+ Source: testcontainers.DockerVolumeMountSource{
+ Name: volumeName,
+ },
+ Target: cacheFilePath,
+ })
+}
diff --git a/modules/firebase/ports.go b/modules/firebase/ports.go
new file mode 100644
index 0000000000..ea4b7cdc65
--- /dev/null
+++ b/modules/firebase/ports.go
@@ -0,0 +1,22 @@
+package firebase
+
+import (
+ "context"
+ "fmt"
+
+ "github.com/docker/go-connections/nat"
+)
+
+func (c *Container) ConnectionString(ctx context.Context, portName nat.Port) (string, error) {
+ host, err := c.Host(ctx)
+ if err != nil {
+ return "", fmt.Errorf("host: %w", err)
+ }
+
+ port, err := c.MappedPort(ctx, portName)
+ if err != nil {
+ return "", fmt.Errorf("mapped port: %w", err)
+ }
+
+ return fmt.Sprintf("%s:%s", host, port.Port()), nil
+}
diff --git a/modules/firebase/testdata/failure/.firebaserc b/modules/firebase/testdata/failure/.firebaserc
new file mode 100644
index 0000000000..601f86b856
--- /dev/null
+++ b/modules/firebase/testdata/failure/.firebaserc
@@ -0,0 +1,5 @@
+{
+ "projects": {
+ "default": "test"
+ }
+}
diff --git a/modules/firebase/testdata/failure/firebase.json b/modules/firebase/testdata/failure/firebase.json
new file mode 100644
index 0000000000..4455c6ac35
--- /dev/null
+++ b/modules/firebase/testdata/failure/firebase.json
@@ -0,0 +1,39 @@
+{
+ "emulators": {
+ "ui": {
+ "//": "UI will only partially work in the container. Because ports get mangled, some of the features will be broken. A solution is to run UI externally and provide the mangled ports as inputs.",
+ "enabled": true,
+ "port": 4000,
+ "host": "0.0.0.0"
+ },
+ "hub": {
+ "port": 4400,
+ "host": "0.0.0.0"
+ },
+ "logging": {
+ "port": 4600,
+ "host": "0.0.0.0"
+ },
+ "firestore": {
+ "port": 8080,
+ "host": "0.0.0.0",
+ "websocketPort": 8081
+ },
+ "auth": {
+ "port": 9099,
+ "host": "0.0.0.0"
+ },
+ "storage": {
+ "port": 9199,
+ "host": "0.0.0.0"
+ },
+ "singleProjectMode": true
+ },
+ "firestore": {
+ "rules": "firestore.rules",
+ "indexes": "firestore.indexes.json"
+ },
+ "storage": {
+ "rules": "storage.rules"
+ }
+}
diff --git a/modules/firebase/testdata/failure/firestore.indexes.json b/modules/firebase/testdata/failure/firestore.indexes.json
new file mode 100644
index 0000000000..415027e5dd
--- /dev/null
+++ b/modules/firebase/testdata/failure/firestore.indexes.json
@@ -0,0 +1,4 @@
+{
+ "indexes": [],
+ "fieldOverrides": []
+}
diff --git a/modules/firebase/testdata/failure/firestore.rules b/modules/firebase/testdata/failure/firestore.rules
new file mode 100644
index 0000000000..de31b558d6
--- /dev/null
+++ b/modules/firebase/testdata/failure/firestore.rules
@@ -0,0 +1,9 @@
+rules_version = '2';
+service cloud.firestore {
+ match /databases/{database}/documents {
+ match /{document=**} {
+ allow read, write: if false;
+ }
+ }
+}
+
diff --git a/modules/firebase/testdata/failure/storage.rules b/modules/firebase/testdata/failure/storage.rules
new file mode 100644
index 0000000000..8348b99153
--- /dev/null
+++ b/modules/firebase/testdata/failure/storage.rules
@@ -0,0 +1,8 @@
+rules_version = '2';
+service firebase.storage {
+ match /b/{bucket}/o {
+ match /{allPaths=**} {
+ allow read, write: if true;
+ }
+ }
+}
diff --git a/modules/firebase/testdata/firebase/.firebaserc b/modules/firebase/testdata/firebase/.firebaserc
new file mode 100644
index 0000000000..601f86b856
--- /dev/null
+++ b/modules/firebase/testdata/firebase/.firebaserc
@@ -0,0 +1,5 @@
+{
+ "projects": {
+ "default": "test"
+ }
+}
diff --git a/modules/firebase/testdata/firebase/firebase.json b/modules/firebase/testdata/firebase/firebase.json
new file mode 100644
index 0000000000..4455c6ac35
--- /dev/null
+++ b/modules/firebase/testdata/firebase/firebase.json
@@ -0,0 +1,39 @@
+{
+ "emulators": {
+ "ui": {
+ "//": "UI will only partially work in the container. Because ports get mangled, some of the features will be broken. A solution is to run UI externally and provide the mangled ports as inputs.",
+ "enabled": true,
+ "port": 4000,
+ "host": "0.0.0.0"
+ },
+ "hub": {
+ "port": 4400,
+ "host": "0.0.0.0"
+ },
+ "logging": {
+ "port": 4600,
+ "host": "0.0.0.0"
+ },
+ "firestore": {
+ "port": 8080,
+ "host": "0.0.0.0",
+ "websocketPort": 8081
+ },
+ "auth": {
+ "port": 9099,
+ "host": "0.0.0.0"
+ },
+ "storage": {
+ "port": 9199,
+ "host": "0.0.0.0"
+ },
+ "singleProjectMode": true
+ },
+ "firestore": {
+ "rules": "firestore.rules",
+ "indexes": "firestore.indexes.json"
+ },
+ "storage": {
+ "rules": "storage.rules"
+ }
+}
diff --git a/modules/firebase/testdata/firebase/firestore.indexes.json b/modules/firebase/testdata/firebase/firestore.indexes.json
new file mode 100644
index 0000000000..415027e5dd
--- /dev/null
+++ b/modules/firebase/testdata/firebase/firestore.indexes.json
@@ -0,0 +1,4 @@
+{
+ "indexes": [],
+ "fieldOverrides": []
+}
diff --git a/modules/firebase/testdata/firebase/firestore.rules b/modules/firebase/testdata/firebase/firestore.rules
new file mode 100644
index 0000000000..de31b558d6
--- /dev/null
+++ b/modules/firebase/testdata/firebase/firestore.rules
@@ -0,0 +1,9 @@
+rules_version = '2';
+service cloud.firestore {
+ match /databases/{database}/documents {
+ match /{document=**} {
+ allow read, write: if false;
+ }
+ }
+}
+
diff --git a/modules/firebase/testdata/firebase/storage.rules b/modules/firebase/testdata/firebase/storage.rules
new file mode 100644
index 0000000000..8348b99153
--- /dev/null
+++ b/modules/firebase/testdata/firebase/storage.rules
@@ -0,0 +1,8 @@
+rules_version = '2';
+service firebase.storage {
+ match /b/{bucket}/o {
+ match /{allPaths=**} {
+ allow read, write: if true;
+ }
+ }
+}