Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add systemd integ tests to run with docker #17308

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions qa/systemd-test/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import org.opensearch.gradle.Architecture
import org.opensearch.gradle.VersionProperties
import org.opensearch.gradle.testfixtures.TestFixturesPlugin

apply plugin: 'opensearch.standalone-rest-test'
apply plugin: 'opensearch.test.fixtures'

testFixtures.useFixture()

dockerCompose {
useComposeFiles = ['docker-compose.yml']
}


tasks.register("integTest", Test) {
outputs.doNotCacheIf('Build cache is disabled for Docker tests') { true }
maxParallelForks = '1'
include '**/*IT.class'
}

tasks.named("check").configure { dependsOn "integTest" }

tasks.named("integTest").configure {
dependsOn "composeUp"
finalizedBy "composeDown"
}
64 changes: 64 additions & 0 deletions qa/systemd-test/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
services:
# self-contained systemd example: run 'docker-compose up' to see it
centos:
image: opensearch-systemd-test
container_name: opensearch-systemd-test-container
build:
dockerfile_inline: |
FROM centos:8
RUN sed -i 's|mirrorlist=|#mirrorlist=|g' /etc/yum.repos.d/*.repo && \
sed -i 's|#baseurl=http://mirror.centos.org|baseurl=http://vault.centos.org|g' /etc/yum.repos.d/*.repo

# install systemd
RUN dnf -y install systemd && dnf clean all
# in practice, you'd COPY in the RPM you want to test right here
RUN dnf -y install https://artifacts.opensearch.org/releases/bundle/opensearch/2.18.0/opensearch-2.18.0-linux-x64.rpm && dnf clean all
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the problem with this approach is that we are always testing a fixed & stale version of opensearch and its systemd unit. Unfortunately, I don't have a solution for it. @reta you could something better?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We build RPMs as part of the distributions [1], shouldn't we use those instead?

[1] https://github.com/opensearch-project/OpenSearch/tree/main/distribution/packages/rpm

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks. that solution looks perfect.

question: the RPM is built once for every major/minor version release cycle? or it gets updated as we merge code in main? or a nightly build?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: the RPM is built once for every major/minor version release cycle? or it gets updated as we merge code in main? or a nightly build?

It is part of the build: ./gradlew assemble

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@peterzhuamazon Could you please suggest a way we can resolve this (i.e. taking the latest distribution and not hardcoding it every time a new version is released?)

Copy link
Collaborator

@reta reta Feb 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@RajatGupta02 so we build the RPM as part the Gradle build itself, there should be no need to look for the distribution RPMs anywhere outside the local build itself. These tests could be run as part of distribution builds. Take a look on [1] as an inspiration, adds some tests for archive distribution. We could have a test module for packages as well that will use Docker Compose against RPMs

[1] https://github.com/opensearch-project/OpenSearch/tree/main/distribution/archives/integ-test-zip

Copy link
Contributor Author

@RajatGupta02 RajatGupta02 Feb 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually I had a discussion regarding the same with @peterzhuamazon, since the rpm distribution being locally built with ./gradlew assemble gave errors while installing, to which he explained that there are certain scripts that are added on top of it to make it runnable, and that is done in the opensearch-build repo

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need this scripts? If yes, we have to move these test(s) to opensearch-build and make it part of their workflow

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@RajatGupta02 yeah, moving it to opensearch-build might make more sense, because in the other PR @peterzhuamazon suggests that the systemd configs are eventually moved into opensearch-build, so ideally we should test against that copy of systemd unit.

but let's evaluate how much is the effort for the move, I know you have limited time and you have already spent a significant time on getting this up and ready in core.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will have to sync up with @peterzhuamazon to understand how should we integrate new tests within the opensearch-build repo

# add a test-user
RUN useradd -ms /bin/bash testuser
# no colors
ENV SYSTEMD_COLORS=0
# no escapes
ENV SYSTEMD_URLIFY=0
# explicitly specify docker virtualization
ENV container=docker
# for debugging systemd issues in container, you want this, but it is very loud!
# ENV SYSTEMD_LOG_LEVEL=debug
# plumb journald logs to stdout
COPY <<EOF /etc/systemd/journald.conf
[Journal]
ForwardToConsole=yes
EOF
# start systemd as PID 1
CMD ["usr/sbin/init"]
# enable opensearch service
RUN systemctl enable opensearch
# shutdown systemd properly
STOPSIGNAL SIGRTMIN+3
# disable security plugin, as i don't configure SSL (but could be done with openssl or whatever right here)
RUN echo "plugins.security.disabled: true" >> /etc/opensearch/opensearch.yml
RUN echo "network.host: 0.0.0.0" >> /etc/opensearch/opensearch.yml
RUN echo "discovery.type: single-node" >> /etc/opensearch/opensearch.yml
# provide /dev/console for journal logs to go to stdout
tty: true
# capabilities to allow systemd to sandbox
cap_add:
# https://systemd.io/CONTAINER_INTERFACE/#what-you-shouldnt-do bullet 1
- SYS_ADMIN
# https://systemd.io/CONTAINER_INTERFACE/#what-you-shouldnt-do bullet 2
- MKNOD
# evil, but best you can do on docker? podman is better here.
cgroup: host
volumes:
- /sys/fs/cgroup:/sys/fs/cgroup
# tmpfs mounts for systemd
tmpfs:
- /run
- /run/lock
# health check for opensearch
ports:
- "9200:9200"
- "9300:9300"
privileged: true
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:9200/_cluster/health"]
start_period: 15s
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
/*
* SPDX-License-Identifier: Apache-2.0
*
* The OpenSearch Contributors require contributions made to
* this file be licensed under the Apache-2.0 license or a
* compatible open source license.
*/

