diff --git a/jetty-home/src/main/resources/bin/jetty.sh b/jetty-home/src/main/resources/bin/jetty.sh index bca4af8ea1b4..1aa18617d833 100755 --- a/jetty-home/src/main/resources/bin/jetty.sh +++ b/jetty-home/src/main/resources/bin/jetty.sh @@ -216,6 +216,46 @@ pidKill() fi } +testFileSystemPermissions() +{ + # Don't test file system permissions if user is root + if [ $UID -eq 0 ] ; then + (( DEBUG )) && echo "Not testing file system permissions: uid is 0" + return 0 + fi + + # Don't test if JETTY_USER is specified + # as the Jetty process will switch to a different user id on startup + if [ -n "$JETTY_USER" ] ; then + (( DEBUG )) && echo "Not testing file system permissions: JETTY_USER=$JETTY_USER" + return 0 + fi + + # Don't test if setuid is specified + # as the Jetty process will switch to a different user id on startup + if expr "${JETTY_ARGS[*]}" : '.*setuid.*' >/dev/null + then + (( DEBUG )) && echo "Not testing file system permissions: setuid in use" + return 0 + fi + + # Test if PID can be written from this userid + if ! touch "$JETTY_PID" + then + echo "** ERROR: Unable to touch file: $JETTY_PID" + echo " Correct issues preventing use of \$JETTY_PID and try again." + exit 1 + fi + + # Test if STATE can be written from this userid + if ! touch "$JETTY_STATE" + then + echo "** ERROR: Unable to touch file: $JETTY_STATE" + echo " Correct issues preventing use of \$JETTY_STATE and try again." + exit 1 + fi +} + readConfig() { (( DEBUG )) && echo "Reading $1.." @@ -240,6 +280,10 @@ dumpEnv() echo "JETTY_START_TIMEOUT = $JETTY_START_TIMEOUT" echo "JETTY_SYS_PROPS = $JETTY_SYS_PROPS" echo "RUN_ARGS = ${RUN_ARGS[*]}" + echo "ID = $(id)" + echo "JETTY_USER = $JETTY_USER" + echo "USE_START_STOP_DAEMON = $USE_START_STOP_DAEMON" + echo "START_STOP_DAEMON = $START_STOP_DAEMON_AVAILABLE" } @@ -249,6 +293,7 @@ dumpEnv() CONFIGS=() NO_START=0 DEBUG=0 +USE_START_STOP_DAEMON=1 while [[ $1 = -* ]]; do case $1 in @@ -404,7 +449,6 @@ case "`uname`" in CYGWIN*) JETTY_STATE="`cygpath -w $JETTY_STATE`";; esac - JETTY_ARGS=(${JETTY_ARGS[*]} "jetty.state=$JETTY_STATE" "jetty.pid=$JETTY_PID") ################################################## @@ -412,6 +456,7 @@ JETTY_ARGS=(${JETTY_ARGS[*]} "jetty.state=$JETTY_STATE" "jetty.pid=$JETTY_PID") ################################################## if [ -f "$JETTY_CONF" ] && [ -r "$JETTY_CONF" ] then + (( DEBUG )) && echo "$JETTY_CONF: (begin read) JETTY_ARGS.length=${#JETTY_ARGS[@]}" while read -r CONF do if expr "$CONF" : '#' >/dev/null ; then @@ -427,16 +472,17 @@ then do if [ -r "$XMLFILE" ] && [ -f "$XMLFILE" ] then - JETTY_ARGS=(${JETTY_ARGS[*]} "$XMLFILE") + JETTY_ARGS[${#JETTY_ARGS[@]}]=$XMLFILE else echo "** WARNING: Cannot read '$XMLFILE' specified in '$JETTY_CONF'" fi done else # assume it's a command line parameter (let start.jar deal with its validity) - JETTY_ARGS=(${JETTY_ARGS[*]} "$CONF") + JETTY_ARGS[${#JETTY_ARGS[@]}]=$CONF fi done < "$JETTY_CONF" + (( DEBUG )) && echo "$JETTY_CONF: (finished read) JETTY_ARGS.length=${#JETTY_ARGS[@]}" fi ################################################## @@ -507,8 +553,22 @@ case "`uname`" in CYGWIN*) JETTY_START="`cygpath -w $JETTY_START`";; esac +# Determine if we can use start-stop-daemon or not +START_STOP_DAEMON_AVAILABLE=0 + +if (( USE_START_STOP_DAEMON )) +then + # only if root user is executing jetty.sh, and the start-stop-daemon exists + if [ $UID -eq 0 ] && type start-stop-daemon > /dev/null 2>&1 + then + START_STOP_DAEMON_AVAILABLE=1 + else + USE_START_STOP_DAEMON=0 + fi +fi + # Collect the dry-run (of opts,path,main,args) from the jetty.base configuration -JETTY_DRY_RUN=$("$JAVA" -jar "$JETTY_START" --dry-run=opts,path,main,args ${JETTY_ARGS[*]} ${JAVA_OPTIONS[*]}) +JETTY_DRY_RUN=$(echo "${JETTY_ARGS[*]} ${JAVA_OPTIONS[*]}" | xargs "$JAVA" -jar "$JETTY_START" --dry-run=opts,path,main,args) RUN_ARGS=($JETTY_SYS_PROPS ${JETTY_DRY_RUN[@]}) if (( DEBUG )) @@ -531,24 +591,12 @@ case "$ACTION" in exit fi - if ! touch "$JETTY_PID" - then - echo "** ERROR: Unable to touch file: $JETTY_PID" - echo " Correct issues preventing use of \$JETTY_PID and try again." - exit 1 - fi - - if ! touch "$JETTY_STATE" - then - echo "** ERROR: Unable to touch file: $JETTY_STATE" - echo " Correct issues preventing use of \$JETTY_STATE and try again." - exit 1 - fi + testFileSystemPermissions echo -n "Starting Jetty: " # Startup from a service file - if [ $UID -eq 0 ] && type start-stop-daemon > /dev/null 2>&1 + if (( USE_START_STOP_DAEMON )) then unset CH_USER if [ -n "$JETTY_USER" ] @@ -556,13 +604,14 @@ case "$ACTION" in CH_USER="--chuid $JETTY_USER" fi - echo ${RUN_ARGS[@]} --start-log-file="$JETTY_START_LOG" | xargs start-stop-daemon \ + # use of --pidfile /dev/null disables internal pidfile + # management of the start-stop-daemon (see man page) + echo ${RUN_ARGS[@]} | xargs start-stop-daemon \ --start $CH_USER \ - --pidfile "$JETTY_PID" \ + --pidfile /dev/null \ --chdir "$JETTY_BASE" \ --background \ - --output "${JETTY_RUN}/start-stop.log" - --make-pidfile \ + --output "${JETTY_RUN}/start-stop.log" \ --startas "$JAVA" \ -- (( DEBUG )) && echo "Starting: start-stop-daemon" @@ -618,25 +667,41 @@ case "$ACTION" in stop) echo -n "Stopping Jetty: " - # Stop from a service file - if [ $UID -eq 0 ] && type start-stop-daemon > /dev/null 2>&1; then - start-stop-daemon -K -p"$JETTY_PID" -d"$JETTY_HOME" -a "$JAVA" -s HUP + if [ ! -r "$JETTY_PID" ] ; then + echo "** ERROR: no pid found at $JETTY_PID" + exit 1 + fi + + PID=$(tail -1 "$JETTY_PID") + if [ -z "$PID" ] ; then + echo "** ERROR: no pid found in $JETTY_PID" + exit 1 + fi + + # Stopping service started with start-stop-daemon + if (( USE_START_STOP_DAEMON )) ; then + (( DEBUG )) && echo "Issuing HUP to $PID" + start-stop-daemon --stop \ + --pid "$PID" \ + --chdir "$JETTY_BASE" \ + --startas "$JAVA" \ + --signal HUP TIMEOUT=30 while running "$JETTY_PID"; do + (( DEBUG )) && echo "Issuing KILL to $PID" if (( TIMEOUT-- == 0 )); then - start-stop-daemon -K -p"$JETTY_PID" -d"$JETTY_HOME" -a "$JAVA" -s KILL + start-stop-daemon --stop \ + --pid "$PID" \ + --chdir "$JETTY_BASE" \ + --startas "$JAVA" \ + --signal KILL fi sleep 1 done else - # Stop from a non-service path - if [ ! -r "$JETTY_PID" ] ; then - echo "** ERROR: no pid found at $JETTY_PID" - exit 1 - fi - + # Stopping from non-service start pidKill "$JETTY_PID" 30 fi diff --git a/tests/test-distribution/src/test/java/org/eclipse/jetty/tests/distribution/jettysh/ImageFromDSL.java b/tests/test-distribution/src/test/java/org/eclipse/jetty/tests/distribution/jettysh/ImageFromDSL.java new file mode 100644 index 000000000000..c149ef957535 --- /dev/null +++ b/tests/test-distribution/src/test/java/org/eclipse/jetty/tests/distribution/jettysh/ImageFromDSL.java @@ -0,0 +1,51 @@ +// +// ======================================================================== +// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + +package org.eclipse.jetty.tests.distribution.jettysh; + +import java.util.function.Consumer; + +import org.testcontainers.images.builder.ImageFromDockerfile; +import org.testcontainers.images.builder.dockerfile.DockerfileBuilder; + +/** + * Simplify the use of {@link ImageFromDockerfile} to get some sanity in naming convention + * and {@link #toString()} behaviors so that Test execution makes sense. + */ +public class ImageFromDSL extends ImageFromDockerfile +{ + private ImageFromDSL parentImage; + + public ImageFromDSL(ImageFromDSL baseImage, String suffix, Consumer builderConsumer) + { + this(baseImage.getDockerImageName() + "-" + suffix, builderConsumer); + this.parentImage = baseImage; + } + + public ImageFromDSL(String name, Consumer builderConsumer) + { + super(name, false); + withDockerfileFromBuilder(builderConsumer); + } + + public ImageFromDSL getParentImage() + { + return parentImage; + } + + @Override + public String toString() + { + return getDockerImageName(); + } +} diff --git a/tests/test-distribution/src/test/java/org/eclipse/jetty/tests/distribution/jettysh/ImageOS.java b/tests/test-distribution/src/test/java/org/eclipse/jetty/tests/distribution/jettysh/ImageOS.java new file mode 100644 index 000000000000..1744ded579d5 --- /dev/null +++ b/tests/test-distribution/src/test/java/org/eclipse/jetty/tests/distribution/jettysh/ImageOS.java @@ -0,0 +1,55 @@ +// +// ======================================================================== +// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + +package org.eclipse.jetty.tests.distribution.jettysh; + +import java.io.File; +import java.nio.file.Path; +import java.util.function.Consumer; + +import org.eclipse.jetty.tests.hometester.JettyHomeTester; +import org.testcontainers.images.builder.dockerfile.DockerfileBuilder; + +public abstract class ImageOS extends ImageFromDSL +{ + public static final String REGISTRY = "registry.jetty.org"; + public static final String REPOSITORY = REGISTRY + "/jetty-sh"; + + private Path jettyHomePath; + + public ImageOS(String osid, Consumer builderConsumer) + { + super(REPOSITORY + ":" + osid, builderConsumer); + } + + protected File getJettyHomeDir() + { + if (jettyHomePath == null) + { + String jettyVersion = System.getProperty("jettyVersion"); + try + { + JettyHomeTester homeTester = JettyHomeTester.Builder.newInstance() + .jettyVersion(jettyVersion) + .mavenLocalRepository(System.getProperty("mavenRepoPath")) + .build(); + jettyHomePath = homeTester.getJettyHome(); + } + catch (Exception e) + { + throw new RuntimeException("Unable to get unpacked JETTY_HOME dir", e); + } + } + return jettyHomePath.toFile(); + } +} diff --git a/tests/test-distribution/src/test/java/org/eclipse/jetty/tests/distribution/jettysh/ImageOSAmazonCorretto11.java b/tests/test-distribution/src/test/java/org/eclipse/jetty/tests/distribution/jettysh/ImageOSAmazonCorretto11.java new file mode 100644 index 000000000000..a00483181536 --- /dev/null +++ b/tests/test-distribution/src/test/java/org/eclipse/jetty/tests/distribution/jettysh/ImageOSAmazonCorretto11.java @@ -0,0 +1,50 @@ +// +// ======================================================================== +// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + +package org.eclipse.jetty.tests.distribution.jettysh; + +/** + * An OS Image of linux specific for running on Amazon AWS. + * This is based on Amazon Linux 2 (which is based on Alpine 3). + * Amazon Corretto JDK 11 is installed. + * This image does NOT come with start-stop-daemon installed. + * Instead of apt, it uses yum (the redhat package manager) + */ +public class ImageOSAmazonCorretto11 extends ImageOS +{ + public ImageOSAmazonCorretto11() + { + super("amazoncorretto-jdk11", + builder -> + builder + .from("amazoncorretto:11.0.20") + .run("yum update -y ; " + + "yum install -y curl tar gzip vim shadow-utils net-tools") + .env("TEST_DIR", "/var/test") + .env("JETTY_HOME", "$TEST_DIR/jetty-home") + .env("JETTY_BASE", "$TEST_DIR/jetty-base") + .env("PATH", "$PATH:${JETTY_HOME}/bin/") + .user("root") + // Configure /etc/default/jetty + .run("echo \"JETTY_HOME=${JETTY_HOME}\" > /etc/default/jetty ; " + + "echo \"JETTY_BASE=${JETTY_BASE}\" >> /etc/default/jetty ; " + + "echo \"JETTY_RUN=${JETTY_BASE}\" >> /etc/default/jetty ") + // setup Jetty Home + .copy("/opt/jetty/", "${JETTY_HOME}/") + .env("PATH", "$PATH:${JETTY_HOME}/bin/") + .run("chmod ugo+x ${JETTY_HOME}/bin/jetty.sh") + .build() + ); + withFileFromFile("/opt/jetty/", getJettyHomeDir()); + } +} diff --git a/tests/test-distribution/src/test/java/org/eclipse/jetty/tests/distribution/jettysh/ImageOSUbuntuJammyJDK17.java b/tests/test-distribution/src/test/java/org/eclipse/jetty/tests/distribution/jettysh/ImageOSUbuntuJammyJDK17.java new file mode 100644 index 000000000000..963960b85407 --- /dev/null +++ b/tests/test-distribution/src/test/java/org/eclipse/jetty/tests/distribution/jettysh/ImageOSUbuntuJammyJDK17.java @@ -0,0 +1,50 @@ +// +// ======================================================================== +// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + +package org.eclipse.jetty.tests.distribution.jettysh; + +/** + * An OS Image of Ubuntu Linux 22.04, + * Adding JDK17 toolchain. + * it comes with start-stop-daemon installed + */ +public class ImageOSUbuntuJammyJDK17 extends ImageOS +{ + public ImageOSUbuntuJammyJDK17() + { + super("ubuntu-22.04-jdk17", + builder -> + builder + .from("ubuntu:22.04") + .run("apt update ; " + + "apt -y upgrade ; " + + "apt install -y openjdk-17-jdk-headless ; " + + "apt install -y curl vim net-tools ") + .env("TEST_DIR", "/var/test") + .env("JETTY_HOME", "$TEST_DIR/jetty-home") + .env("JETTY_BASE", "$TEST_DIR/jetty-base") + .env("PATH", "$PATH:${JETTY_HOME}/bin/") + .user("root") + // Configure /etc/default/jetty + .run("echo \"JETTY_HOME=${JETTY_HOME}\" > /etc/default/jetty ; " + + "echo \"JETTY_BASE=${JETTY_BASE}\" >> /etc/default/jetty ; " + + "echo \"JETTY_RUN=${JETTY_BASE}\" >> /etc/default/jetty ") + // setup Jetty Home + .copy("/opt/jetty/", "${JETTY_HOME}/") + .env("PATH", "$PATH:${JETTY_HOME}/bin/") + .run("chmod ugo+x ${JETTY_HOME}/bin/jetty.sh") + .build() + ); + withFileFromFile("/opt/jetty/", getJettyHomeDir()); + } +} diff --git a/tests/test-distribution/src/test/java/org/eclipse/jetty/tests/distribution/jettysh/ImageUserChange.java b/tests/test-distribution/src/test/java/org/eclipse/jetty/tests/distribution/jettysh/ImageUserChange.java new file mode 100644 index 000000000000..eaf3e86066cd --- /dev/null +++ b/tests/test-distribution/src/test/java/org/eclipse/jetty/tests/distribution/jettysh/ImageUserChange.java @@ -0,0 +1,40 @@ +// +// ======================================================================== +// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + +package org.eclipse.jetty.tests.distribution.jettysh; + +/** + * A docker image with JETTY_USER set to id `jetty`. + * JETTY_HOME is owned by `root`. + * JETTY_BASE is owned by `jetty` + */ +public class ImageUserChange extends ImageFromDSL +{ + public ImageUserChange(ImageOS osImage) + { + super(osImage, "user-change", builder -> + builder + .from(osImage.getDockerImageName()) + // setup "jetty" user and Jetty Base directory + .run("chmod ugo+x ${JETTY_HOME}/bin/jetty.sh ; " + + "mkdir -p ${JETTY_BASE} ; " + + "useradd --home-dir=${JETTY_BASE} --shell=/bin/bash jetty ; " + + "chown jetty:jetty ${JETTY_BASE} ; " + + "chmod a+w ${JETTY_BASE} ; " + + "echo \"JETTY_USER=jetty\" > /etc/default/jetty") // user change + .user("jetty") + // Configure Jetty Base + .workDir("${JETTY_BASE}") + .build()); + } +} diff --git a/tests/test-distribution/src/test/java/org/eclipse/jetty/tests/distribution/jettysh/ImageUserRoot.java b/tests/test-distribution/src/test/java/org/eclipse/jetty/tests/distribution/jettysh/ImageUserRoot.java new file mode 100644 index 000000000000..fd9c81c3c068 --- /dev/null +++ b/tests/test-distribution/src/test/java/org/eclipse/jetty/tests/distribution/jettysh/ImageUserRoot.java @@ -0,0 +1,33 @@ +// +// ======================================================================== +// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + +package org.eclipse.jetty.tests.distribution.jettysh; + +/** + * A docker image with no JETTY_USER set, everything executes as `root`. + */ +public class ImageUserRoot extends ImageFromDSL +{ + public ImageUserRoot(ImageOS osImage) + { + super(osImage, "user-root", builder -> + builder + .from(osImage.getDockerImageName()) + .run("mkdir -p ${JETTY_BASE} ; " + + "chmod u+x ${JETTY_HOME}/bin/jetty.sh ; " + + "chmod a+w ${JETTY_BASE}") + // Configure Jetty Base + .workDir("${JETTY_BASE}") + .build()); + } +} diff --git a/tests/test-distribution/src/test/java/org/eclipse/jetty/tests/distribution/jettysh/JettyShStartTest.java b/tests/test-distribution/src/test/java/org/eclipse/jetty/tests/distribution/jettysh/JettyShStartTest.java new file mode 100644 index 000000000000..7172aec203d1 --- /dev/null +++ b/tests/test-distribution/src/test/java/org/eclipse/jetty/tests/distribution/jettysh/JettyShStartTest.java @@ -0,0 +1,221 @@ +// +// ======================================================================== +// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + +package org.eclipse.jetty.tests.distribution.jettysh; + +import java.net.URI; +import java.nio.channels.AsynchronousCloseException; +import java.nio.file.Path; +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import java.util.Stack; +import java.util.concurrent.ExecutionException; +import java.util.stream.Stream; + +import org.awaitility.Awaitility; +import org.eclipse.jetty.client.api.ContentResponse; +import org.eclipse.jetty.http.HttpStatus; +import org.eclipse.jetty.tests.distribution.AbstractJettyHomeTest; +import org.eclipse.jetty.toolchain.test.MavenPaths; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.testcontainers.containers.Container; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.startupcheck.IsRunningStartupCheckStrategy; +import org.testcontainers.containers.wait.strategy.ShellStrategy; +import org.testcontainers.images.PullPolicy; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.allOf; +import static org.hamcrest.Matchers.anyOf; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.matchesRegex; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +/** + * Test of jetty-home/bin/jetty.sh as generic start mechanism. + */ +public class JettyShStartTest extends AbstractJettyHomeTest +{ + private static final Logger LOG = LoggerFactory.getLogger(JettyShStartTest.class); + + public static Stream jettyImages() + { + List images = new ArrayList<>(); + + // Loop through all OS images + for (ImageOS osImage : List.of(new ImageOSUbuntuJammyJDK17(), new ImageOSAmazonCorretto11())) + { + // Establish user Images based on OS Image + List userImages = new ArrayList<>(); + userImages.add(new ImageUserRoot(osImage)); + userImages.add(new ImageUserChange(osImage)); + + // Loop through user Images to establish various JETTY_BASE configurations + for (ImageFromDSL userImage : userImages) + { + // Basic JETTY_BASE + images.add(new ImageFromDSL(userImage, "base-basic", builder -> + builder + .from(userImage.getDockerImageName()) + // Create a basic configuration of jetty-base + .run("java -jar ${JETTY_HOME}/start.jar --add-modules=http,deploy") + .build())); + + // Complex JETTY_BASE with spaces + Path basesWithSpaces = MavenPaths.findTestResourceDir("bases/spaces-with-conf"); + ImageFromDSL baseComplexWithSpacesImage = new ImageFromDSL(userImage, "base-complex-with-spaces", builder -> + { + builder + .from(userImage.getDockerImageName()) + // Create a basic configuration of jetty-base + .run("java -jar ${JETTY_HOME}/start.jar --add-modules=http,deploy") + .copy("/tests/bases-with-spaces/", "${JETTY_BASE}/"); + if (userImage instanceof ImageUserChange) + { + // Make sure we change the ownership of JETTY_BASE if we are testing a user change mode + builder.user("root") + .run("chown -R jetty:jetty $JETTY_BASE") + .user("jetty"); + } + builder.build(); + }); + baseComplexWithSpacesImage.withFileFromFile("/tests/bases-with-spaces/", basesWithSpaces.toFile()); + images.add(baseComplexWithSpacesImage); + } + } + + return images.stream().map(Arguments::of); + } + + @ParameterizedTest + @MethodSource("jettyImages") + public void testStartStopJettyBase(ImageFromDSL jettyImage) throws Exception + { + ensureParentImagesExist(jettyImage); + + try (GenericContainer genericContainer = new GenericContainer<>(jettyImage)) + { + genericContainer.withImagePullPolicy(PullPolicy.defaultPolicy()); + genericContainer.setWaitStrategy(new ShellStrategy().withCommand("id")); + + genericContainer.withExposedPorts(80, 8080) // jetty + .withCommand("/bin/sh", "-c", "while true; do pwd | nc -l -p 80; done") + .withStartupAttempts(2) + .withStartupCheckStrategy(new IsRunningStartupCheckStrategy()) + .start(); + + LOG.info("Started: " + jettyImage.getDockerImageName()); + + System.err.println("== jetty.sh start =="); + Container.ExecResult result = genericContainer.execInContainer("/var/test/jetty-home/bin/jetty.sh", "start"); + assertThat(result.getExitCode(), is(0)); + /* + * Example successful output + * ---- + * STDOUT: + * Starting Jetty: . started + * OK Wed Oct 18 19:29:35 UTC 2023 + * ---- + */ + Awaitility.await().atMost(Duration.ofSeconds(5)).until(result::getStdout, + allOf( + containsString("Starting Jetty:"), + containsString("\nOK ") + )); + + startHttpClient(); + + URI containerUriRoot = URI.create("http://" + genericContainer.getHost() + ":" + genericContainer.getMappedPort(8080) + "/"); + LOG.debug("Container URI Root: {}", containerUriRoot); + + System.err.println("== Attempt GET request to service =="); + ContentResponse response = client.GET(containerUriRoot); + assertEquals(HttpStatus.NOT_FOUND_404, response.getStatus(), new ResponseDetails(response)); + assertThat(response.getContentAsString(), containsString("Powered by Eclipse Jetty:// Server")); + + System.err.println("== jetty.sh status (should be running) =="); + result = genericContainer.execInContainer("/var/test/jetty-home/bin/jetty.sh", "status"); + assertThat(result.getExitCode(), is(0)); + Awaitility.await().atMost(Duration.ofSeconds(5)).until(result::getStdout, + containsString("Jetty running pid")); + + System.err.println("== jetty.sh stop =="); + result = genericContainer.execInContainer("/var/test/jetty-home/bin/jetty.sh", "stop"); + assertThat(result.getExitCode(), is(0)); + /* Looking for output from jetty.sh indicating a stopped jetty. + * STDOUT Example 1 + * ---- + * Stopping Jetty: OK\n + * ---- + * STOUT Example 2 + * ---- + * Stopping Jetty: .Killed 12345\n + * OK\n + * ---- + */ + Awaitility.await().atMost(Duration.ofSeconds(5)).until(result::getStdout, + matchesRegex("Stopping Jetty: .*[\n]?OK[\n]")); + + System.err.println("== jetty.sh status (should be stopped) =="); + result = genericContainer.execInContainer("/var/test/jetty-home/bin/jetty.sh", "status"); + assertThat(result.getExitCode(), is(1)); + Awaitility.await().atMost(Duration.ofSeconds(5)).until(result::getStdout, + containsString("Jetty NOT running")); + + System.err.println("== Attempt GET request to non-existent service =="); + client.setConnectTimeout(1000); + Exception failedGetException = assertThrows(Exception.class, () -> client.GET(containerUriRoot)); + // GET failure can result in either exception below (which one is based on timing / race) + assertThat(failedGetException, anyOf( + instanceOf(ExecutionException.class), + instanceOf(AsynchronousCloseException.class)) + ); + } + } + + private void ensureParentImagesExist(ImageFromDSL jettyImage) + { + // The build stack for images + Stack images = new Stack<>(); + + ImageFromDSL parent = jettyImage; + while ((parent = parent.getParentImage()) != null) + { + images.push(parent); + } + + // Create the images (allowing testcontainers cache to do its thing) + while (!images.isEmpty()) + { + ImageFromDSL image = images.pop(); + createImage(image); + } + } + + private void createImage(ImageFromDSL image) + { + LOG.debug("Create Image: {}", image.getDockerImageName()); + try (GenericContainer container = new GenericContainer<>(image)) + { + container.start(); + } + } +} diff --git a/tests/test-distribution/src/test/resources/bases/spaces-with-conf/dir one/test1.xml b/tests/test-distribution/src/test/resources/bases/spaces-with-conf/dir one/test1.xml new file mode 100644 index 000000000000..b16b24aae946 --- /dev/null +++ b/tests/test-distribution/src/test/resources/bases/spaces-with-conf/dir one/test1.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/tests/test-distribution/src/test/resources/bases/spaces-with-conf/dir two/test2.xml b/tests/test-distribution/src/test/resources/bases/spaces-with-conf/dir two/test2.xml new file mode 100644 index 000000000000..b16b24aae946 --- /dev/null +++ b/tests/test-distribution/src/test/resources/bases/spaces-with-conf/dir two/test2.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/tests/test-distribution/src/test/resources/bases/spaces-with-conf/etc/jetty.conf b/tests/test-distribution/src/test/resources/bases/spaces-with-conf/etc/jetty.conf new file mode 100644 index 000000000000..365800cdc6cc --- /dev/null +++ b/tests/test-distribution/src/test/resources/bases/spaces-with-conf/etc/jetty.conf @@ -0,0 +1,4 @@ +# test configurations with spaces +'dir one/test1.xml' +'dir two/test2.xml' +--module=state,pid diff --git a/tests/test-distribution/src/test/resources/bases/spaces-with-conf/logs/.gitignore b/tests/test-distribution/src/test/resources/bases/spaces-with-conf/logs/.gitignore new file mode 100644 index 000000000000..bf0824e596ed --- /dev/null +++ b/tests/test-distribution/src/test/resources/bases/spaces-with-conf/logs/.gitignore @@ -0,0 +1 @@ +*.log \ No newline at end of file diff --git a/tests/test-distribution/src/test/resources/bases/spaces-with-conf/modules/test.mod b/tests/test-distribution/src/test/resources/bases/spaces-with-conf/modules/test.mod new file mode 100644 index 000000000000..3c36a1e656a5 --- /dev/null +++ b/tests/test-distribution/src/test/resources/bases/spaces-with-conf/modules/test.mod @@ -0,0 +1,5 @@ +[exec] +-showversion +-XX:+PrintCommandLineFlags +-Xlog:gc*:file=/var/test/jetty-base/logs/gc.log:time,level,tags +-XX:ErrorFile=/var/test/jetty-base/logs/jvm_crash_pid_%p.log \ No newline at end of file diff --git a/tests/test-distribution/src/test/resources/bases/spaces-with-conf/start.d/test.ini b/tests/test-distribution/src/test/resources/bases/spaces-with-conf/start.d/test.ini new file mode 100644 index 000000000000..066141861728 --- /dev/null +++ b/tests/test-distribution/src/test/resources/bases/spaces-with-conf/start.d/test.ini @@ -0,0 +1 @@ +--module=test \ No newline at end of file diff --git a/tests/test-distribution/src/test/resources/jetty-logging.properties b/tests/test-distribution/src/test/resources/jetty-logging.properties index ef1afe439643..4b707608b275 100644 --- a/tests/test-distribution/src/test/resources/jetty-logging.properties +++ b/tests/test-distribution/src/test/resources/jetty-logging.properties @@ -1,4 +1,8 @@ # Jetty Logging using jetty-slf4j-impl org.eclipse.jetty.logging.appender.MESSAGE_ESCAPE=false +org.eclipse.jetty.logging.appender.NAME_CONDENSE=false +org.eclipse.jetty.LEVEL=INFO +org.testcontainers.LEVEL=INFO +#org.testcontainers.LEVEL=DEBUG #org.eclipse.jetty.LEVEL=DEBUG #org.eclipse.jetty.tests.distribution.LEVEL=DEBUG