From bb045441de4228aa97032261e4d66b74d4f61932 Mon Sep 17 00:00:00 2001 From: Florent Biville <445792+fbiville@users.noreply.github.com> Date: Thu, 30 Mar 2023 18:30:04 +0200 Subject: [PATCH] Add support for Neo4j configuration (#992) * 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 --- modules/neo4j/config.go | 67 ++++++++++++++++++-- modules/neo4j/go.mod | 4 -- modules/neo4j/go.sum | 10 --- modules/neo4j/neo4j.go | 14 +++-- modules/neo4j/neo4j_test.go | 115 ++++++++++++++++++++++++++++++++++- modules/neo4j/slices_test.go | 10 +++ 6 files changed, 196 insertions(+), 24 deletions(-) create mode 100644 modules/neo4j/slices_test.go diff --git a/modules/neo4j/config.go b/modules/neo4j/config.go index 414223ca487..cbf27e65a67 100644 --- a/modules/neo4j/config.go +++ b/modules/neo4j/config.go @@ -1,7 +1,9 @@ package neo4j import ( + "errors" "fmt" + "github.com/testcontainers/testcontainers-go" "strings" ) @@ -11,6 +13,8 @@ type config struct { imageCoordinates string adminPassword string labsPlugins []string + neo4jSettings map[string]string + logger testcontainers.Logging } type LabsPlugin string @@ -57,8 +61,42 @@ 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() @@ -66,13 +104,34 @@ func (c config) exportEnv() map[string]string { 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) +} diff --git a/modules/neo4j/go.mod b/modules/neo4j/go.mod index 67dfb5ebfd8..a5ada1a1191 100644 --- a/modules/neo4j/go.mod +++ b/modules/neo4j/go.mod @@ -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 ) @@ -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 @@ -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 @@ -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 ) diff --git a/modules/neo4j/go.sum b/modules/neo4j/go.sum index bb2bdbd2d9f..981a2d32d80 100644 --- a/modules/neo4j/go.sum +++ b/modules/neo4j/go.sum @@ -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= @@ -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= @@ -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= @@ -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= diff --git a/modules/neo4j/neo4j.go b/modules/neo4j/neo4j.go index af3e3b61dce..e4b36e01504 100644 --- a/modules/neo4j/neo4j.go +++ b/modules/neo4j/neo4j.go @@ -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" @@ -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) @@ -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 diff --git a/modules/neo4j/neo4j_test.go b/modules/neo4j/neo4j_test.go index a2b085485eb..1f8ce5355e9 100644 --- a/modules/neo4j/neo4j_test.go +++ b/modules/neo4j/neo4j_test.go @@ -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) @@ -46,6 +50,79 @@ 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 { @@ -53,9 +130,12 @@ func setupNeo4j(ctx context.Context, t *testing.T) *neo4j.Neo4jContainer { 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 } @@ -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) +} diff --git a/modules/neo4j/slices_test.go b/modules/neo4j/slices_test.go new file mode 100644 index 00000000000..c35fcd3ce73 --- /dev/null +++ b/modules/neo4j/slices_test.go @@ -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 +}