/*
* Licensed to Elasticsearch under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/*
* Modifications Copyright OpenSearch Contributors. See
* GitHub history for details.
*/

package org.opensearch.systemdinteg;

import org.apache.hc.core5.http.HttpHeaders;
import org.apache.hc.core5.http.HttpHost;
import org.apache.hc.core5.http.HttpStatus;
import org.junit.After;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.BufferedReader;
import java.net.HttpURLConnection;
import java.net.URL;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;


public class SystemdIT {
private static final String OPENSEARCH_URL = "http://localhost:9200"; // OpenSearch URL (port 9200)
private static String containerId;
private static String opensearchPid;
private static final String CONTAINER_NAME = "opensearch-systemd-test-container";

@BeforeClass
public static void setup() throws IOException, InterruptedException {
containerId = getContainerId();

String status = executeCommand("docker exec " + containerId + " systemctl status opensearch", "Failed to check OpenSearch status");

opensearchPid = getOpenSearchPid();

if (opensearchPid.isEmpty()) {
throw new RuntimeException("Failed to find OpenSearch process ID");
}
}

private static String getContainerId() throws IOException, InterruptedException {
return executeCommand("docker ps -qf name=" + CONTAINER_NAME, "OpenSearch container '" + CONTAINER_NAME + "' is not running");
}

private static String getOpenSearchPid() throws IOException, InterruptedException {
String command = "docker exec " + containerId + " systemctl show --property=MainPID opensearch";
String output = executeCommand(command, "Failed to get OpenSearch PID");
return output.replace("MainPID=", "").trim();
}

private boolean checkPathExists(String path) throws IOException, InterruptedException {
String command = String.format("docker exec %s test -e %s && echo true || echo false", containerId, path);
return Boolean.parseBoolean(executeCommand(command, "Failed to check path existence"));
}

private boolean checkPathReadable(String path) throws IOException, InterruptedException {
String command = String.format("docker exec %s su opensearch -s /bin/sh -c 'test -r %s && echo true || echo false'", containerId, path);
return Boolean.parseBoolean(executeCommand(command, "Failed to check read permission"));
}

private boolean checkPathWritable(String path) throws IOException, InterruptedException {
String command = String.format("docker exec %s su opensearch -s /bin/sh -c 'test -w %s && echo true || echo false'", containerId, path);
return Boolean.parseBoolean(executeCommand(command, "Failed to check write permission"));
}

private String getPathOwnership(String path) throws IOException, InterruptedException {
String command = String.format("docker exec %s stat -c '%%U:%%G' %s", containerId, path);
return executeCommand(command, "Failed to get path ownership");
}

private static String executeCommand(String command, String errorMessage) throws IOException, InterruptedException {
Process process = Runtime.getRuntime().exec(new String[]{"bash", "-c", command});
try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
StringBuilder output = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
output.append(line).append("\n");
}
if (process.waitFor() != 0) {
throw new RuntimeException(errorMessage);
}
return output.toString().trim();
}
}

@Test
public void testClusterHealth() throws IOException {
HttpURLConnection healthCheck = (HttpURLConnection) new URL(OPENSEARCH_URL + "/_cluster/health").openConnection();
healthCheck.setRequestMethod("GET");
int healthResponseCode = healthCheck.getResponseCode();
assertTrue(healthResponseCode == HttpURLConnection.HTTP_OK);
}

@Test
public void testMaxProcesses() throws IOException, InterruptedException {
String limits = executeCommand("docker exec " + containerId + " cat /proc/" + opensearchPid + "/limits", "Failed to read process limits");
assertTrue("Max processes limit should be 4096 or unlimited",
limits.contains("Max processes 4096 4096") ||
limits.contains("Max processes unlimited unlimited"));
}

