forked from elastic/elasticsearch
/
DockerTests.java
818 lines (658 loc) · 35.2 KB
/
DockerTests.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
package org.elasticsearch.packaging.test;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.http.client.fluent.Request;
import org.elasticsearch.packaging.util.Distribution;
import org.elasticsearch.packaging.util.Installation;
import org.elasticsearch.packaging.util.Platforms;
import org.elasticsearch.packaging.util.ProcessInfo;
import org.elasticsearch.packaging.util.ServerUtils;
import org.elasticsearch.packaging.util.Shell;
import org.elasticsearch.packaging.util.Shell.Result;
import org.junit.After;
import org.junit.Before;
import org.junit.BeforeClass;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import static java.nio.file.attribute.PosixFilePermissions.fromString;
import static org.elasticsearch.packaging.util.Docker.assertPermissionsAndOwnership;
import static org.elasticsearch.packaging.util.Docker.chownWithPrivilegeEscalation;
import static org.elasticsearch.packaging.util.Docker.copyFromContainer;
import static org.elasticsearch.packaging.util.Docker.existsInContainer;
import static org.elasticsearch.packaging.util.Docker.getContainerLogs;
import static org.elasticsearch.packaging.util.Docker.getImageLabels;
import static org.elasticsearch.packaging.util.Docker.getImageName;
import static org.elasticsearch.packaging.util.Docker.getJson;
import static org.elasticsearch.packaging.util.Docker.mkDirWithPrivilegeEscalation;
import static org.elasticsearch.packaging.util.Docker.removeContainer;
import static org.elasticsearch.packaging.util.Docker.rmDirWithPrivilegeEscalation;
import static org.elasticsearch.packaging.util.Docker.runContainer;
import static org.elasticsearch.packaging.util.Docker.runContainerExpectingFailure;
import static org.elasticsearch.packaging.util.Docker.verifyContainerInstallation;
import static org.elasticsearch.packaging.util.Docker.waitForElasticsearch;
import static org.elasticsearch.packaging.util.DockerRun.builder;
import static org.elasticsearch.packaging.util.FileMatcher.p600;
import static org.elasticsearch.packaging.util.FileMatcher.p644;
import static org.elasticsearch.packaging.util.FileMatcher.p660;
import static org.elasticsearch.packaging.util.FileMatcher.p775;
import static org.elasticsearch.packaging.util.FileUtils.append;
import static org.elasticsearch.packaging.util.FileUtils.rm;
import static org.hamcrest.Matchers.arrayContaining;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.emptyString;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.greaterThanOrEqualTo;
import static org.hamcrest.Matchers.hasItems;
import static org.hamcrest.Matchers.hasKey;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.matchesPattern;
import static org.hamcrest.Matchers.not;
import static org.hamcrest.Matchers.nullValue;
import static org.hamcrest.Matchers.startsWith;
import static org.junit.Assume.assumeFalse;
import static org.junit.Assume.assumeTrue;
/**
* This class tests the Elasticsearch Docker images. We have more than one because we build
* an image with a custom, small base image, and an image based on RedHat's UBI.
*/
public class DockerTests extends PackagingTestCase {
private Path tempDir;
@BeforeClass
public static void filterDistros() {
assumeTrue("only Docker", distribution().isDocker());
}
@Before
public void setupTest() throws IOException {
installation = runContainer(distribution());
tempDir = createTempDir(DockerTests.class.getSimpleName());
}
@After
public void teardownTest() {
removeContainer();
rm(tempDir);
}
/**
* Checks that the Docker image can be run, and that it passes various checks.
*/
public void test010Install() {
verifyContainerInstallation(installation, distribution());
}
/**
* Check that the /_xpack API endpoint's presence is correct for the type of distribution being tested.
*/
public void test011PresenceOfXpack() throws Exception {
waitForElasticsearch(installation);
final int statusCode = Request.Get("http://localhost:9200/_xpack").execute().returnResponse().getStatusLine().getStatusCode();
if (distribution.isOSS()) {
assertThat(statusCode, greaterThanOrEqualTo(400));
} else {
assertThat(statusCode, equalTo(200));
}
}
/**
* Checks that no plugins are initially active.
*/
public void test020PluginsListWithNoPlugins() {
final Installation.Executables bin = installation.executables();
final Result r = sh.run(bin.pluginTool + " list");
assertThat("Expected no plugins to be listed", r.stdout, emptyString());
}
/**
* Check that the JDK's cacerts file is a symlink to the copy provided by the operating system.
*/
public void test040JavaUsesTheOsProvidedKeystore() {
final String path = sh.run("realpath jdk/lib/security/cacerts").stdout;
assertThat(path, equalTo("/etc/pki/ca-trust/extracted/java/cacerts"));
}
/**
* Checks that there are Amazon trusted certificates in the cacaerts keystore.
*/
public void test041AmazonCaCertsAreInTheKeystore() {
final boolean matches = sh.run("jdk/bin/keytool -cacerts -storepass changeit -list | grep trustedCertEntry").stdout.lines()
.anyMatch(line -> line.contains("amazonrootca"));
assertTrue("Expected Amazon trusted cert in cacerts", matches);
}
/**
* Check that when the keystore is created on startup, it is created with the correct permissions.
*/
public void test042KeystorePermissionsAreCorrect() throws Exception {
waitForElasticsearch(installation);
assertPermissionsAndOwnership(installation.config("elasticsearch.keystore"), p660);
}
/**
* Send some basic index, count and delete requests, in order to check that the installation
* is minimally functional.
*/
public void test050BasicApiTests() throws Exception {
waitForElasticsearch(installation);
assertTrue(existsInContainer(installation.logs.resolve("gc.log")));
ServerUtils.runElasticsearchTests();
}
/**
* Check that the default config can be overridden using a bind mount, and that env vars are respected
*/
public void test070BindMountCustomPathConfAndJvmOptions() throws Exception {
copyFromContainer(installation.config("elasticsearch.yml"), tempDir.resolve("elasticsearch.yml"));
copyFromContainer(installation.config("log4j2.properties"), tempDir.resolve("log4j2.properties"));
// we have to disable Log4j from using JMX lest it will hit a security
// manager exception before we have configured logging; this will fail
// startup since we detect usages of logging before it is configured
final String jvmOptions = "-Xms512m\n-Xmx512m\n-Dlog4j2.disable.jmx=true\n";
append(tempDir.resolve("jvm.options"), jvmOptions);
// Make the temp directory and contents accessible when bind-mounted.
Files.setPosixFilePermissions(tempDir, fromString("rwxrwxrwx"));
// These permissions are necessary to run the tests under Vagrant
Files.setPosixFilePermissions(tempDir.resolve("elasticsearch.yml"), p644);
Files.setPosixFilePermissions(tempDir.resolve("log4j2.properties"), p644);
// Restart the container
final Map<Path, Path> volumes = Map.of(tempDir, Path.of("/usr/share/elasticsearch/config"));
runContainer(distribution(), builder().volumes(volumes).envVars(Map.of("ES_JAVA_OPTS", "-XX:-UseCompressedOops")));
waitForElasticsearch(installation);
final JsonNode nodes = getJson("/_nodes").get("nodes");
final String nodeId = nodes.fieldNames().next();
final int heapSize = nodes.at("/" + nodeId + "/jvm/mem/heap_init_in_bytes").intValue();
final boolean usingCompressedPointers = nodes.at("/" + nodeId + "/jvm/using_compressed_ordinary_object_pointers").asBoolean();
logger.warn(nodes.at("/" + nodeId + "/jvm/mem/heap_init_in_bytes"));
assertThat("heap_init_in_bytes", heapSize, equalTo(536870912));
assertThat("using_compressed_ordinary_object_pointers", usingCompressedPointers, equalTo(false));
}
/**
* Check that the default config can be overridden using a bind mount, and that env vars are respected.
*/
public void test071BindMountCustomPathWithDifferentUID() throws Exception {
Platforms.onLinux(() -> {
final Path tempEsDataDir = tempDir.resolve("esDataDir");
// Make the local directory and contents accessible when bind-mounted
mkDirWithPrivilegeEscalation(tempEsDataDir, 1500, 0);
// Restart the container
final Map<Path, Path> volumes = Map.of(tempEsDataDir.toAbsolutePath(), installation.data);
runContainer(distribution(), builder().volumes(volumes));
waitForElasticsearch(installation);
final JsonNode nodes = getJson("/_nodes");
assertThat(nodes.at("/_nodes/total").intValue(), equalTo(1));
assertThat(nodes.at("/_nodes/successful").intValue(), equalTo(1));
assertThat(nodes.at("/_nodes/failed").intValue(), equalTo(0));
// Ensure container is stopped before we remove tempEsDataDir, so nothing
// is using the directory.
removeContainer();
rmDirWithPrivilegeEscalation(tempEsDataDir);
});
}
/**
* Check that it is possible to run Elasticsearch under a different user and group to the default.
* Note that while the default configuration files are world-readable, when we execute Elasticsearch
* it will attempt to create a keystore under the `config` directory. This will fail unless
* we also bind-mount the config dir.
*/
public void test072RunEsAsDifferentUserAndGroup() throws Exception {
assumeFalse(Platforms.WINDOWS);
final Path tempEsDataDir = tempDir.resolve("esDataDir");
final Path tempEsConfigDir = tempDir.resolve("esConfDir");
final Path tempEsLogsDir = tempDir.resolve("esLogsDir");
Files.createDirectory(tempEsConfigDir);
Files.createDirectory(tempEsConfigDir.resolve("jvm.options.d"));
Files.createDirectory(tempEsDataDir);
Files.createDirectory(tempEsLogsDir);
copyFromContainer(installation.config("elasticsearch.yml"), tempEsConfigDir);
copyFromContainer(installation.config("jvm.options"), tempEsConfigDir);
copyFromContainer(installation.config("log4j2.properties"), tempEsConfigDir);
chownWithPrivilegeEscalation(tempEsConfigDir, "501:501");
chownWithPrivilegeEscalation(tempEsDataDir, "501:501");
chownWithPrivilegeEscalation(tempEsLogsDir, "501:501");
// Define the bind mounts
final Map<Path, Path> volumes = new HashMap<>();
volumes.put(tempEsDataDir.toAbsolutePath(), installation.data);
volumes.put(tempEsConfigDir.toAbsolutePath(), installation.config);
volumes.put(tempEsLogsDir.toAbsolutePath(), installation.logs);
// Restart the container
runContainer(distribution(), builder().volumes(volumes).uid(501, 501));
waitForElasticsearch(installation);
}
/**
* Check that it is possible to run Elasticsearch under a different user and group to the default,
* without bind-mounting any directories, provided the container user is added to the `root` group.
*/
public void test073RunEsAsDifferentUserAndGroupWithoutBindMounting() throws Exception {
// Restart the container
runContainer(distribution(), builder().uid(501, 501).extraArgs("--group-add 0"));
waitForElasticsearch(installation);
}
/**
* Check that the elastic user's password can be configured via a file and the ELASTIC_PASSWORD_FILE environment variable.
*/
public void test080ConfigurePasswordThroughEnvironmentVariableFile() throws Exception {
// Test relies on configuring security
assumeTrue(distribution.isDefault());
final String xpackPassword = "hunter2";
final String passwordFilename = "password.txt";
// ELASTIC_PASSWORD_FILE
Files.writeString(tempDir.resolve(passwordFilename), xpackPassword + "\n");
Map<String, String> envVars = Map.of(
"ELASTIC_PASSWORD_FILE",
"/run/secrets/" + passwordFilename,
// Enable security so that we can test that the password has been used
"xpack.security.enabled",
"true"
);
// File permissions need to be secured in order for the ES wrapper to accept
// them for populating env var values
Files.setPosixFilePermissions(tempDir.resolve(passwordFilename), p600);
// But when running in Vagrant, also ensure ES can actually access the file
chownWithPrivilegeEscalation(tempDir.resolve(passwordFilename), "1000:0");
final Map<Path, Path> volumes = Map.of(tempDir, Path.of("/run/secrets"));
// Restart the container
runContainer(distribution(), builder().volumes(volumes).envVars(envVars));
// If we configured security correctly, then this call will only work if we specify the correct credentials.
try {
waitForElasticsearch("green", null, installation, "elastic", "hunter2");
} catch (Exception e) {
throw new AssertionError(
"Failed to check whether Elasticsearch had started. This could be because "
+ "authentication isn't working properly. Check the container logs",
e
);
}
// Also check that an unauthenticated call fails
final int statusCode = Request.Get("http://localhost:9200/_nodes").execute().returnResponse().getStatusLine().getStatusCode();
assertThat("Expected server to require authentication", statusCode, equalTo(401));
}
/**
* Check that when verifying the file permissions of _FILE environment variables, symlinks
* are followed.
*/
public void test081SymlinksAreFollowedWithEnvironmentVariableFiles() throws Exception {
// Test relies on configuring security
assumeTrue(distribution.isDefault());
// Test relies on symlinks
assumeFalse(Platforms.WINDOWS);
final String xpackPassword = "hunter2";
final String passwordFilename = "password.txt";
final String symlinkFilename = "password_symlink";
// ELASTIC_PASSWORD_FILE
Files.writeString(tempDir.resolve(passwordFilename), xpackPassword + "\n");
// Link to the password file. We can't use an absolute path for the target, because
// it won't resolve inside the container.
Files.createSymbolicLink(tempDir.resolve(symlinkFilename), Path.of(passwordFilename));
Map<String, String> envVars = Map.of(
"ELASTIC_PASSWORD_FILE",
"/run/secrets/" + symlinkFilename,
// Enable security so that we can test that the password has been used
"xpack.security.enabled",
"true"
);
// File permissions need to be secured in order for the ES wrapper to accept
// them for populating env var values. The wrapper will resolve the symlink
// and check the target's permissions.
Files.setPosixFilePermissions(tempDir.resolve(passwordFilename), p600);
final Map<Path, Path> volumes = Map.of(tempDir, Path.of("/run/secrets"));
// Restart the container - this will check that Elasticsearch started correctly,
// and didn't fail to follow the symlink and check the file permissions
runContainer(distribution(), builder().volumes(volumes).envVars(envVars));
}
/**
* Check that environment variables cannot be used with _FILE environment variables.
*/
public void test082CannotUseEnvVarsAndFiles() throws Exception {
final String passwordFilename = "password.txt";
Files.writeString(tempDir.resolve(passwordFilename), "other_hunter2\n");
Map<String, String> envVars = new HashMap<>();
envVars.put("ELASTIC_PASSWORD", "hunter2");
envVars.put("ELASTIC_PASSWORD_FILE", "/run/secrets/" + passwordFilename);
// File permissions need to be secured in order for the ES wrapper to accept
// them for populating env var values
Files.setPosixFilePermissions(tempDir.resolve(passwordFilename), p600);
final Map<Path, Path> volumes = Map.of(tempDir, Path.of("/run/secrets"));
final Result dockerLogs = runContainerExpectingFailure(distribution, builder().volumes(volumes).envVars(envVars));
assertThat(
dockerLogs.stderr,
containsString("ERROR: Both ELASTIC_PASSWORD_FILE and ELASTIC_PASSWORD are set. These are mutually exclusive.")
);
}
/**
* Check that when populating environment variables by setting variables with the suffix "_FILE",
* the files' permissions are checked.
*/
public void test083EnvironmentVariablesUsingFilesHaveCorrectPermissions() throws Exception {
final String passwordFilename = "password.txt";
Files.writeString(tempDir.resolve(passwordFilename), "hunter2\n");
Map<String, String> envVars = Map.of("ELASTIC_PASSWORD_FILE", "/run/secrets/" + passwordFilename);
// Set invalid file permissions
Files.setPosixFilePermissions(tempDir.resolve(passwordFilename), p660);
final Map<Path, Path> volumes = Map.of(tempDir, Path.of("/run/secrets"));
// Restart the container
final Result dockerLogs = runContainerExpectingFailure(distribution(), builder().volumes(volumes).envVars(envVars));
assertThat(
dockerLogs.stderr,
containsString(
"ERROR: File /run/secrets/" + passwordFilename + " from ELASTIC_PASSWORD_FILE must have file permissions 400 or 600"
)
);
}
/**
* Check that when verifying the file permissions of _FILE environment variables, symlinks
* are followed, and that invalid target permissions are detected.
*/
public void test084SymlinkToFileWithInvalidPermissionsIsRejected() throws Exception {
// Test relies on configuring security
assumeTrue(distribution.isDefault());
// Test relies on symlinks
assumeFalse(Platforms.WINDOWS);
final String xpackPassword = "hunter2";
final String passwordFilename = "password.txt";
final String symlinkFilename = "password_symlink";
// ELASTIC_PASSWORD_FILE
Files.writeString(tempDir.resolve(passwordFilename), xpackPassword + "\n");
// Link to the password file. We can't use an absolute path for the target, because
// it won't resolve inside the container.
Files.createSymbolicLink(tempDir.resolve(symlinkFilename), Path.of(passwordFilename));
Map<String, String> envVars = Map.of(
"ELASTIC_PASSWORD_FILE",
"/run/secrets/" + symlinkFilename,
// Enable security so that we can test that the password has been used
"xpack.security.enabled",
"true"
);
// Set invalid permissions on the file that the symlink targets
Files.setPosixFilePermissions(tempDir.resolve(passwordFilename), p775);
final Map<Path, Path> volumes = Map.of(tempDir, Path.of("/run/secrets"));
// Restart the container
final Result dockerLogs = runContainerExpectingFailure(distribution(), builder().volumes(volumes).envVars(envVars));
assertThat(
dockerLogs.stderr,
containsString(
"ERROR: File "
+ passwordFilename
+ " (target of symlink /run/secrets/"
+ symlinkFilename
+ " from ELASTIC_PASSWORD_FILE) must have file permissions 400 or 600, but actually has: 775"
)
);
}
/**
* Check that environment variables are translated to -E options even for commands invoked under
* `docker exec`, where the Docker image's entrypoint is not executed.
*/
public void test085EnvironmentVariablesAreRespectedUnderDockerExec() throws Exception {
// This test relies on a CLI tool attempting to connect to Elasticsearch, and the
// tool in question is only in the default distribution.
assumeTrue(distribution.isDefault());
installation = runContainer(
distribution(),
builder().envVars(Map.of("xpack.security.enabled", "true", "ELASTIC_PASSWORD", "hunter2"))
);
// The tool below requires a keystore, so ensure that ES is fully initialised before proceeding.
waitForElasticsearch("green", null, installation, "elastic", "hunter2");
sh.getEnv().put("http.host", "this.is.not.valid");
// This will fail because of the extra env var
final Result result = sh.runIgnoreExitCode("bash -c 'echo y | elasticsearch-setup-passwords auto'");
assertFalse("elasticsearch-setup-passwords command should have failed", result.isSuccess());
assertThat(result.stdout, containsString("java.net.UnknownHostException: this.is.not.valid"));
}
/**
* Check whether the elasticsearch-certutil tool has been shipped correctly,
* and if present then it can execute.
*/
public void test090SecurityCliPackaging() {
final Installation.Executables bin = installation.executables();
final Path securityCli = installation.lib.resolve("tools").resolve("security-cli");
if (distribution().isDefault()) {
assertTrue(existsInContainer(securityCli));
Result result = sh.run(bin.certutilTool + " --help");
assertThat(result.stdout, containsString("Simplifies certificate creation for use with the Elastic Stack"));
// Ensure that the exit code from the java command is passed back up through the shell script
result = sh.runIgnoreExitCode(bin.certutilTool + " invalid-command");
assertThat(result.isSuccess(), is(false));
assertThat(result.stdout, containsString("Unknown command [invalid-command]"));
} else {
assertFalse(existsInContainer(securityCli));
}
}
/**
* Check that the elasticsearch-shard tool is shipped in the Docker image and is executable.
*/
public void test091ElasticsearchShardCliPackaging() {
final Installation.Executables bin = installation.executables();
final Result result = sh.run(bin.shardTool + " -h");
assertThat(result.stdout, containsString("A CLI tool to remove corrupted parts of unrecoverable shards"));
}
/**
* Check that the elasticsearch-node tool is shipped in the Docker image and is executable.
*/
public void test092ElasticsearchNodeCliPackaging() {
final Installation.Executables bin = installation.executables();
final Result result = sh.run(bin.nodeTool + " -h");
assertThat(
"Failed to find expected message about the elasticsearch-node CLI tool",
result.stdout,
containsString("A CLI tool to do unsafe cluster and index manipulations on current node")
);
}
/**
* Check that no core dumps have been accidentally included in the Docker image.
*/
public void test100NoCoreFilesInImage() {
assertFalse("Unexpected core dump found in Docker image", existsInContainer("/core*"));
}
/**
* Check that there are no files with a GID other than 0.
*/
public void test101AllFilesAreGroupZero() {
// Run a `find` command in a new container without Elasticsearch running, so
// that the results aren't subject to sporadic failures from files appearing /
// disappearing while `find` is traversing the filesystem.
//
// We also create a file under `data/` to ensure that files are created with the
// expected group.
final Shell localSh = new Shell();
final String findResults = localSh.run(
"docker run --rm --tty " + getImageName(distribution) + " bash -c ' touch data/test && find . \\! -group 0 ' "
).stdout;
assertThat("Found some files whose GID != 0", findResults, is(emptyString()));
}
/**
* Check that the Docker image has the expected "Label Schema" labels.
* @see <a href="http://label-schema.org/">Label Schema website</a>
*/
public void test110OrgLabelSchemaLabels() throws Exception {
final Map<String, String> labels = getImageLabels(distribution);
final Map<String, String> staticLabels = new HashMap<>();
staticLabels.put("name", "Elasticsearch");
staticLabels.put("schema-version", "1.0");
staticLabels.put("url", "https://www.elastic.co/products/elasticsearch");
staticLabels.put("usage", "https://www.elastic.co/guide/en/elasticsearch/reference/index.html");
staticLabels.put("vcs-url", "https://github.com/elastic/elasticsearch");
staticLabels.put("vendor", "Elastic");
staticLabels.put("license", "Elastic-License-2.0");
// TODO: we should check the actual version value
final Set<String> dynamicLabels = Set.of("build-date", "vcs-ref", "version");
final String prefix = "org.label-schema";
staticLabels.forEach((suffix, value) -> {
String key = prefix + "." + suffix;
assertThat(labels, hasKey(key));
assertThat(labels.get(key), equalTo(value));
});
dynamicLabels.forEach(label -> {
String key = prefix + "." + label;
assertThat(labels, hasKey(key));
});
}
/**
* Check that the Docker image has the expected "Open Containers Annotations" labels.
* @see <a href="https://github.com/opencontainers/image-spec/blob/master/annotations.md">Open Containers Annotations</a>
*/
public void test110OrgOpencontainersLabels() throws Exception {
final Map<String, String> labels = getImageLabels(distribution);
final Map<String, String> staticLabels = new HashMap<>();
staticLabels.put("title", "Elasticsearch");
staticLabels.put("url", "https://www.elastic.co/products/elasticsearch");
staticLabels.put("documentation", "https://www.elastic.co/guide/en/elasticsearch/reference/index.html");
staticLabels.put("source", "https://github.com/elastic/elasticsearch");
staticLabels.put("vendor", "Elastic");
staticLabels.put("licenses", "Elastic-License-2.0");
// TODO: we should check the actual version value
final Set<String> dynamicLabels = Set.of("created", "revision", "version");
final String prefix = "org.opencontainers.image";
staticLabels.forEach((suffix, value) -> {
String key = prefix + "." + suffix;
assertThat(labels, hasKey(key));
assertThat(labels.get(key), equalTo(value));
});
dynamicLabels.forEach(label -> {
String key = prefix + "." + label;
assertThat(labels, hasKey(key));
});
}
/**
* Check that the container logs contain the expected content for Elasticsearch itself.
*/
public void test120DockerLogsIncludeElasticsearchLogs() throws Exception {
waitForElasticsearch(installation);
final Result containerLogs = getContainerLogs();
assertThat("Container logs should contain full class names", containerLogs.stdout, containsString("org.elasticsearch.node.Node"));
assertThat("Container logs don't contain INFO level messages", containerLogs.stdout, containsString("INFO"));
}
/**
* Check that it is possible to write logs to disk
*/
public void test121CanUseStackLoggingConfig() throws Exception {
runContainer(distribution(), builder().envVars(Map.of("ES_LOG_STYLE", "file")));
waitForElasticsearch(installation);
final Result containerLogs = getContainerLogs();
final List<String> stdout = containerLogs.stdout.lines().collect(Collectors.toList());
assertThat(
"Container logs should be formatted using the stack config",
stdout.get(stdout.size() - 1),
matchesPattern("^\\[\\d\\d\\d\\d-.*")
);
assertThat("[logs/docker-cluster.log] should exist but it doesn't", existsInContainer("logs/docker-cluster.log"), is(true));
}
/**
* Check that the default logging config can be explicitly selected.
*/
public void test122CanUseDockerLoggingConfig() throws Exception {
runContainer(distribution(), builder().envVars(Map.of("ES_LOG_STYLE", "console")));
waitForElasticsearch(installation);
final Result containerLogs = getContainerLogs();
final List<String> stdout = containerLogs.stdout.lines().collect(Collectors.toList());
assertThat("Container logs should be formatted using the docker config", stdout.get(stdout.size() - 1), startsWith("{\""));
assertThat("[logs/docker-cluster.log] shouldn't exist but it does", existsInContainer("logs/docker-cluster.log"), is(false));
}
/**
* Check that an unknown logging config is rejected
*/
public void test123CannotUseUnknownLoggingConfig() {
final Result result = runContainerExpectingFailure(distribution(), builder().envVars(Map.of("ES_LOG_STYLE", "unknown")));
assertThat(result.stderr, containsString("ERROR: ES_LOG_STYLE set to [unknown]. Expected [console] or [file]"));
}
/**
* Check that the Java process running inside the container has the expected UID, GID and username.
*/
public void test130JavaHasCorrectOwnership() {
final ProcessInfo info = ProcessInfo.getProcessInfo(sh, "java");
assertThat("Incorrect UID", info.uid, equalTo(1000));
assertThat("Incorrect username", info.username, equalTo("elasticsearch"));
assertThat("Incorrect GID", info.gid, equalTo(0));
assertThat("Incorrect group", info.group, equalTo("root"));
}
/**
* Check that the init process running inside the container has the expected PID, UID, GID and user.
* The PID is particularly important because PID 1 handles signal forwarding and child reaping.
*/
public void test131InitProcessHasCorrectPID() {
final ProcessInfo info = ProcessInfo.getProcessInfo(sh, "tini");
assertThat("Incorrect PID", info.pid, equalTo(1));
assertThat("Incorrect UID", info.uid, equalTo(1000));
assertThat("Incorrect username", info.username, equalTo("elasticsearch"));
assertThat("Incorrect GID", info.gid, equalTo(0));
assertThat("Incorrect group", info.group, equalTo("root"));
}
/**
* Check that Elasticsearch reports per-node cgroup information.
*/
public void test140CgroupOsStatsAreAvailable() throws Exception {
waitForElasticsearch(installation);
final JsonNode nodes = getJson("/_nodes/stats/os").get("nodes");
final String nodeId = nodes.fieldNames().next();
final JsonNode cgroupStats = nodes.at("/" + nodeId + "/os/cgroup");
assertFalse("Couldn't find /nodes/{nodeId}/os/cgroup in API response", cgroupStats.isMissingNode());
assertThat("Failed to find [cpu] in node OS cgroup stats", cgroupStats.get("cpu"), not(nullValue()));
assertThat("Failed to find [cpuacct] in node OS cgroup stats", cgroupStats.get("cpuacct"), not(nullValue()));
}
/**
* Check that when available system memory is constrained by Docker, the machine-dependant heap sizing
* logic sets the correct heap size, based on the container limits.
*/
public void test150MachineDependentHeap() throws Exception {
// Start by ensuring `jvm.options` doesn't define any heap options
final Path jvmOptionsPath = tempDir.resolve("jvm.options");
final Path containerJvmOptionsPath = installation.config("jvm.options");
copyFromContainer(containerJvmOptionsPath, jvmOptionsPath);
final List<String> jvmOptions = Files.readAllLines(jvmOptionsPath)
.stream()
.filter(line -> (line.startsWith("-Xms") || line.startsWith("-Xmx")) == false)
.collect(Collectors.toList());
Files.writeString(jvmOptionsPath, String.join("\n", jvmOptions));
// Now run the container, being explicit about the available memory
runContainer(distribution(), builder().memory("942m").volumes(Map.of(jvmOptionsPath, containerJvmOptionsPath)));
waitForElasticsearch(installation);
// Grab the container output and find the line where it print the JVM arguments. This will
// let us see what the automatic heap sizing calculated.
final Optional<String> jvmArgumentsLine = getContainerLogs().stdout.lines()
.filter(line -> line.contains("JVM arguments"))
.findFirst();
assertThat("Failed to find jvmArguments in container logs", jvmArgumentsLine.isPresent(), is(true));
final JsonNode jsonNode = new ObjectMapper().readTree(jvmArgumentsLine.get());
final String argsStr = jsonNode.get("message").textValue();
final List<String> xArgs = Arrays.stream(argsStr.substring(1, argsStr.length() - 1).split(",\\s*"))
.filter(arg -> arg.startsWith("-X"))
.collect(Collectors.toList());
// This is roughly 0.4 * 942
assertThat(xArgs, hasItems("-Xms376m", "-Xmx376m"));
}
/**
* Check that the UBI images has the correct license information in the correct place.
*/
public void test200UbiImagesHaveLicenseDirectory() {
assumeTrue(distribution.packaging == Distribution.Packaging.DOCKER_UBI);
final String[] files = sh.run("find /licenses -type f").stdout.split("\n");
assertThat(files, arrayContaining("/licenses/LICENSE"));
// UBI image doesn't contain `diff`
final String ubiLicense = sh.run("cat /licenses/LICENSE").stdout;
final String distroLicense = sh.run("cat /usr/share/elasticsearch/LICENSE.txt").stdout;
assertThat(ubiLicense, equalTo(distroLicense));
}
/**
* Check that the UBI image has the expected labels
*/
public void test210UbiLabels() throws Exception {
assumeTrue(distribution.packaging == Distribution.Packaging.DOCKER_UBI);
final Map<String, String> labels = getImageLabels(distribution);
final Map<String, String> staticLabels = new HashMap<>();
staticLabels.put("name", "Elasticsearch");
staticLabels.put("maintainer", "infra@elastic.co");
staticLabels.put("vendor", "Elastic");
staticLabels.put("summary", "Elasticsearch");
staticLabels.put("description", "You know, for search.");
final Set<String> dynamicLabels = Set.of("release", "version");
staticLabels.forEach((key, value) -> {
assertThat(labels, hasKey(key));
assertThat(labels.get(key), equalTo(value));
});
dynamicLabels.forEach(key -> assertThat(labels, hasKey(key)));
}
}