Skip to content

Commit 46d68aa

Browse files
authored
chore(elasticsearch)!: use Run function (#3407)
* chore(elasticsearch): use Run function * fix: close the temp file to avoid leaking a file descriptor * chore!: return error in options
1 parent 29480b8 commit 46d68aa

File tree

2 files changed

+111
-107
lines changed

2 files changed

+111
-107
lines changed

modules/elasticsearch/elasticsearch.go

Lines changed: 108 additions & 105 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,12 @@ import (
1414
)
1515

1616
const (
17-
defaultHTTPPort = "9200"
18-
defaultTCPPort = "9300"
19-
defaultPassword = "changeme"
20-
defaultUsername = "elastic"
21-
defaultCaCertPath = "/usr/share/elasticsearch/config/certs/http_ca.crt"
22-
minimalImageVersion = "7.9.2"
17+
defaultHTTPPort = "9200"
18+
defaultTCPPort = "9300"
19+
defaultPassword = "changeme"
20+
defaultUsername = "elastic"
21+
defaultCaCertPath = "/usr/share/elasticsearch/config/certs/http_ca.crt"
22+
envPassword = "ELASTIC_PASSWORD"
2323
)
2424

2525
const (
@@ -43,52 +43,37 @@ func RunContainer(ctx context.Context, opts ...testcontainers.ContainerCustomize
4343

4444
// Run creates an instance of the Elasticsearch container type
4545
func Run(ctx context.Context, img string, opts ...testcontainers.ContainerCustomizer) (*ElasticsearchContainer, error) {
46-
req := testcontainers.GenericContainerRequest{
47-
ContainerRequest: testcontainers.ContainerRequest{
48-
Image: img,
49-
Env: map[string]string{
50-
"discovery.type": "single-node",
51-
"cluster.routing.allocation.disk.threshold_enabled": "false",
52-
},
53-
ExposedPorts: []string{
54-
defaultHTTPPort + "/tcp",
55-
defaultTCPPort + "/tcp",
56-
},
57-
},
58-
Started: true,
46+
moduleOpts := []testcontainers.ContainerCustomizer{
47+
testcontainers.WithExposedPorts(defaultHTTPPort+"/tcp", defaultTCPPort+"/tcp"),
48+
testcontainers.WithEnv(map[string]string{
49+
"discovery.type": "single-node",
50+
"cluster.routing.allocation.disk.threshold_enabled": "false",
51+
}),
5952
}
6053

6154
// Gather all config options (defaults and then apply provided options)
6255
options := defaultOptions()
6356
for _, opt := range opts {
6457
if apply, ok := opt.(Option); ok {
65-
apply(options)
66-
}
67-
if err := opt.Customize(&req); err != nil {
68-
return nil, err
58+
if err := apply(options); err != nil {
59+
return nil, fmt.Errorf("apply option: %w", err)
60+
}
6961
}
7062
}
7163

72-
// Transfer the password settings to the container request
73-
if err := configurePassword(options, &req); err != nil {
74-
return nil, err
75-
}
64+
moduleOpts = append(moduleOpts, opts...)
7665

77-
if isAtLeastVersion(req.Image, 7) {
78-
req.LifecycleHooks = append(req.LifecycleHooks,
79-
testcontainers.ContainerLifecycleHooks{
80-
PostCreates: []testcontainers.ContainerHook{configureJvmOpts},
81-
},
82-
)
83-
}
66+
// Transfer the password settings to the container request
67+
moduleOpts = append(moduleOpts, configurePassword(options))
68+
moduleOpts = append(moduleOpts, configureJvmOpts())
8469

8570
// Set the default waiting strategy if not already set.
86-
setWaitFor(options, &req.ContainerRequest)
71+
moduleOpts = append(moduleOpts, configureWaitFor(options))
8772

88-
container, err := testcontainers.GenericContainer(ctx, req)
73+
ctr, err := testcontainers.Run(ctx, img, moduleOpts...)
8974
var esContainer *ElasticsearchContainer
90-
if container != nil {
91-
esContainer = &ElasticsearchContainer{Container: container, Settings: *options}
75+
if ctr != nil {
76+
esContainer = &ElasticsearchContainer{Container: ctr, Settings: *options}
9277
}
9378
if err != nil {
9479
return esContainer, fmt.Errorf("generic container: %w", err)
@@ -120,40 +105,32 @@ func (w *certWriter) Read(r io.Reader) error {
120105
return nil
121106
}
122107

123-
// setWaitFor sets the req.WaitingFor strategy based on settings.
124-
func setWaitFor(options *Options, req *testcontainers.ContainerRequest) {
125-
var strategies []wait.Strategy
126-
if req.WaitingFor != nil {
127-
// Custom waiting strategy, ensure we honour it.
128-
strategies = append(strategies, req.WaitingFor)
129-
}
108+
// configureWaitFor sets the req.WaitingFor strategy based on settings.
109+
func configureWaitFor(options *Options) testcontainers.CustomizeRequestOption {
110+
return func(req *testcontainers.GenericContainerRequest) error {
111+
var strategies []wait.Strategy
130112

131-
waitHTTP := wait.ForHTTP("/").WithPort(defaultHTTPPort)
132-
if sslRequired(req) {
133-
waitHTTP = waitHTTP.WithTLS(true).WithAllowInsecure(true)
134-
cw := &certWriter{
135-
options: options,
136-
certPool: x509.NewCertPool(),
137-
}
113+
waitHTTP := wait.ForHTTP("/").WithPort(defaultHTTPPort)
114+
if sslRequired(req) {
115+
cw := &certWriter{
116+
options: options,
117+
certPool: x509.NewCertPool(),
118+
}
138119

139-
waitHTTP = waitHTTP.
140-
WithTLS(true, &tls.Config{RootCAs: cw.certPool})
120+
waitHTTP = waitHTTP.
121+
WithTLS(true, &tls.Config{RootCAs: cw.certPool}).WithAllowInsecure(true)
141122

142-
strategies = append(strategies, wait.ForFile(defaultCaCertPath).WithMatcher(cw.Read))
143-
}
123+
strategies = append(strategies, wait.ForFile(defaultCaCertPath).WithMatcher(cw.Read))
124+
}
144125

145-
if options.Password != "" || options.Username != "" {
146-
waitHTTP = waitHTTP.WithBasicAuth(options.Username, options.Password)
147-
}
126+
if options.Password != "" || options.Username != "" {
127+
waitHTTP = waitHTTP.WithBasicAuth(options.Username, options.Password)
128+
}
148129

149-
strategies = append(strategies, waitHTTP)
130+
strategies = append(strategies, waitHTTP)
150131

151-
if len(strategies) > 1 {
152-
req.WaitingFor = wait.ForAll(strategies...)
153-
return
132+
return testcontainers.WithAdditionalWaitStrategy(strategies...)(req)
154133
}
155-
156-
req.WaitingFor = strategies[0]
157134
}
158135

159136
// configureAddress sets the address of the Elasticsearch container.
@@ -175,7 +152,7 @@ func (c *ElasticsearchContainer) configureAddress(ctx context.Context) error {
175152
}
176153

177154
// sslRequired returns true if the SSL is required, otherwise false.
178-
func sslRequired(req *testcontainers.ContainerRequest) bool {
155+
func sslRequired(req *testcontainers.GenericContainerRequest) bool {
179156
if !isAtLeastVersion(req.Image, 8) {
180157
return false
181158
}
@@ -200,58 +177,84 @@ func sslRequired(req *testcontainers.ContainerRequest) bool {
200177

201178
// configurePassword transfers the password settings to the container request.
202179
// If the password is not set, it will be set to "changeme" for Elasticsearch 8
203-
func configurePassword(settings *Options, req *testcontainers.GenericContainerRequest) error {
204-
// set "changeme" as default password for Elasticsearch 8
205-
if isAtLeastVersion(req.Image, 8) && settings.Password == "" {
206-
WithPassword(defaultPassword)(settings)
207-
}
208-
209-
if settings.Password != "" {
210-
if isOSS(req.Image) {
211-
return errors.New("it's not possible to activate security on Elastic OSS Image. Please switch to the default distribution")
180+
func configurePassword(settings *Options) testcontainers.CustomizeRequestOption {
181+
return func(req *testcontainers.GenericContainerRequest) error {
182+
// set "changeme" as default password for Elasticsearch 8
183+
if isAtLeastVersion(req.Image, 8) && settings.Password == "" {
184+
if err := WithPassword(defaultPassword)(settings); err != nil {
185+
return fmt.Errorf("with password: %w", err)
186+
}
212187
}
213188

214-
if _, ok := req.Env["ELASTIC_PASSWORD"]; !ok {
215-
req.Env["ELASTIC_PASSWORD"] = settings.Password
216-
}
189+
if settings.Password != "" {
190+
if isOSS(req.Image) {
191+
return errors.New("it's not possible to activate security on Elastic OSS Image. Please switch to the default distribution")
192+
}
217193

218-
// major version 8 is secure by default and does not need this to enable authentication
219-
if !isAtLeastVersion(req.Image, 8) {
220-
req.Env["xpack.security.enabled"] = "true"
194+
if _, ok := req.Env[envPassword]; !ok {
195+
req.Env[envPassword] = settings.Password
196+
}
197+
198+
// major version 8 is secure by default and does not need this to enable authentication
199+
if !isAtLeastVersion(req.Image, 8) {
200+
req.Env["xpack.security.enabled"] = "true"
201+
}
221202
}
222-
}
223203

224-
return nil
204+
return nil
205+
}
225206
}
226207

227208
// configureJvmOpts sets the default memory of the Elasticsearch instance to 2GB.
228209
// This functions, which is only available since version 7, is called as a post create hook
229210
// for the container request.
230-
func configureJvmOpts(ctx context.Context, container testcontainers.Container) error {
231-
// Sets default memory of elasticsearch instance to 2GB
232-
defaultJVMOpts := `-Xms2G
211+
func configureJvmOpts() testcontainers.CustomizeRequestOption {
212+
return func(req *testcontainers.GenericContainerRequest) error {
213+
if !isAtLeastVersion(req.Image, 7) {
214+
return nil
215+
}
216+
217+
return testcontainers.WithAdditionalLifecycleHooks(
218+
testcontainers.ContainerLifecycleHooks{
219+
PostCreates: []testcontainers.ContainerHook{
220+
func(ctx context.Context, container testcontainers.Container) error {
221+
// Sets default memory of elasticsearch instance to 2GB
222+
defaultJVMOpts := `-Xms2G
233223
-Xmx2G
234224
-Dingest.geoip.downloader.enabled.default=false
235225
`
236226

237-
tmpDir := os.TempDir()
238-
239-
tmpFile, err := os.CreateTemp(tmpDir, "elasticsearch-default-memory-vm.options")
240-
if err != nil {
241-
return err
242-
}
243-
defer os.Remove(tmpFile.Name()) // clean up
244-
245-
if _, err := tmpFile.WriteString(defaultJVMOpts); err != nil {
246-
return err
247-
}
248-
249-
// Spaces are deliberate to allow user to define additional jvm options as elasticsearch resolves option files lexicographically
250-
if err := container.CopyFileToContainer(
251-
ctx, tmpFile.Name(),
252-
"/usr/share/elasticsearch/config/jvm.options.d/ elasticsearch-default-memory-vm.options", 0o644); err != nil {
253-
return err
227+
tmpDir := os.TempDir()
228+
229+
// The temp file is closed to not leak a file descriptor.
230+
tmpFile, err := os.CreateTemp(tmpDir, "elasticsearch-default-memory-vm.options")
231+
if err != nil {
232+
return err
233+
}
234+
defer os.Remove(tmpFile.Name()) // clean up
235+
236+
if _, err := tmpFile.WriteString(defaultJVMOpts); err != nil {
237+
if cerr := tmpFile.Close(); cerr != nil {
238+
return errors.Join(err, cerr)
239+
}
240+
return err
241+
}
242+
243+
if err := tmpFile.Close(); err != nil {
244+
return err
245+
}
246+
247+
// Spaces are deliberate to allow user to define additional jvm options as elasticsearch resolves option files lexicographically
248+
if err := container.CopyFileToContainer(
249+
ctx, tmpFile.Name(),
250+
"/usr/share/elasticsearch/config/jvm.options.d/ elasticsearch-default-memory-vm.options", 0o644); err != nil {
251+
return err
252+
}
253+
254+
return nil
255+
},
256+
},
257+
},
258+
)(req)
254259
}
255-
256-
return nil
257260
}

modules/elasticsearch/options.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ func defaultOptions() *Options {
2424
var _ testcontainers.ContainerCustomizer = (*Option)(nil)
2525

2626
// Option is an option for the Elasticsearch container.
27-
type Option func(*Options)
27+
type Option func(*Options) error
2828

2929
// Customize is a NOOP. It's defined to satisfy the testcontainers.ContainerCustomizer interface.
3030
func (o Option) Customize(*testcontainers.GenericContainerRequest) error {
@@ -34,7 +34,8 @@ func (o Option) Customize(*testcontainers.GenericContainerRequest) error {
3434

3535
// WithPassword sets the password for the Elasticsearch container.
3636
func WithPassword(password string) Option {
37-
return func(o *Options) {
37+
return func(o *Options) error {
3838
o.Password = password
39+
return nil
3940
}
4041
}

0 commit comments

Comments
 (0)