@Test
public void testFileDescriptorLimit() throws IOException, InterruptedException {
String limits = executeCommand("docker exec " + containerId + " cat /proc/" + opensearchPid + "/limits", "Failed to read process limits");
assertTrue("File descriptor limit should be at least 65535",
limits.contains("Max open files 65535 65535") ||
limits.contains("Max open files unlimited unlimited"));
}

@Test
public void testSystemCallFilter() throws IOException, InterruptedException {
// Check if Seccomp is enabled
String seccomp = executeCommand("docker exec " + containerId + " grep Seccomp /proc/" + opensearchPid + "/status", "Failed to read Seccomp status");
assertFalse("Seccomp should be enabled", seccomp.contains("0"));

// Test specific system calls that should be blocked
String rebootResult = executeCommand("docker exec " + containerId + " su opensearch -c 'kill -s SIGHUP 1' 2>&1 || echo 'Operation not permitted'", "Failed to test reboot system call");
assertTrue("Reboot system call should be blocked", rebootResult.contains("Operation not permitted"));

String swapResult = executeCommand("docker exec " + containerId + " su opensearch -c 'swapon -a' 2>&1 || echo 'Operation not permitted'", "Failed to test swap system call");
assertTrue("Swap system call should be blocked", swapResult.contains("Operation not permitted"));
}

@Test
public void testReadOnlyPaths() throws IOException, InterruptedException {
String[] readOnlyPaths = {
"/etc/os-release", "/usr/lib/os-release", "/etc/system-release",
"/proc/self/mountinfo", "/proc/diskstats",
"/proc/self/cgroup", "/sys/fs/cgroup/cpu", "/sys/fs/cgroup/cpu/-",
"/sys/fs/cgroup/cpuacct", "/sys/fs/cgroup/cpuacct/-",
"/sys/fs/cgroup/memory", "/sys/fs/cgroup/memory/-"
};

for (String path : readOnlyPaths) {
if (checkPathExists(path)) {
assertTrue("Path should be readable: " + path, checkPathReadable(path));
assertFalse("Path should not be writable: " + path, checkPathWritable(path));
}
}
}

@Test
public void testReadWritePaths() throws IOException, InterruptedException {
String[] readWritePaths = {"/var/log/opensearch", "/var/lib/opensearch"};
for (String path : readWritePaths) {
assertTrue("Path should exist: " + path, checkPathExists(path));
assertTrue("Path should be readable: " + path, checkPathReadable(path));
assertTrue("Path should be writable: " + path, checkPathWritable(path));
assertEquals("Path should be owned by opensearch:opensearch", "opensearch:opensearch", getPathOwnership(path));
}
}

@Test
public void testProcessExit() throws IOException, InterruptedException {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: should we name it as testOpensearchProcessCannotExit?


String scriptContent = "#!/bin/sh\n" +
"if [ $# -ne 1 ]; then\n" +
" echo \"Usage: $0 <PID>\"\n" +
" exit 1\n" +
"fi\n" +
"if kill -15 $1 2>/dev/null; then\n" +
" echo \"SIGTERM signal sent to process $1\"\n" +
"else\n" +
" echo \"Failed to send SIGTERM to process $1\"\n" +
"fi\n" +
"sleep 2\n" +
"if kill -0 $1 2>/dev/null; then\n" +
" echo \"Process $1 is still running\"\n" +
"else\n" +
" echo \"Process $1 has terminated\"\n" +
"fi";

String[] command = {
"docker",
"exec",
"-u", "testuser",
containerId,
"sh",
"-c",
"echo '" + scriptContent.replace("'", "'\"'\"'") + "' > /tmp/terminate.sh && chmod +x /tmp/terminate.sh && /tmp/terminate.sh " + opensearchPid
};

ProcessBuilder processBuilder = new ProcessBuilder(command);
Process process = processBuilder.start();

// Wait a moment for any potential termination to take effect
Thread.sleep(2000);

// Check if the OpenSearch process is still running
String processCheck = executeCommand(
"docker exec " + containerId + " kill -0 " + opensearchPid + " 2>/dev/null && echo 'Running' || echo 'Not running'",
"Failed to check process status"
);

// Verify the OpenSearch service status
String serviceStatus = executeCommand(
"docker exec " + containerId + " systemctl is-active opensearch",
"Failed to check OpenSearch service status"
);

assertTrue("OpenSearch process should still be running", processCheck.contains("Running"));
assertEquals("OpenSearch service should be active", "active", serviceStatus.trim());
}

}
Loading