Skip to content

Commit

Permalink
Add support for Neo4j configuration (testcontainers#992)
Browse files Browse the repository at this point in the history
* Add support for Neo4j configuration

* Remove testify

* Add missing setting name example in docs

* Use logger

* Document possible config ordering issue

* Remove bogus license header
  • Loading branch information
fbiville authored and mdelapenya committed Apr 3, 2023
1 parent 43e9120 commit bb04544
Show file tree
Hide file tree
Showing 6 changed files with 196 additions and 24 deletions.
67 changes: 63 additions & 4 deletions modules/neo4j/config.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package neo4j

import (
"errors"
"fmt"
"github.com/testcontainers/testcontainers-go"
"strings"
)

Expand All @@ -11,6 +13,8 @@ type config struct {
imageCoordinates string
adminPassword string
labsPlugins []string
neo4jSettings map[string]string
logger testcontainers.Logging
}

type LabsPlugin string
Expand Down Expand Up @@ -57,22 +61,77 @@ func WithLabsPlugin(plugins ...LabsPlugin) Option {
}
}

func (c config) exportEnv() map[string]string {
env := make(map[string]string)
// WithNeo4jSetting adds Neo4j a single configuration setting to the container.
// The setting can be added as in the official Neo4j configuration, the function automatically translates the setting
// name (e.g. dbms.tx_log.rotation.size) into the format required by the Neo4j container.
// This function can be called multiple times. A warning is emitted if a key is overwritten.
// See WithNeo4jSettings to add multiple settings at once
// Note: credentials must be configured with WithAdminPassword
func WithNeo4jSetting(key, value string) Option {
return func(c *config) {
c.addSetting(key, value)
}
}

// WithNeo4jSettings adds multiple Neo4j configuration settings to the container.
// The settings can be added as in the official Neo4j configuration, the function automatically translates each setting
// name (e.g. dbms.tx_log.rotation.size) into the format required by the Neo4j container.
// This function can be called multiple times. A warning is emitted if a key is overwritten.
// See WithNeo4jSetting to add a single setting
// Note: credentials must be configured with WithAdminPassword
func WithNeo4jSettings(settings map[string]string) Option {
return func(c *config) {
for key, value := range settings {
c.addSetting(key, value)
}
}
}

// WithLogger sets a custom logger to be used by the container
// Consider calling this before other "With functions" as these may generate logs
func WithLogger(logger testcontainers.Logging) Option {
return func(c *config) {
c.logger = logger
}
}

func (c *config) exportEnv() map[string]string {
env := c.neo4jSettings // set this first to ensure it has the lowest precedence
env["NEO4J_AUTH"] = c.authEnvVar()
if len(c.labsPlugins) > 0 {
env["NEO4JLABS_PLUGINS"] = c.labsPluginsEnvVar()
}
return env
}

func (c config) authEnvVar() string {
func (c *config) authEnvVar() string {
if c.adminPassword == "" {
return "none"
}
return fmt.Sprintf("neo4j/%s", c.adminPassword)
}

func (c config) labsPluginsEnvVar() string {
func (c *config) labsPluginsEnvVar() string {
return fmt.Sprintf(`["%s"]`, strings.Join(c.labsPlugins, `","`))
}

func (c *config) addSetting(key string, newVal string) {
normalizedKey := formatNeo4jConfig(key)
if oldVal, found := c.neo4jSettings[normalizedKey]; found {
c.logger.Printf("setting %q with value %q is now overwritten with value %q\n", []any{key, oldVal, newVal}...)
}
c.neo4jSettings[normalizedKey] = newVal
}

func (c *config) validate() error {
if c.logger == nil {
return errors.New("nil logger is not permitted")
}
return nil
}

func formatNeo4jConfig(name string) string {
result := strings.ReplaceAll(name, "_", "__")
result = strings.ReplaceAll(result, ".", "_")
return fmt.Sprintf("NEO4J_%s", result)
}
4 changes: 0 additions & 4 deletions modules/neo4j/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ go 1.19
require (
github.com/docker/go-connections v0.4.0
github.com/neo4j/neo4j-go-driver/v5 v5.6.0
github.com/stretchr/testify v1.8.2
github.com/testcontainers/testcontainers-go v0.19.0
gotest.tools/gotestsum v1.9.0
)
Expand All @@ -16,7 +15,6 @@ require (
github.com/cenkalti/backoff/v4 v4.2.0 // indirect
github.com/containerd/containerd v1.6.19 // indirect
github.com/cpuguy83/dockercfg v0.3.1 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dnephin/pflag v1.0.7 // indirect
github.com/docker/distribution v2.8.1+incompatible // indirect
github.com/docker/docker v23.0.1+incompatible // indirect
Expand All @@ -40,7 +38,6 @@ require (
github.com/opencontainers/image-spec v1.1.0-rc2 // indirect
github.com/opencontainers/runc v1.1.5 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/sirupsen/logrus v1.9.0 // indirect
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect
golang.org/x/net v0.7.0 // indirect
Expand All @@ -51,7 +48,6 @@ require (
google.golang.org/genproto v0.0.0-20220617124728-180714bec0ad // indirect
google.golang.org/grpc v1.47.0 // indirect
google.golang.org/protobuf v1.28.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
gotest.tools/v3 v3.4.0 // indirect
)

Expand Down
10 changes: 0 additions & 10 deletions modules/neo4j/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -99,9 +99,7 @@ github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+o
github.com/klauspost/compress v1.15.9 h1:wKRjX6JRtDdrE9qwa4b/Cip7ACOshUI4smpCQanqjSY=
github.com/klauspost/compress v1.15.9/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
Expand Down Expand Up @@ -138,7 +136,6 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
github.com/rogpeppe/go-internal v1.8.1 h1:geMPLpDpQOgVyCg5z5GoRwLHepNdb71NXb67XFkP+Eg=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/seccomp/libseccomp-golang v0.9.2-0.20220502022130-f33da4d89646/go.mod h1:JA8cRccbGaA1s33RQf7Y1+q9gHmZX1yB/z9WDN1C6fg=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
Expand All @@ -148,15 +145,10 @@ github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0
github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww=
github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE=
Expand Down Expand Up @@ -292,14 +284,12 @@ google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQ
google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw=
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
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/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
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/gotestsum v1.9.0 h1:Jbo/0k/sIOXIJu51IZxEAt27n77xspFEfL6SqKUR72A=
gotest.tools/gotestsum v1.9.0/go.mod h1:6JHCiN6TEjA7Kaz23q1bH0e2Dc3YJjDUZ0DmctFZf+w=
gotest.tools/v3 v3.3.0/go.mod h1:Mcr9QNxkg0uMvy/YElmo4SpXgJKWgQvYrT7Kw5RzJ1A=
Expand Down
14 changes: 10 additions & 4 deletions modules/neo4j/neo4j.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,9 @@ import (
"context"
"fmt"
"github.com/docker/go-connections/nat"
"github.com/testcontainers/testcontainers-go"
"github.com/testcontainers/testcontainers-go/wait"
"net/http"

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

const defaultImageName = "neo4j"
Expand Down Expand Up @@ -39,12 +38,18 @@ func (c Neo4jContainer) BoltUrl(ctx context.Context) (string, error) {

// StartContainer creates an instance of the Neo4j container type
func StartContainer(ctx context.Context, options ...Option) (*Neo4jContainer, error) {
settings := config{
settings := &config{
imageCoordinates: fmt.Sprintf("docker.io/%s:%s", defaultImageName, defaultTag),
adminPassword: "password",
neo4jSettings: map[string]string{},
logger: testcontainers.Logger,
}
for _, option := range options {
option(&settings)
option(settings)
}

if err := settings.validate(); err != nil {
return nil, err
}

httpPort, _ := nat.NewPort("tcp", defaultHttpPort)
Expand All @@ -69,6 +74,7 @@ func StartContainer(ctx context.Context, options ...Option) (*Neo4jContainer, er
container, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
ContainerRequest: request,
Started: true,
Logger: settings.logger,
})
if err != nil {
return nil, err
Expand Down
115 changes: 113 additions & 2 deletions modules/neo4j/neo4j_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,19 @@ package neo4j_test

import (
"context"
"fmt"
neo "github.com/neo4j/neo4j-go-driver/v5/neo4j"
"github.com/stretchr/testify/require"
"github.com/testcontainers/testcontainers-go/modules/neo4j"
"io"
"strings"
"testing"
)

const testPassword = "letmein!"

func TestNeo4j(outer *testing.T) {
outer.Parallel()

ctx := context.Background()

container := setupNeo4j(ctx, outer)
Expand Down Expand Up @@ -46,16 +50,92 @@ func TestNeo4j(outer *testing.T) {
}
})

outer.Run("is configured with custom Neo4j settings", func(t *testing.T) {
env := getContainerEnv(t, ctx, container)

if !strings.Contains(env, "NEO4J_dbms_tx__log_rotation_size=42M") {
t.Fatal("expected to custom setting to be exported but was not")
}
})
}

func TestNeo4jWithWrongSettings(outer *testing.T) {
outer.Parallel()

ctx := context.Background()

outer.Run("ignores auth setting outside WithAdminPassword", func(t *testing.T) {
container, err := neo4j.StartContainer(ctx,
neo4j.WithAdminPassword(testPassword),
neo4j.WithNeo4jSetting("AUTH", "neo4j/thisisgonnabeignored"),
)
if err != nil {
t.Fatalf("expected env to successfully run but did not: %s", err)
}
t.Cleanup(func() {
if err := container.Terminate(ctx); err != nil {
outer.Fatalf("failed to terminate container: %s", err)
}
})

env := getContainerEnv(t, ctx, container)

if !strings.Contains(env, "NEO4J_AUTH=neo4j/"+testPassword) {
t.Fatalf("expected WithAdminPassword to have higher precedence than auth set with WithNeo4jSetting")
}
})

outer.Run("warns about overwrites of setting keys", func(t *testing.T) {
logger := &inMemoryLogger{}
container, err := neo4j.StartContainer(ctx,
neo4j.WithLogger(logger), // needs to go before WithNeo4jSetting and WithNeo4jSettings
neo4j.WithAdminPassword(testPassword),
neo4j.WithNeo4jSetting("some.key", "value1"),
neo4j.WithNeo4jSettings(map[string]string{"some.key": "value2"}),
neo4j.WithNeo4jSetting("some.key", "value3"),
)
if err != nil {
t.Fatalf("expected env to successfully run but did not: %s", err)
}
t.Cleanup(func() {
if err := container.Terminate(ctx); err != nil {
outer.Fatalf("failed to terminate container: %s", err)
}
})

errorLogs := logger.Logs()
if !Contains(errorLogs, `setting "some.key" with value "value1" is now overwritten with value "value2"`+"\n") ||
!Contains(errorLogs, `setting "some.key" with value "value2" is now overwritten with value "value3"`+"\n") {
t.Fatalf("expected setting overwrites to be logged")
}
if !strings.Contains(getContainerEnv(t, ctx, container), "NEO4J_some_key=value3") {
t.Fatalf("expected custom setting to be set with last value")
}
})

outer.Run("rejects nil logger", func(t *testing.T) {
container, err := neo4j.StartContainer(ctx, neo4j.WithLogger(nil))

if container != nil {
t.Fatalf("container must not be created with nil logger")
}
if err == nil || err.Error() != "nil logger is not permitted" {
t.Fatalf("expected config validation error but got no error")
}
})
}

func setupNeo4j(ctx context.Context, t *testing.T) *neo4j.Neo4jContainer {
// neo4jCreateContainer {
container, err := neo4j.StartContainer(ctx,
neo4j.WithAdminPassword(testPassword),
neo4j.WithLabsPlugin(neo4j.Apoc),
neo4j.WithNeo4jSetting("dbms.tx_log.rotation.size", "42M"),
)
// }
require.Nil(t, err)
if err != nil {
t.Fatalf("expected container to successfully initialize but did not: %s", err)
}
return container
}

Expand All @@ -75,3 +155,34 @@ func createDriver(t *testing.T, ctx context.Context, container *neo4j.Neo4jConta
})
return driver
}

func getContainerEnv(t *testing.T, ctx context.Context, container *neo4j.Neo4jContainer) string {
exec, reader, err := container.Exec(ctx, []string{"env"})

if err != nil {
t.Fatalf("expected env to successfully run but did not: %s", err)
}
if exec != 0 {
t.Fatalf("expected env to exit with status 0 but exited with: %d", exec)
}
envVars, err := io.ReadAll(reader)
if err != nil {
t.Fatalf("expected to read all bytes from env output but did not: %s", err)
}
return string(envVars)
}

const logSeparator = "---$$$---"

type inMemoryLogger struct {
buffer strings.Builder
}

func (iml *inMemoryLogger) Printf(msg string, args ...interface{}) {
iml.buffer.Write([]byte(fmt.Sprintf(msg, args...)))
iml.buffer.Write([]byte(logSeparator))
}

func (iml *inMemoryLogger) Logs() []string {
return strings.Split(iml.buffer.String(), logSeparator)
}
10 changes: 10 additions & 0 deletions modules/neo4j/slices_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package neo4j_test

func Contains[T comparable](items []T, search T) bool {
for _, item := range items {
if item == search {
return true
}
}
return false
}

0 comments on commit bb04544

Please sign in to comment.