Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import build.base.configuration.Configuration;
import build.base.configuration.ConfigurationBuilder;
import build.base.foundation.Introspection;
import build.base.logging.Logger;
import build.base.naming.UniqueNameGenerator;
import build.base.option.TemporaryDirectory;
import build.base.option.WorkingDirectory;
Expand Down Expand Up @@ -69,6 +70,11 @@
public abstract class AbstractTemplatedLauncher<A extends Application, P extends Platform, N extends Process>
implements TemplatedLauncher<A, P, N> {

/**
* The {@link Logger}.
*/
private static final Logger LOGGER = Logger.get(AbstractTemplatedLauncher.class);

/**
* A {@link UniqueNameGenerator} to generate unique {@link Application} names.
*/
Expand Down Expand Up @@ -295,7 +301,8 @@ public Dependency dependency() {
});
}
catch (final ClassNotFoundException e) {
// TODO: log the fact that the Class can't be found
LOGGER.debug("Could not load class [{0}] for Iterable injection resolution",
namedTypeUsage.typeName().canonicalName(), e);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -267,7 +267,7 @@ public InjectionFramework injectionFramework() {

@Override
public void close() {
// TODO: ensure any asynchronous tasks are shutdown?
this.server.close();
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@
import java.io.InputStream;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
* An {@link InputStreamProcessor} for {@link JsonNode}s.
Expand All @@ -42,6 +44,11 @@
public class JsonNodeInputStreamProcessor
implements InputStreamProcessor<JsonNode> {

/**
* The {@link Logger}.
*/
private static final Logger LOG = Logger.getLogger(JsonNodeInputStreamProcessor.class.getName());

/**
* The {@link JsonFactory} to produce {@link JsonParser}s.
*/
Expand Down Expand Up @@ -101,13 +108,13 @@ public void cancel() {
final JsonNode jsonNode = this.objectMapper.readTree(parser);
subscriber.onNext(jsonNode);
} catch (final Throwable throwable) {
// TODO: log the throwable
LOG.log(Level.FINE, "Failed to read or deliver a JSON node from the stream", throwable);
failed = true;
subscriber.onError(throwable);
}
}
} catch (final Throwable throwable) {
// TODO: log the throwable
LOG.log(Level.FINE, "Failed while processing the JSON input stream", throwable);
if (!failed) {
subscriber.onError(throwable);
}
Expand All @@ -119,7 +126,7 @@ public void cancel() {

inputStream.close();
} catch (final IOException e) {
// TODO: log the throwable
LOG.log(Level.FINE, "Failed to close the JSON input stream", e);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@

import build.base.configuration.Configuration;
import build.spawn.docker.Container;
import build.spawn.docker.option.KillSignal;
import jakarta.inject.Inject;
import okhttp3.HttpUrl;
import okhttp3.Request;
Expand All @@ -45,24 +46,33 @@ public class KillContainer
@Inject
private Container container;

/**
* The {@link Configuration} for the {@link Command}.
*/
private final Configuration configuration;

/**
* Constructs a {@link KillContainer} {@link Command}.
*
* @param configuration the {@link Configuration} to kill the {@link Container}
*/
public KillContainer(final Configuration configuration) {
this.configuration = configuration == null
? Configuration.empty()
: configuration;
}

@Override
protected Request createRequest(final HttpUrl.Builder httpUrlBuilder) {

// TODO: include the SigTerm for stopping
final KillSignal signal = this.configuration.get(KillSignal.class);

return new Request.Builder()
.url(httpUrlBuilder
.addPathSegment("containers")
.addPathSegment(this.container.id())
.addPathSegment("kill")
.addQueryParameter("signal", signal.signalName())
.build())
.post(EMPTY_BODY)
.build();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,9 @@ class CompletableFuturesNamingTests {

/**
* Ensure the {@code CompletableFutures} utility class is spelled correctly.
* <p>
* BUG: The class is currently named {@code CompleteableFutures} (extra {@code e} after
* {@code Complet}) instead of the standard Java spelling {@code CompletableFutures},
* which matches {@link java.util.concurrent.CompletableFuture}. Any caller that attempts
* to import or reference the class by its correct name will receive a compile or runtime
* error.
*/
@Test
void classNameShouldBeSpelledCorrectly() {
// Class.forName will throw ClassNotFoundException because only "CompleteableFutures" exists
assertThatCode(() -> Class.forName("build.spawn.docker.okhttp.CompletableFutures"))
.doesNotThrowAnyException();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
package build.spawn.docker.okhttp.command;

import build.base.configuration.Configuration;
import build.base.io.Terminal;
import build.spawn.docker.Container;
import build.spawn.docker.Executable;
import build.spawn.docker.Image;
import build.spawn.docker.option.Command;
import build.spawn.docker.option.KillSignal;
import okhttp3.HttpUrl;
import org.junit.jupiter.api.Test;

import java.lang.reflect.Field;
import java.nio.file.Path;
import java.util.Map;
import java.util.Optional;
import java.util.OptionalInt;
import java.util.concurrent.CompletableFuture;

import static org.assertj.core.api.Assertions.assertThat;

/**
* Tests for the HTTP request built by {@link KillContainer}.
*
* @author reed.vonredwitz
* @since Apr-2026
*/
class KillContainerRequestTests {

/**
* A minimal {@link Container} stub that returns a fixed id and throws
* {@link UnsupportedOperationException} for all other methods.
*/
private static final class StubContainer
implements Container {

private final String id;

StubContainer(final String id) {
this.id = id;
}

@Override
public String id() {
return this.id;
}

@Override
public Image image() {
throw new UnsupportedOperationException();
}

@Override
public Configuration configuration() {
throw new UnsupportedOperationException();
}

@Override
public CompletableFuture<Container> onStart() {
throw new UnsupportedOperationException();
}

@Override
public CompletableFuture<Container> onExit() {
throw new UnsupportedOperationException();
}

@Override
public OptionalInt exitValue() {
throw new UnsupportedOperationException();
}

@Override
public Terminal attach(final Configuration configuration) {
throw new UnsupportedOperationException();
}

@Override
public Executable createExecutable(final Command command) {
throw new UnsupportedOperationException();
}

@Override
public void remove(final Configuration configuration) {
throw new UnsupportedOperationException();
}

@Override
public void copyFiles(final Path archivePath,
final String destinationDirectory,
final Path... filesToCopy) {
throw new UnsupportedOperationException();
}

@Override
public Optional<Map<String, String>> fileInformation(final Path filePath) {
throw new UnsupportedOperationException();
}

@Override
public void stop(final Configuration configuration) {
throw new UnsupportedOperationException();
}

@Override
public void kill(final Configuration configuration) {
throw new UnsupportedOperationException();
}

@Override
public CompletableFuture<Container> pause() {
throw new UnsupportedOperationException();
}

@Override
public CompletableFuture<Container> unpause() {
throw new UnsupportedOperationException();
}

@Override
public Optional<Information> inspect() {
throw new UnsupportedOperationException();
}
}

/**
* Injects a {@link StubContainer} into the {@link KillContainer}'s {@code container} field.
*
* @param killCommand the command to inject into
* @param containerId the container id the stub should return
* @throws Exception if reflection fails
*/
private static void injectContainer(final KillContainer killCommand, final String containerId)
throws Exception {

final Field field = KillContainer.class.getDeclaredField("container");
field.setAccessible(true);
field.set(killCommand, new StubContainer(containerId));
}

/**
* Ensure the kill request includes a {@code signal} query parameter matching the
* {@link KillSignal} in the configuration.
*/
@Test
void requestShouldIncludeConfiguredSignal() throws Exception {
final var command = new KillContainer(Configuration.of(KillSignal.SIGTERM));

injectContainer(command, "abc123");

final var url = HttpUrl.parse("http://localhost/");
final var request = command.createRequest(url.newBuilder());

assertThat(request.url().queryParameter("signal")).isEqualTo("SIGTERM");
}

/**
* Ensure the default signal is {@code SIGKILL} when no {@link KillSignal} is configured,
* preserving Docker's default kill behaviour.
*/
@Test
void requestShouldDefaultToSigkill() throws Exception {
final var command = new KillContainer(Configuration.empty());

injectContainer(command, "abc123");

final var url = HttpUrl.parse("http://localhost/");
final var request = command.createRequest(url.newBuilder());

assertThat(request.url().queryParameter("signal")).isEqualTo("SIGKILL");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,6 @@ class ContainerInformationTests {
/**
* Ensure that {@link ContainerInformation#name()} returns the plain container name
* without JSON quote characters.
* <p>
* BUG: {@link ContainerInformation#name()} calls {@code jsonNode().get("Name").toString()},
* which returns the JSON representation of the text node (e.g. {@code "\"/my-container\""})
* rather than the raw text value. It should call {@code asText()} instead.
*
* @throws Exception if reflection-based field injection fails
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package build.spawn.docker.option;

/*-
* #%L
* Spawn Docker (Client)
* %%
* Copyright (C) 2026 Workday, Inc.
* %%
* Licensed 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.
* #L%
*/

import build.base.configuration.Default;
import build.base.configuration.Option;

/**
* The POSIX signal sent to a {@link build.spawn.docker.Container} when killing it.
* <p>
* Corresponds to the {@code signal} query parameter of the
* <a href="https://docs.docker.com/engine/api/v1.41/#operation/ContainerKill">Kill Container</a> API.
*
* @author reed.vonredwitz
* @since Apr-2026
*/
public enum KillSignal
implements Option {

/**
* {@code SIGKILL} — immediately terminates the container (Docker's default).
*/
@Default
SIGKILL,

/**
* {@code SIGTERM} — requests graceful termination, allowing the container to clean up.
*/
SIGTERM,

/**
* {@code SIGQUIT} — requests the container to produce a core dump and terminate.
*/
SIGQUIT,

/**
* {@code SIGHUP} — signals the container to reload its configuration.
*/
SIGHUP;

/**
* Returns the signal name as required by the Docker Engine API.
*
* @return the signal name, e.g. {@code "SIGKILL"}
*/
public String signalName() {
return name();
}
}
Loading
Loading