Skip to content

Commit 35bf0cd

Browse files
Michael Ellismdelapenya
andauthored
feat(gcloud)!: add support to seed data when using RunBigQueryContainer (#2523)
* Update bigquery container to have an optional seed yaml file * update opts snippet to handle error; update documentation * Update based on feedback * chore: use new API for running big query container * chore: run make lint * chore: use testify's require * chore: remove unused * fix: process yaml file just once * chore: rename variable * chore: run mod tidy * chore: pass a reader to WithDataYAML option * chore: do not allow multiple calls to WithDataYAML * chore: embed test resource * chore: simplify reader * fix: update docs * chore: use the embed file even more * docs: wording * chore: simplify tests * chore: use original assertion --------- Co-authored-by: Manuel de la Peña <[email protected]>
1 parent 8b4fa8e commit 35bf0cd

File tree

6 files changed

+165
-10
lines changed

6 files changed

+165
-10
lines changed

docs/modules/gcloud.md

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ go get github.com/testcontainers/testcontainers-go/modules/gcloud
1717
## Usage example
1818

1919
!!!info
20-
By default, the all the emulators use `gcr.io/google.com/cloudsdktool/cloud-sdk:367.0.0-emulators` as the default Docker image, except for the BigQuery emulator, which uses `ghcr.io/goccy/bigquery-emulator:0.4.3`, and Spanner, which uses `gcr.io/cloud-spanner-emulator/emulator:1.4.0`.
20+
By default, the all the emulators use `gcr.io/google.com/cloudsdktool/cloud-sdk:367.0.0-emulators` as the default Docker image, except for the BigQuery emulator, which uses `ghcr.io/goccy/bigquery-emulator:0.6.1`, and Spanner, which uses `gcr.io/cloud-spanner-emulator/emulator:1.4.0`.
2121

2222
### BigQuery
2323

@@ -28,6 +28,22 @@ go get github.com/testcontainers/testcontainers-go/modules/gcloud
2828

2929
It's important to set the `option.WithEndpoint()` option using the container's URI, as shown in the client example above.
3030

31+
#### Data YAML (Seed File)
32+
33+
- 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>
34+
35+
If you would like to do additional initialization in the BigQuery container, add a `data.yaml` file represented by an `io.Reader` to the container request with the `WithDataYAML` function.
36+
That file is copied after the container is created but before it's started. The startup command then used will look like `--project test --data-from-yaml /testcontainers-data.yaml`.
37+
38+
An example of a `data.yaml` file that seeds the BigQuery instance with datasets and tables is shown below:
39+
40+
<!--codeinclude-->
41+
[Data Yaml content](../../modules/gcloud/testdata/data.yaml)
42+
<!--/codeinclude-->
43+
44+
!!!warning
45+
This feature is only available for the `BigQuery` container, and if you pass multiple `WithDataYAML` options, an error is returned.
46+
3147
### BigTable
3248

3349
<!--codeinclude-->

modules/gcloud/bigquery.go

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import (
1111
// Deprecated: use RunBigQuery instead
1212
// RunBigQueryContainer creates an instance of the GCloud container type for BigQuery.
1313
func RunBigQueryContainer(ctx context.Context, opts ...testcontainers.ContainerCustomizer) (*GCloudContainer, error) {
14-
return RunBigQuery(ctx, "ghcr.io/goccy/bigquery-emulator:0.4.3", opts...)
14+
return RunBigQuery(ctx, "ghcr.io/goccy/bigquery-emulator:0.6.1", opts...)
1515
}
1616

1717
// RunBigQuery creates an instance of the GCloud container type for BigQuery.
@@ -31,7 +31,20 @@ func RunBigQuery(ctx context.Context, img string, opts ...testcontainers.Contain
3131
return nil, err
3232
}
3333

34-
req.Cmd = []string{"--project", settings.ProjectID}
34+
req.Cmd = append(req.Cmd, "--project", settings.ProjectID)
35+
36+
// Process data yaml file only for the BigQuery container.
37+
if settings.bigQueryDataYaml != nil {
38+
containerPath := "/testcontainers-data.yaml"
39+
40+
req.Cmd = append(req.Cmd, "--data-from-yaml", containerPath)
41+
42+
req.Files = append(req.Files, testcontainers.ContainerFile{
43+
Reader: settings.bigQueryDataYaml,
44+
ContainerFilePath: containerPath,
45+
FileMode: 0o644,
46+
})
47+
}
3548

3649
return newGCloudContainer(ctx, req, 9050, settings, "http://")
3750
}

modules/gcloud/bigquery_test.go

Lines changed: 85 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
11
package gcloud_test
22

33
import (
4+
"bytes"
45
"context"
6+
_ "embed"
57
"errors"
68
"fmt"
79
"log"
10+
"testing"
811

912
"cloud.google.com/go/bigquery"
13+
"github.com/stretchr/testify/require"
1014
"google.golang.org/api/iterator"
1115
"google.golang.org/api/option"
1216
"google.golang.org/api/option/internaloption"
@@ -17,13 +21,16 @@ import (
1721
"github.com/testcontainers/testcontainers-go/modules/gcloud"
1822
)
1923

24+
//go:embed testdata/data.yaml
25+
var dataYaml []byte
26+
2027
func ExampleRunBigQueryContainer() {
2128
// runBigQueryContainer {
2229
ctx := context.Background()
2330

2431
bigQueryContainer, err := gcloud.RunBigQuery(
2532
ctx,
26-
"ghcr.io/goccy/bigquery-emulator:0.4.3",
33+
"ghcr.io/goccy/bigquery-emulator:0.6.1",
2734
gcloud.WithProjectID("bigquery-project"),
2835
)
2936
defer func() {
@@ -82,7 +89,83 @@ func ExampleRunBigQueryContainer() {
8289
}
8390

8491
fmt.Println(val)
85-
8692
// Output:
8793
// [30]
8894
}
95+
96+
func TestBigQueryWithDataYAML(t *testing.T) {
97+
ctx := context.Background()
98+
99+
t.Run("valid", func(t *testing.T) {
100+
bigQueryContainer, err := gcloud.RunBigQuery(
101+
ctx,
102+
"ghcr.io/goccy/bigquery-emulator:0.6.1",
103+
gcloud.WithProjectID("test"),
104+
gcloud.WithDataYAML(bytes.NewReader(dataYaml)),
105+
)
106+
testcontainers.CleanupContainer(t, bigQueryContainer)
107+
require.NoError(t, err)
108+
109+
projectID := bigQueryContainer.Settings.ProjectID
110+
111+
opts := []option.ClientOption{
112+
option.WithEndpoint(bigQueryContainer.URI),
113+
option.WithGRPCDialOption(grpc.WithTransportCredentials(insecure.NewCredentials())),
114+
option.WithoutAuthentication(),
115+
internaloption.SkipDialSettingsValidation(),
116+
}
117+
118+
client, err := bigquery.NewClient(ctx, projectID, opts...)
119+
require.NoError(t, err)
120+
defer client.Close()
121+
122+
selectQuery := client.Query("SELECT * FROM dataset1.table_a where name = @name")
123+
selectQuery.QueryConfig.Parameters = []bigquery.QueryParameter{
124+
{Name: "name", Value: "bob"},
125+
}
126+
it, err := selectQuery.Read(ctx)
127+
require.NoError(t, err)
128+
129+
var val []bigquery.Value
130+
for {
131+
err := it.Next(&val)
132+
if errors.Is(err, iterator.Done) {
133+
break
134+
}
135+
require.NoError(t, err)
136+
}
137+
138+
require.Equal(t, int64(30), val[0])
139+
})
140+
141+
t.Run("multi-value-set", func(t *testing.T) {
142+
bigQueryContainer, err := gcloud.RunBigQuery(
143+
ctx,
144+
"ghcr.io/goccy/bigquery-emulator:0.6.1",
145+
gcloud.WithProjectID("test"),
146+
gcloud.WithDataYAML(bytes.NewReader(dataYaml)),
147+
gcloud.WithDataYAML(bytes.NewReader(dataYaml)),
148+
)
149+
testcontainers.CleanupContainer(t, bigQueryContainer)
150+
require.EqualError(t, err, `data yaml already exists`)
151+
})
152+
153+
t.Run("multi-value-not-set", func(t *testing.T) {
154+
noValueOption := func() testcontainers.CustomizeRequestOption {
155+
return func(req *testcontainers.GenericContainerRequest) error {
156+
req.Cmd = append(req.Cmd, "--data-from-yaml")
157+
return nil
158+
}
159+
}
160+
161+
bigQueryContainer, err := gcloud.RunBigQuery(
162+
ctx,
163+
"ghcr.io/goccy/bigquery-emulator:0.6.1",
164+
noValueOption(), // because --project is always added last, this option will receive `--project` as value, which results in an error
165+
gcloud.WithProjectID("test"),
166+
gcloud.WithDataYAML(bytes.NewReader(dataYaml)),
167+
)
168+
testcontainers.CleanupContainer(t, bigQueryContainer)
169+
require.Error(t, err)
170+
})
171+
}

modules/gcloud/gcloud.go

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ package gcloud
22

33
import (
44
"context"
5+
"errors"
56
"fmt"
7+
"io"
68

79
"github.com/docker/go-connections/nat"
810

@@ -44,7 +46,8 @@ func newGCloudContainer(ctx context.Context, req testcontainers.GenericContainer
4446
}
4547

4648
type options struct {
47-
ProjectID string
49+
ProjectID string
50+
bigQueryDataYaml io.Reader
4851
}
4952

5053
func defaultOptions() options {
@@ -57,7 +60,7 @@ func defaultOptions() options {
5760
var _ testcontainers.ContainerCustomizer = (*Option)(nil)
5861

5962
// Option is an option for the GCloud container.
60-
type Option func(*options)
63+
type Option func(*options) error
6164

6265
// Customize is a NOOP. It's defined to satisfy the testcontainers.ContainerCustomizer interface.
6366
func (o Option) Customize(*testcontainers.GenericContainerRequest) error {
@@ -67,8 +70,26 @@ func (o Option) Customize(*testcontainers.GenericContainerRequest) error {
6770

6871
// WithProjectID sets the project ID for the GCloud container.
6972
func WithProjectID(projectID string) Option {
70-
return func(o *options) {
73+
return func(o *options) error {
7174
o.ProjectID = projectID
75+
return nil
76+
}
77+
}
78+
79+
// WithDataYAML seeds the BigQuery project for the GCloud container with an [io.Reader] representing
80+
// the data yaml file, which is used to copy the file to the container, and then processed to seed
81+
// the BigQuery project.
82+
//
83+
// Other GCloud containers will ignore this option.
84+
// If this option is passed multiple times, an error is returned.
85+
func WithDataYAML(r io.Reader) Option {
86+
return func(o *options) error {
87+
if o.bigQueryDataYaml != nil {
88+
return errors.New("data yaml already exists")
89+
}
90+
91+
o.bigQueryDataYaml = r
92+
return nil
7293
}
7394
}
7495

@@ -77,7 +98,9 @@ func applyOptions(req *testcontainers.GenericContainerRequest, opts []testcontai
7798
settings := defaultOptions()
7899
for _, opt := range opts {
79100
if apply, ok := opt.(Option); ok {
80-
apply(&settings)
101+
if err := apply(&settings); err != nil {
102+
return options{}, err
103+
}
81104
}
82105
if err := opt.Customize(req); err != nil {
83106
return options{}, err

modules/gcloud/go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ require (
1010
cloud.google.com/go/pubsub v1.36.2
1111
cloud.google.com/go/spanner v1.57.0
1212
github.com/docker/go-connections v0.5.0
13+
github.com/stretchr/testify v1.9.0
1314
github.com/testcontainers/testcontainers-go v0.34.0
1415
google.golang.org/api v0.169.0
1516
google.golang.org/grpc v1.64.1
@@ -74,7 +75,6 @@ require (
7475
github.com/shirou/gopsutil/v3 v3.23.12 // indirect
7576
github.com/shoenig/go-m1cpu v0.1.6 // indirect
7677
github.com/sirupsen/logrus v1.9.3 // indirect
77-
github.com/stretchr/testify v1.9.0 // indirect
7878
github.com/tklauser/go-sysconf v0.3.12 // indirect
7979
github.com/tklauser/numcpus v0.6.1 // indirect
8080
github.com/yusufpapurcu/wmi v1.2.3 // indirect

modules/gcloud/testdata/data.yaml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
projects:
2+
- id: test
3+
datasets:
4+
- id: dataset1
5+
tables:
6+
- id: table_a
7+
columns:
8+
- name: id
9+
type: INTEGER
10+
- name: name
11+
type: STRING
12+
- name: createdAt
13+
type: TIMESTAMP
14+
data:
15+
- id: 1
16+
name: alice
17+
createdAt: "2022-10-21T00:00:00"
18+
- id: 30
19+
name: bob
20+
createdAt: "2022-10-21T00:00:00"

0 commit comments

Comments
 (0)