diff --git a/CLAUDE.md b/CLAUDE.md index 1bb797d..a38d6a1 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,7 +4,7 @@ Spawn is a Java 25 framework for programmatically launching and controlling processes, JVMs, and Docker containers. It provides a unified abstraction (`Platform` / `Application` / `Process`) over different execution environments. The core pattern: define a `Specification`, call `platform.launch(spec)`, get back an `Application` with `CompletableFuture`-based lifecycle hooks. -**Stack**: Java 25, Maven, OkHttp3, Jackson, junixsocket, proprietary `build.base.*` and `build.typesystem.injection` libraries from Workday Artifactory +**Stack**: Java 25, Maven, Jackson, junixsocket, proprietary `build.base.*` and `build.codemodel.injection` **Structure**: 8 Maven modules in a monorepo, each mapping to a JPMS module: - `spawn-option` → shared option types @@ -14,7 +14,7 @@ Spawn is a Java 25 framework for programmatically launching and controlling proc - `spawn-local-platform` → local OS process launcher (`LocalMachine`) - `spawn-local-jdk` → JDK detection + `LocalJDKLauncher` - `spawn-docker` → Docker Engine API interfaces -- `spawn-docker-okhttp` → OkHttp-based Docker implementation (slated for replacement with Java HTTP Client) +- `spawn-docker-jdk` → JDK HTTP Client-based Docker implementation (uses `java.net.http` + junixsocket) For detailed architecture, see [docs/CODEBASE_MAP.md](docs/CODEBASE_MAP.md). @@ -22,10 +22,10 @@ For detailed architecture, see [docs/CODEBASE_MAP.md](docs/CODEBASE_MAP.md). ```bash ./mvnw clean install # build all modules + run tests -./mvnw clean install -pl spawn-docker-okhttp # build specific module +./mvnw clean install -pl spawn-docker-jdk # build specific module ``` -Tests requiring Docker are gated by `@EnabledIf("isDockerAvailable")`. The `spawn-docker-okhttp` module requires `--enable-native-access=ALL-UNNAMED` (configured in surefire). +Tests requiring Docker are gated by `@EnabledIf("isDockerAvailable")`. The `spawn-docker-jdk` module requires `--enable-native-access=ALL-UNNAMED` (configured in surefire). ## Key Conventions diff --git a/docs/CODEBASE_MAP.md b/docs/CODEBASE_MAP.md index 1c6c6f6..84d7688 100644 --- a/docs/CODEBASE_MAP.md +++ b/docs/CODEBASE_MAP.md @@ -61,7 +61,7 @@ graph TB DockerOptions["Docker option types"] end - subgraph DockerImpl["spawn-docker-okhttp (OkHttp impl)"] + subgraph DockerImpl["spawn-docker-jdk (JDK impl)"] AbstractSession SessionFactories["Session.Factory impls"] Commands["Command classes"] @@ -115,7 +115,7 @@ spawn.build/ ├── spawn-local-platform/ # JPMS: build.spawn.platform.local — OS process launcher ├── spawn-local-jdk/ # JPMS: build.spawn.platform.local.jdk — JDK detection ├── spawn-docker/ # JPMS: build.spawn.docker — Docker Engine API interfaces -├── spawn-docker-okhttp/ # JPMS: build.spawn.docker.okhttp — OkHttp implementation +├── spawn-docker-jdk/ # JPMS: build.spawn.docker.jdk — JDK HTTP Client implementation └── pom.xml # Parent POM; manages versions, Checkstyle, Surefire ``` @@ -335,9 +335,9 @@ Server (listens on spawn:// URI) --- -### `spawn-docker-okhttp` +### `spawn-docker-jdk` -**Purpose:** OkHttp-based concrete implementation of `spawn-docker` interfaces. +**Purpose:** JDK-native concrete implementation of `spawn-docker` interfaces. Uses `java.net.http.HttpClient` for TCP and `java.nio.channels.SocketChannel` with `UnixDomainSocketAddress` for Unix domain sockets. No third-party HTTP dependencies. **Entry point:** Four `Session.Factory` implementations discovered via `ServiceLoader`, in priority order: 1. `UnixDomainSocketBasedSession.Factory` — Unix socket (`/var/run/docker.sock` or Docker Desktop socket) 2. `LocalHostBasedSessionFactory` — TCP `localhost:2375` @@ -347,28 +347,25 @@ Server (listens on spawn:// URI) **Key files:** | File | Purpose | |------|---------| -| `AbstractSession.java` | OkHttp client + DI context + event streaming; self-implements `Images`; `Authenticate` on construction | -| `TCPSocketBasedSession.java` | TCP variant; configures OkHttp timeouts; no connection pooling | -| `UnixDomainSocketBasedSession.java` | Unix domain socket via junixsocket; `ConnectedDomainSocket` adapter for OkHttp | -| `command/AbstractCommand.java` | Template method for OkHttp request/response lifecycle | -| `command/AbstractBlockingCommand.java` | Short-lived synchronous commands; infinite timeout | +| `HttpTransport.java` | Thin transport interface; `Request` record + `Response` interface | +| `Http11Parser.java` | HTTP/1.1 response parser for raw socket streams (Content-Length + chunked) | +| `JavaHttpClientTransport.java` | `HttpTransport` impl using `java.net.http.HttpClient` for TCP | +| `UnixSocketHttpTransport.java` | `HttpTransport` impl using `UnixDomainSocketAddress` + `Http11Parser` | +| `AbstractSession.java` | `HttpTransport` + DI context + event streaming; self-implements `Images`; `Authenticate` on construction | +| `TCPSocketBasedSession.java` | TCP variant using `JavaHttpClientTransport` | +| `UnixDomainSocketBasedSession.java` | Unix domain socket variant using `UnixSocketHttpTransport` | +| `command/AbstractCommand.java` | Template method for `HttpTransport` request/response lifecycle | +| `command/AbstractBlockingCommand.java` | Short-lived synchronous commands | | `command/AbstractNonBlockingCommand.java` | Streaming commands (events, attach); keeps response open | | `command/AbstractEventBasedBlockingCommand.java` | Subscribes to events before sending request; used by `PullImage` | | `command/FrameProcessor.java` | Demultiplexes Docker's 8-byte-header binary stream protocol | -| `event/GetSystemEvents.java` | Streaming JSON parser; publishes `StatusEvent`; virtual thread | -| `event/StatusEvent.java` | Docker event with `"status"` field | -| `model/OkHttpBasedContainer.java` | Full `Container` impl; `@PostInject` wires event subscription for `onStart/onExit` | -| `model/OkHttpBasedImage.java` | `Image` impl; `start()` creates then starts container; auto-removes on start failure | +| `event/GetSystemEvents.java` | Streaming JSON parser; publishes `ActionEvent`; virtual thread | +| `model/DockerContainer.java` | Full `Container` impl; `@PostInject` wires event subscription for `onStart/onExit` | +| `model/DockerImage.java` | `Image` impl; `start()` creates then starts container; auto-removes on start failure | | `model/AbstractJsonBasedResult.java` | DI-injected `Session`, `JsonNode`, `ObjectMapper` for all model classes | -**Why slated for replacement with Java HTTP Client:** -- OkHttp 5.x (Kotlin) pulls in `kotlin.stdlib` as runtime dependency -- Unix domain socket support requires third-party `junixsocket` with native binaries -- Java 16+ has `java.net.UnixDomainSocketAddress`; Java 11+ has `java.net.http.HttpClient` -- The `ConnectedDomainSocket` adapter (200+ LOC boilerplate) would be eliminated entirely - -**Known bugs in `spawn-docker-okhttp`:** -- `GetSystemEvents` and `OkHttpBasedContainer` have debug `System.out.println` calls in production code +**Known bugs:** +- `GetSystemEvents` and `DockerContainer` have debug `System.out.println` calls in production code - `CopyFiles` constructor validation inverts the check (throws when file has content instead of when it's empty) - `NetworkInformation.driver()` reads lowercase `"driver"` but Docker API returns `"Driver"` → always returns empty string - `ContainerInformation.links()`: splits on `:` — `ArrayIndexOutOfBoundsException` if link string has no colon diff --git a/pom.xml b/pom.xml index 635c57e..5899894 100644 --- a/pom.xml +++ b/pom.xml @@ -73,10 +73,8 @@ 1.18.4 2.21.2 6.0.3 - 2.10.1 2.0.1 5.23.0 - 4.12.0 0.19.0 @@ -107,7 +105,7 @@ spawn-option spawn-docker - spawn-docker-okhttp + spawn-docker-jdk diff --git a/spawn-docker-okhttp/pom.xml b/spawn-docker-jdk/pom.xml similarity index 83% rename from spawn-docker-okhttp/pom.xml rename to spawn-docker-jdk/pom.xml index 068507a..4656261 100644 --- a/spawn-docker-okhttp/pom.xml +++ b/spawn-docker-jdk/pom.xml @@ -11,9 +11,9 @@ ${revision} - spawn-docker-okhttp + spawn-docker-jdk - Spawn Docker (OkHttp Client) + Spawn Docker (JDK Client) @@ -76,23 +76,6 @@ ${codemodel.version} - - com.kohlschutter.junixsocket - junixsocket-common - ${junixsocket.version} - - - - com.kohlschutter.junixsocket - junixsocket-native-common - ${junixsocket.version} - - - - com.squareup.okhttp3 - okhttp - ${okhttp.version} - jakarta.inject @@ -154,18 +137,24 @@ maven-surefire-plugin ${maven-surefire-plugin.version} - --enable-native-access=ALL-UNNAMED + --add-modules jdk.httpserver --add-reads build.spawn.docker.jdk=jdk.httpserver true false + + org.apache.maven.plugins + maven-compiler-plugin + + + --add-reads=build.spawn.docker.jdk=jdk.httpserver + + + org.apache.maven.plugins maven-dependency-plugin - - com.kohlschutter.junixsocket:junixsocket-native-common - build.codemodel:codemodel-foundation build.codemodel:jdk-codemodel diff --git a/spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/AbstractSession.java b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/AbstractSession.java similarity index 75% rename from spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/AbstractSession.java rename to spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/AbstractSession.java index b534766..28b1704 100644 --- a/spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/AbstractSession.java +++ b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/AbstractSession.java @@ -1,8 +1,8 @@ -package build.spawn.docker.okhttp; +package build.spawn.docker.jdk; /*- * #%L - * Spawn Docker (OkHttp Client) + * Spawn Docker (JDK Client) * %% * Copyright (C) 2026 Workday, Inc. * %% @@ -37,32 +37,27 @@ import build.spawn.docker.Network; import build.spawn.docker.Networks; import build.spawn.docker.Session; -import build.spawn.docker.okhttp.command.Authenticate; -import build.spawn.docker.okhttp.command.BuildImage; -import build.spawn.docker.okhttp.command.Command; -import build.spawn.docker.okhttp.command.CreateNetwork; -import build.spawn.docker.okhttp.command.DeleteNetwork; -import build.spawn.docker.okhttp.command.GetSystemEvents; -import build.spawn.docker.okhttp.command.GetSystemInformation; -import build.spawn.docker.okhttp.command.InspectImage; -import build.spawn.docker.okhttp.command.InspectNetwork; -import build.spawn.docker.okhttp.command.PullImage; -import build.spawn.docker.okhttp.model.OkHttpBasedImage; +import build.spawn.docker.jdk.command.Authenticate; +import build.spawn.docker.jdk.command.BuildImage; +import build.spawn.docker.jdk.command.Command; +import build.spawn.docker.jdk.command.CreateNetwork; +import build.spawn.docker.jdk.command.DeleteNetwork; +import build.spawn.docker.jdk.command.GetSystemEvents; +import build.spawn.docker.jdk.command.GetSystemInformation; +import build.spawn.docker.jdk.command.InspectImage; +import build.spawn.docker.jdk.command.InspectNetwork; +import build.spawn.docker.jdk.command.PullImage; +import build.spawn.docker.jdk.model.DockerImage; import build.spawn.docker.option.DockerAPIVersion; import build.spawn.docker.option.DockerRegistry; import build.spawn.docker.option.IdentityToken; import com.fasterxml.jackson.databind.ObjectMapper; -import okhttp3.HttpUrl; -import okhttp3.OkHttpClient; import java.nio.charset.StandardCharsets; import java.nio.file.Path; import java.util.Base64; import java.util.Objects; import java.util.Optional; -import java.util.function.Supplier; -import java.util.logging.Level; -import java.util.logging.Logger; /** * An abstract {@link Session} implementation. @@ -79,9 +74,9 @@ public class AbstractSession private final Configuration configuration; /** - * The {@link OkHttpClient}. + * The {@link HttpTransport} for communicating with the Docker Engine. */ - private final OkHttpClient httpClient; + private final HttpTransport transport; /** * The dependency injection {@link Context} to use for creating {@link Command}s. @@ -115,38 +110,21 @@ public class AbstractSession private final GetSystemEvents systemEvents; /** - * Constructs an {@link AbstractSession} using the specified {@link OkHttpClient} and {@link HttpUrl.Builder} - * {@link Supplier}s and provided {@link Configuration}. - *

- * The {@link OkHttpClient} {@link Supplier} is used to obtain {@link OkHttpClient}s when required by a - * {@link Session} and more specifically, when {@link Command}s for the {@link Session} need to be - * {@link Command#submit()}ted for execution. {@link Session}s are free to cache and reuse {@link OkHttpClient}s - * for {@link Command}s, so it should not be assumed that a new {@link OkHttpClient} is created per {@link Command} - * or a single {@link OkHttpClient} is used for all {@link Command}s. - *

- * The {@link HttpUrl.Builder} {@link Supplier} is used to obtain base {@link HttpUrl}s from which {@link Command}s - * may be constructed for submission with a {@link OkHttpClient}. Each {@link Command} is guaranteed to be - * provided (usually injected) with a new {@link HttpUrl.Builder}, thus allowing per-{@link Command} customization. + * Constructs an {@link AbstractSession} using the specified {@link HttpTransport} and {@link Configuration}. * - * @param injectionFramework the {@link InjectionFramework} to use for {@link build.codemodel.injection.Dependency} injection - * @param clientSupplier the {@link OkHttpClient} {@link Supplier} - * @param httpUrlBuilderSupplier the {@link HttpUrl.Builder} {@link Supplier} - * @param configuration the {@link Configuration} + * @param injectionFramework the {@link InjectionFramework} to use for {@link build.codemodel.injection.Dependency} injection + * @param transport the {@link HttpTransport} for communicating with the Docker Engine + * @param configuration the {@link Configuration} */ @SuppressWarnings("unchecked") protected AbstractSession(final InjectionFramework injectionFramework, - final Supplier clientSupplier, - final Supplier httpUrlBuilderSupplier, + final HttpTransport transport, final Configuration configuration) { Objects.requireNonNull(injectionFramework, "The InjectionFramework must not be null"); - Objects.requireNonNull(clientSupplier, "The Supplier must not be null"); - Objects.requireNonNull(httpUrlBuilderSupplier, "The Supplier must not be null"); + Objects.requireNonNull(transport, "The HttpTransport must not be null"); - Logger.getLogger(OkHttpClient.class.getName()).setLevel(Level.FINE); - - // establish the HttpClient from the Supplier (allowing us to connect to the Docker Engine) - this.httpClient = clientSupplier.get(); + this.transport = transport; this.configuration = configuration == null ? Configuration.empty() @@ -173,8 +151,7 @@ protected AbstractSession(final InjectionFramework injectionFramework, this.context.addResolver(ConfigurationResolver.of(configuration)); - this.context.bind(OkHttpClient.class).to(this.httpClient); - this.context.bind(HttpUrl.Builder.class).to(httpUrlBuilderSupplier); + this.context.bind(HttpTransport.class).to(this.transport); this.context.bind(Session.class).to(this); this.context.bind(AbstractSession.class).to(this); this.context.bind((Class) getClass()).to(this); @@ -249,7 +226,7 @@ protected AbstractSession(final InjectionFramework injectionFramework, .getEncoder() .encode(json.getBytes(StandardCharsets.UTF_8))); - return (Authenticator) builder -> builder.addHeader("X-Registry-Auth", encoded); + return (Authenticator) builder -> builder.withHeader("X-Registry-Auth", encoded); }) .orElse(Authenticator.NONE)); @@ -316,7 +293,7 @@ public Optional get(final String nameOrId, final Configuration configurat .inject(new InspectImage(nameOrId, configuration)) .submit() .map(info -> createContext() - .inject(new OkHttpBasedImage(info.imageId()))); + .inject(new DockerImage(info.imageId()))); } @Override @@ -351,16 +328,7 @@ public void close() { // close the reading of System Events this.systemEvents.close(); - // the OkHttpClient doesn't require shutdown to clean up - // but to stop accepting requests / responses we need to stop the internal executor service - this.httpClient - .dispatcher() - .executorService() - .shutdownNow(); - - this.httpClient - .connectionPool() - .evictAll(); + // HttpTransport implementations manage their own connection lifecycle } class NetworksImpl diff --git a/spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/Authenticator.java b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/Authenticator.java similarity index 71% rename from spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/Authenticator.java rename to spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/Authenticator.java index 73ebec5..b6ffba5 100644 --- a/spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/Authenticator.java +++ b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/Authenticator.java @@ -1,17 +1,17 @@ -package build.spawn.docker.okhttp; +package build.spawn.docker.jdk; /*- * #%L - * Spawn Docker (OkHttp Client) + * Spawn Docker (JDK 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. @@ -20,21 +20,19 @@ * #L% */ -import okhttp3.Request; - -import java.util.function.Function; +import java.util.function.UnaryOperator; /** - * A {@link Function} to configure authentication for a {@link Request.Builder}. + * A {@link UnaryOperator} to configure authentication on an {@link HttpTransport.Request}. * * @author brian.oliver * @since Aug-2022 */ public interface Authenticator - extends Function { + extends UnaryOperator { /** * An {@link Authenticator} that performs no authentication. */ - Authenticator NONE = builder -> builder; + Authenticator NONE = request -> request; } diff --git a/spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/CompletableFutures.java b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/CompletableFutures.java similarity index 95% rename from spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/CompletableFutures.java rename to spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/CompletableFutures.java index e48c93e..e674105 100644 --- a/spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/CompletableFutures.java +++ b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/CompletableFutures.java @@ -1,8 +1,8 @@ -package build.spawn.docker.okhttp; +package build.spawn.docker.jdk; /*- * #%L - * Spawn Docker (OkHttp Client) + * Spawn Docker (JDK Client) * %% * Copyright (C) 2026 Workday, Inc. * %% diff --git a/spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/DockerHostVariableBasedSessionFactory.java b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/DockerHostVariableBasedSessionFactory.java similarity index 98% rename from spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/DockerHostVariableBasedSessionFactory.java rename to spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/DockerHostVariableBasedSessionFactory.java index 6448287..614f521 100644 --- a/spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/DockerHostVariableBasedSessionFactory.java +++ b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/DockerHostVariableBasedSessionFactory.java @@ -1,8 +1,8 @@ -package build.spawn.docker.okhttp; +package build.spawn.docker.jdk; /*- * #%L - * Spawn Docker (OkHttp Client) + * Spawn Docker (JDK Client) * %% * Copyright (C) 2026 Workday, Inc. * %% diff --git a/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/Http11Parser.java b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/Http11Parser.java new file mode 100644 index 0000000..9d9e8f9 --- /dev/null +++ b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/Http11Parser.java @@ -0,0 +1,258 @@ +package build.spawn.docker.jdk; + +/*- + * #%L + * Spawn Docker (JDK 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 java.io.IOException; +import java.io.InputStream; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * Parses HTTP/1.1 responses from an {@link InputStream}, handling {@code Content-Length} and + * {@code Transfer-Encoding: chunked} bodies. + * + * @author brian.oliver + * @since Apr-2026 + */ +class Http11Parser { + + private Http11Parser() { + } + + /** + * Parses an HTTP/1.1 response from the specified {@link InputStream}. + * + * @param in the {@link InputStream} to read from + * @param connection a {@link AutoCloseable} representing the underlying connection, closed on {@link HttpTransport.Response#close()} + * @return the parsed {@link HttpTransport.Response} + * @throws IOException when the response cannot be parsed + */ + static HttpTransport.Response parse(final InputStream in, + final AutoCloseable connection) throws IOException { + + // parse the status line: "HTTP/1.1 200 OK" + final var statusLine = readLine(in); + final var parts = statusLine.split(" ", 3); + final var statusCode = Integer.parseInt(parts[1]); + + // parse the headers + final Map headers = new LinkedHashMap<>(); + String headerLine; + while (!(headerLine = readLine(in)).isEmpty()) { + final var colon = headerLine.indexOf(':'); + if (colon > 0) { + headers.put(headerLine.substring(0, colon).trim().toLowerCase(), + headerLine.substring(colon + 1).trim()); + } + } + + // determine the body stream + final var transferEncoding = headers.getOrDefault("transfer-encoding", ""); + final var contentLengthStr = headers.get("content-length"); + + final InputStream body; + if ("chunked".equalsIgnoreCase(transferEncoding)) { + body = new ChunkedInputStream(in); + } else if (contentLengthStr != null) { + body = new BoundedInputStream(in, Long.parseLong(contentLengthStr)); + } else { + body = in; + } + + return new ParsedResponse(statusCode, headers, body, connection); + } + + /** + * Reads a CRLF-terminated line from the {@link InputStream}. + * + * @param in the {@link InputStream} + * @return the line content without the CRLF + * @throws IOException when reading fails + */ + static String readLine(final InputStream in) throws IOException { + final var sb = new StringBuilder(); + int b; + while ((b = in.read()) != -1) { + if (b == '\r') { + final var next = in.read(); + if (next == '\n') { + break; + } + sb.append((char) b); + if (next != -1) { + sb.append((char) next); + } + } else if (b == '\n') { + break; + } else { + sb.append((char) b); + } + } + return sb.toString(); + } + + /** + * An {@link HttpTransport.Response} backed by a parsed HTTP/1.1 response. + */ + private static final class ParsedResponse + implements HttpTransport.Response { + + private final int statusCode; + private final Map headers; + private final InputStream body; + private final AutoCloseable connection; + + ParsedResponse(final int statusCode, + final Map headers, + final InputStream body, + final AutoCloseable connection) { + this.statusCode = statusCode; + this.headers = headers; + this.body = body; + this.connection = connection; + } + + @Override + public int statusCode() { + return this.statusCode; + } + + @Override + public String header(final String name) { + return this.headers.get(name.toLowerCase()); + } + + @Override + public InputStream bodyStream() { + return this.body; + } + + @Override + public void cancel() { + close(); + } + + @Override + public void close() { + try { + this.connection.close(); + } catch (final Exception ignored) { + // we ignore close failures + } + } + } + + /** + * An {@link InputStream} that decodes HTTP chunked transfer encoding. + */ + static final class ChunkedInputStream + extends InputStream { + + private final InputStream in; + private int remaining; + private boolean done; + + ChunkedInputStream(final InputStream in) { + this.in = in; + this.remaining = 0; + this.done = false; + } + + @Override + public int read() throws IOException { + final var buf = new byte[1]; + final var n = read(buf, 0, 1); + return n == -1 ? -1 : buf[0] & 0xff; + } + + @Override + public int read(final byte[] buf, final int off, final int len) throws IOException { + if (this.done) { + return -1; + } + if (this.remaining == 0) { + // read the chunk size line (hex digits, possibly with extensions) + final var sizeLine = readLine(this.in).trim(); + if (sizeLine.isEmpty()) { + return -1; + } + // strip chunk extensions (anything after ';') + final var semicolon = sizeLine.indexOf(';'); + final var hexSize = semicolon >= 0 ? sizeLine.substring(0, semicolon) : sizeLine; + this.remaining = Integer.parseInt(hexSize.trim(), 16); + if (this.remaining == 0) { + this.done = true; + return -1; + } + } + final var toRead = Math.min(len, this.remaining); + final var n = this.in.read(buf, off, toRead); + if (n > 0) { + this.remaining -= n; + if (this.remaining == 0) { + // consume the trailing CRLF after the chunk data + readLine(this.in); + } + } + return n; + } + } + + /** + * An {@link InputStream} that reads at most a bounded number of bytes. + */ + static final class BoundedInputStream + extends InputStream { + + private final InputStream in; + private long remaining; + + BoundedInputStream(final InputStream in, final long limit) { + this.in = in; + this.remaining = limit; + } + + @Override + public int read() throws IOException { + if (this.remaining <= 0) { + return -1; + } + final var b = this.in.read(); + if (b != -1) { + this.remaining--; + } + return b; + } + + @Override + public int read(final byte[] buf, final int off, final int len) throws IOException { + if (this.remaining <= 0) { + return -1; + } + final var toRead = (int) Math.min(len, this.remaining); + final var n = this.in.read(buf, off, toRead); + if (n > 0) { + this.remaining -= n; + } + return n; + } + } +} diff --git a/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/HttpTransport.java b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/HttpTransport.java new file mode 100644 index 0000000..5410e4d --- /dev/null +++ b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/HttpTransport.java @@ -0,0 +1,196 @@ +package build.spawn.docker.jdk; + +/*- + * #%L + * Spawn Docker (JDK 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 java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * A transport abstraction over HTTP for communicating with the Docker Engine API. + * + * @author brian.oliver + * @since Apr-2026 + */ +public interface HttpTransport { + + /** + * Executes the specified {@link Request} and returns a {@link Response}. + *

+ * The caller is responsible for closing the {@link Response}. + * + * @param request the {@link Request} to execute + * @return the {@link Response} + * @throws IOException when the request fails + */ + Response execute(Request request) throws IOException; + + /** + * An HTTP request. + * + * @param method the HTTP method (GET, POST, DELETE, HEAD) + * @param path the request path including any query parameters (e.g. "/containers/123/start?signal=SIGKILL") + * @param headers the request headers + * @param body the request body bytes, or {@code null} for no body + */ + record Request(String method, String path, Map headers, byte[] body) { + + /** + * Constructs a {@link Request} with a defensive copy of the headers map. + */ + public Request { + headers = new LinkedHashMap<>(headers == null ? Map.of() : headers); + } + + /** + * Returns a copy of this {@link Request} with an additional header. + * + * @param name the header name + * @param value the header value + * @return a new {@link Request} with the header added + */ + public Request withHeader(final String name, final String value) { + final var copy = new LinkedHashMap<>(headers); + copy.put(name, value); + return new Request(method, path, copy, body); + } + + /** + * Returns a copy of this {@link Request} with a {@code Content-Type} header. + * + * @param contentType the content type + * @return a new {@link Request} + */ + public Request withContentType(final String contentType) { + return withHeader("Content-Type", contentType); + } + + /** + * Creates a GET {@link Request} for the specified path. + * + * @param path the request path + * @return a new GET {@link Request} + */ + public static Request get(final String path) { + return new Request("GET", path, Map.of(), null); + } + + /** + * Creates a POST {@link Request} for the specified path and body. + * + * @param path the request path + * @param body the request body bytes, or {@code null} for an empty body + * @return a new POST {@link Request} + */ + public static Request post(final String path, final byte[] body) { + return new Request("POST", path, Map.of(), body); + } + + /** + * Creates a DELETE {@link Request} for the specified path. + * + * @param path the request path + * @return a new DELETE {@link Request} + */ + public static Request delete(final String path) { + return new Request("DELETE", path, Map.of(), null); + } + + /** + * Creates a PUT {@link Request} for the specified path and body. + * + * @param path the request path + * @param body the request body bytes, or {@code null} for an empty body + * @return a new PUT {@link Request} + */ + public static Request put(final String path, final byte[] body) { + return new Request("PUT", path, Map.of(), body); + } + + /** + * Creates a HEAD {@link Request} for the specified path. + * + * @param path the request path + * @return a new HEAD {@link Request} + */ + public static Request head(final String path) { + return new Request("HEAD", path, Map.of(), null); + } + } + + /** + * An HTTP response. + */ + interface Response extends AutoCloseable { + + /** + * Returns the HTTP status code. + * + * @return the status code + */ + int statusCode(); + + /** + * Returns {@code true} when the status code is in the 2xx range. + * + * @return {@code true} if successful + */ + default boolean isSuccessful() { + return statusCode() >= 200 && statusCode() < 300; + } + + /** + * Returns the value of the specified response header, or {@code null} if absent. + * + * @param name the header name (case-insensitive) + * @return the header value, or {@code null} + */ + String header(String name); + + /** + * Returns the response body as an {@link InputStream}. + * + * @return the body stream + * @throws IOException when the body cannot be read + */ + InputStream bodyStream() throws IOException; + + /** + * Returns the complete response body as a {@link String}. + * + * @return the body string + * @throws IOException when the body cannot be read + */ + default String bodyString() throws IOException { + return new String(bodyStream().readAllBytes(), StandardCharsets.UTF_8); + } + + /** + * Cancels any ongoing processing and releases resources. + */ + void cancel(); + + @Override + void close(); + } +} diff --git a/spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/InternalDockerHostBasedSessionFactory.java b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/InternalDockerHostBasedSessionFactory.java similarity index 96% rename from spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/InternalDockerHostBasedSessionFactory.java rename to spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/InternalDockerHostBasedSessionFactory.java index 7f290d3..53a822f 100644 --- a/spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/InternalDockerHostBasedSessionFactory.java +++ b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/InternalDockerHostBasedSessionFactory.java @@ -1,8 +1,8 @@ -package build.spawn.docker.okhttp; +package build.spawn.docker.jdk; /*- * #%L - * Spawn Docker (OkHttp Client) + * Spawn Docker (JDK Client) * %% * Copyright (C) 2026 Workday, Inc. * %% diff --git a/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/JavaHttpClientTransport.java b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/JavaHttpClientTransport.java new file mode 100644 index 0000000..956d46f --- /dev/null +++ b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/JavaHttpClientTransport.java @@ -0,0 +1,134 @@ +package build.spawn.docker.jdk; + +/*- + * #%L + * Spawn Docker (JDK 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 java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.util.Objects; + +/** + * An {@link HttpTransport} implementation that uses {@link HttpClient} to connect to Docker via TCP. + * + * @author brian.oliver + * @since Apr-2026 + */ +public class JavaHttpClientTransport + implements HttpTransport { + + /** + * The {@link HttpClient} to use for executing requests. + */ + private final HttpClient httpClient; + + /** + * The base URL of the Docker daemon (e.g. {@code http://localhost:2375}). + */ + private final String baseUrl; + + /** + * Constructs a {@link JavaHttpClientTransport}. + * + * @param httpClient the {@link HttpClient} + * @param baseUrl the base URL (e.g. {@code http://localhost:2375}) + */ + public JavaHttpClientTransport(final HttpClient httpClient, final String baseUrl) { + this.httpClient = Objects.requireNonNull(httpClient, "The HttpClient must not be null"); + this.baseUrl = Objects.requireNonNull(baseUrl, "The base URL must not be null"); + } + + @Override + public Response execute(final Request request) throws IOException { + try { + final var builder = HttpRequest.newBuilder() + .uri(URI.create(this.baseUrl + request.path())); + + request.headers().forEach(builder::header); + + final var body = request.body(); + final var bodyPublisher = (body == null || body.length == 0) + ? HttpRequest.BodyPublishers.noBody() + : HttpRequest.BodyPublishers.ofByteArray(body); + + switch (request.method()) { + case "GET" -> builder.GET(); + case "DELETE" -> builder.DELETE(); + case "HEAD" -> builder.method("HEAD", HttpRequest.BodyPublishers.noBody()); + case "PUT" -> builder.PUT(bodyPublisher); + default -> builder.POST(bodyPublisher); + } + + final var httpResponse = this.httpClient + .send(builder.build(), HttpResponse.BodyHandlers.ofInputStream()); + + return new JavaHttpResponse(httpResponse); + + } catch (final InterruptedException e) { + Thread.currentThread().interrupt(); + throw new IOException("Request interrupted", e); + } + } + + /** + * An {@link HttpTransport.Response} backed by a {@link HttpResponse}. + */ + private static final class JavaHttpResponse + implements Response { + + private final HttpResponse response; + + JavaHttpResponse(final HttpResponse response) { + this.response = response; + } + + @Override + public int statusCode() { + return this.response.statusCode(); + } + + @Override + public String header(final String name) { + return this.response.headers().firstValue(name).orElse(null); + } + + @Override + public InputStream bodyStream() { + return this.response.body(); + } + + @Override + public void cancel() { + close(); + } + + @Override + public void close() { + try { + this.response.body().close(); + } catch (final IOException ignored) { + // we ignore close failures + } + } + } +} diff --git a/spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/LocalHostBasedSessionFactory.java b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/LocalHostBasedSessionFactory.java similarity index 96% rename from spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/LocalHostBasedSessionFactory.java rename to spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/LocalHostBasedSessionFactory.java index e3b4887..9dec51f 100644 --- a/spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/LocalHostBasedSessionFactory.java +++ b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/LocalHostBasedSessionFactory.java @@ -1,8 +1,8 @@ -package build.spawn.docker.okhttp; +package build.spawn.docker.jdk; /*- * #%L - * Spawn Docker (OkHttp Client) + * Spawn Docker (JDK Client) * %% * Copyright (C) 2026 Workday, Inc. * %% diff --git a/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/TCPSocketBasedSession.java b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/TCPSocketBasedSession.java new file mode 100644 index 0000000..110e278 --- /dev/null +++ b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/TCPSocketBasedSession.java @@ -0,0 +1,58 @@ +package build.spawn.docker.jdk; + +/*- + * #%L + * Spawn Docker (JDK 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.Configuration; +import build.codemodel.injection.InjectionFramework; + +import java.net.InetSocketAddress; +import java.net.http.HttpClient; +import java.time.Duration; + +/** + * A TCP-socket-based {@code Docker} {@link build.spawn.docker.Session}. + * + * @author brian.oliver + * @since Aug-2022 + */ +public class TCPSocketBasedSession + extends AbstractSession { + + /** + * Constructs a {@link TCPSocketBasedSession} for the specified {@link InetSocketAddress}. + * + * @param injectionFramework the {@link InjectionFramework} + * @param socketAddress the {@link InetSocketAddress} + * @param configuration the {@link Configuration} + */ + public TCPSocketBasedSession(final InjectionFramework injectionFramework, + final InetSocketAddress socketAddress, + final Configuration configuration) { + + super(injectionFramework, + new JavaHttpClientTransport( + HttpClient.newBuilder() + .connectTimeout(Duration.ofSeconds(10)) + .build(), + "http://" + socketAddress.getHostString() + ":" + socketAddress.getPort()), + configuration); + } +} diff --git a/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/UnixDomainSocketBasedSession.java b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/UnixDomainSocketBasedSession.java new file mode 100644 index 0000000..6acdc3e --- /dev/null +++ b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/UnixDomainSocketBasedSession.java @@ -0,0 +1,122 @@ +package build.spawn.docker.jdk; + +/*- + * #%L + * Spawn Docker (JDK 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.Configuration; +import build.codemodel.injection.InjectionFramework; +import build.spawn.docker.Session; +import jakarta.inject.Inject; + +import java.io.File; +import java.net.UnixDomainSocketAddress; +import java.nio.channels.SocketChannel; +import java.util.ArrayList; +import java.util.Optional; + +/** + * A Unix Domain Socket-based {@code Docker} {@link Session}. + * + * @author brian.oliver + * @since Jun-2021 + */ +public class UnixDomainSocketBasedSession + extends AbstractSession { + + /** + * The {@code docker.sock} {@link File}. + */ + private static final File DOCKER_SOCK_FILE = resolveDockerSockFile(); + + /** + * Resolves the {@code docker.sock} {@link File} by checking known locations in order of preference. + * + * @return the {@code docker.sock} {@link File} + */ + private static File resolveDockerSockFile() { + final var candidates = new ArrayList(); + + // Docker Desktop on Linux + candidates.add(new File(System.getProperty("user.home") + "/.docker/desktop/docker.sock")); + + // standard Docker Engine location + candidates.add(new File("/var/run/docker.sock")); + + return candidates.stream() + .filter(File::exists) + .findFirst() + .orElse(new File("/var/run/docker.sock")); + } + + /** + * Constructs a {@link UnixDomainSocketBasedSession} using the default {@code docker.sock} file. + * + * @param injectionFramework the {@link InjectionFramework} for Dependency Injection + * @param configuration the {@link Configuration} + */ + public UnixDomainSocketBasedSession(final InjectionFramework injectionFramework, + final Configuration configuration) { + + this(injectionFramework, DOCKER_SOCK_FILE, configuration); + } + + /** + * Constructs a {@link UnixDomainSocketBasedSession} for the specified Unix socket {@link File}. + * + * @param injectionFramework the {@link InjectionFramework} for Dependency Injection + * @param socketFile the Unix socket {@link File} + * @param configuration the {@link Configuration} + */ + public UnixDomainSocketBasedSession(final InjectionFramework injectionFramework, + final File socketFile, + final Configuration configuration) { + + super(injectionFramework, new UnixSocketHttpTransport(socketFile), configuration); + } + + /** + * The {@link Session.Factory} for the {@link UnixDomainSocketBasedSession}s. + */ + public static class Factory + implements Session.Factory { + + /** + * The {@link InjectionFramework} to use for Dependency Injection. + */ + @Inject + private InjectionFramework injectionFramework; + + @Override + public boolean isOperational() { + try (var _ = SocketChannel.open(UnixDomainSocketAddress.of(DOCKER_SOCK_FILE.toPath()))) { + return true; + } catch (final Exception _) { + return false; + } + } + + @Override + public Optional create(final Configuration configuration) { + return isOperational() + ? Optional.of(new UnixDomainSocketBasedSession(this.injectionFramework, configuration)) + : Optional.empty(); + } + } +} diff --git a/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/UnixSocketHttpTransport.java b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/UnixSocketHttpTransport.java new file mode 100644 index 0000000..c0d42f3 --- /dev/null +++ b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/UnixSocketHttpTransport.java @@ -0,0 +1,108 @@ +package build.spawn.docker.jdk; + +/*- + * #%L + * Spawn Docker (JDK 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 java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.OutputStream; +import java.net.UnixDomainSocketAddress; +import java.nio.channels.Channels; +import java.nio.channels.SocketChannel; +import java.nio.charset.StandardCharsets; +import java.util.Map; +import java.util.Objects; + +/** + * An {@link HttpTransport} implementation that communicates with the Docker daemon over a Unix domain socket. + *

+ * Uses {@link SocketChannel} with {@link UnixDomainSocketAddress} (standard since Java 16) to establish + * the socket connection and implements HTTP/1.1 framing directly. + * + * @author brian.oliver + * @since Apr-2026 + */ +public class UnixSocketHttpTransport + implements HttpTransport { + + /** + * The Unix domain socket {@link File}. + */ + private final File socketFile; + + /** + * Constructs a {@link UnixSocketHttpTransport}. + * + * @param socketFile the Unix domain socket {@link File} + */ + public UnixSocketHttpTransport(final File socketFile) { + this.socketFile = Objects.requireNonNull(socketFile, "The socket file must not be null"); + } + + @Override + public Response execute(final Request request) throws IOException { + final var channel = SocketChannel.open(UnixDomainSocketAddress.of(this.socketFile.toPath())); + + final var out = new BufferedOutputStream(Channels.newOutputStream(channel)); + writeRequest(out, request); + out.flush(); + + return Http11Parser.parse(new BufferedInputStream(Channels.newInputStream(channel)), channel); + } + + /** + * Writes an HTTP/1.1 request to the specified {@link OutputStream}. + * + * @param out the {@link OutputStream} + * @param request the {@link Request} + * @throws IOException when writing fails + */ + private void writeRequest(final OutputStream out, final Request request) throws IOException { + writeLine(out, request.method() + " " + request.path() + " HTTP/1.1"); + writeLine(out, "Host: docker"); + writeLine(out, "Connection: close"); + + final var body = request.body(); + writeLine(out, "Content-Length: " + (body != null ? body.length : 0)); + + for (final Map.Entry header : request.headers().entrySet()) { + writeLine(out, header.getKey() + ": " + header.getValue()); + } + + writeLine(out, ""); + + if (body != null && body.length > 0) { + out.write(body); + } + } + + /** + * Writes a CRLF-terminated line to the {@link OutputStream}. + * + * @param out the {@link OutputStream} + * @param line the line content + * @throws IOException when writing fails + */ + private void writeLine(final OutputStream out, final String line) throws IOException { + out.write((line + "\r\n").getBytes(StandardCharsets.UTF_8)); + } +} diff --git a/spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/command/AbstractBlockingCommand.java b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/AbstractBlockingCommand.java similarity index 55% rename from spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/command/AbstractBlockingCommand.java rename to spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/AbstractBlockingCommand.java index a06eeba..96d98ea 100644 --- a/spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/command/AbstractBlockingCommand.java +++ b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/AbstractBlockingCommand.java @@ -1,17 +1,17 @@ -package build.spawn.docker.okhttp.command; +package build.spawn.docker.jdk.command; /*- * #%L - * Spawn Docker (OkHttp Client) + * Spawn Docker (JDK 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. @@ -20,12 +20,7 @@ * #L% */ -import okhttp3.Call; -import okhttp3.OkHttpClient; -import okhttp3.Request; -import okhttp3.Response; - -import java.util.concurrent.TimeUnit; +import build.spawn.docker.jdk.HttpTransport; /** * An abstract {@link Command} that blocks until the {@link Command} has been executed and a response has been @@ -38,35 +33,19 @@ public abstract class AbstractBlockingCommand extends AbstractCommand { @Override - protected OkHttpClient httpClient() { - // ensure the reading from the response never times out (so we can continuously wait for responses) - return super.httpClient().newBuilder() - .connectTimeout(0, TimeUnit.MINUTES) - .readTimeout(0, TimeUnit.MINUTES) - .writeTimeout(0, TimeUnit.MINUTES) - .build(); - } - - @Override - protected void onRequestCreated(final Request request) { + protected void onRequestCreated(final HttpTransport.Request request) { // by default, we don't do anything when the Request is created } @Override - protected void onCallCreated(final Call call) { - // by default, we don't do anything when the Call is created - } - - @Override - protected void onResponseReceived(final Response response) { + protected void onResponseReceived(final HttpTransport.Response response) { // by default, we don't do anything with the Response when it is received } @Override - protected void onSuccessfulRequest(final Request request, - final Response response, + protected void onSuccessfulRequest(final HttpTransport.Request request, + final HttpTransport.Response response, final T result) { - response.close(); } } diff --git a/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/AbstractCommand.java b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/AbstractCommand.java new file mode 100644 index 0000000..fc7958f --- /dev/null +++ b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/AbstractCommand.java @@ -0,0 +1,187 @@ +package build.spawn.docker.jdk.command; + +/*- + * #%L + * Spawn Docker (JDK 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.codemodel.injection.Context; +import build.spawn.docker.jdk.HttpTransport; +import jakarta.inject.Inject; + +import java.io.IOException; + +/** + * An abstract {@link Command} that uses an {@link HttpTransport} to submit request(s) and receive responses, + * that of which may be processed to produce a result. + * + * @param the type of result produced by the {@link Command} + * @author brian.oliver + * @since Jun-2021 + */ +public abstract class AbstractCommand + implements Command { + + /** + * The {@link HttpTransport} to use for executing the {@link Command}. + */ + @Inject + private HttpTransport transport; + + /** + * The dependency injection {@link Context} for the {@link Command}. + */ + @Inject + private Context context; + + /** + * Obtains the {@link HttpTransport} to use for executing {@link Command}s. + * + * @return the {@link HttpTransport} + */ + protected HttpTransport transport() { + return this.transport; + } + + /** + * Creates a new dependency injection {@link Context}, based on the {@link Context} used to create the + * {@link Command}. + * + * @return a new {@link Context} + */ + protected Context createContext() { + return this.context.newContext(); + } + + /** + * Obtains the {@link HttpTransport.Request} to execute for the {@link Command}. + * + * @return the {@link HttpTransport.Request} + */ + abstract protected HttpTransport.Request createRequest(); + + /** + * Creates the result for the {@link Command} based on the successful {@link HttpTransport.Response}. + * + * @param response the {@link HttpTransport.Response} + * @return the result + * @throws IOException should processing the response fail + */ + abstract protected T createResult(HttpTransport.Response response) + throws IOException; + + /** + * Handle when the {@link HttpTransport.Request} has been created for the {@link Command}, but not yet sent. + * + * @param request the {@link HttpTransport.Request} + */ + abstract protected void onRequestCreated(HttpTransport.Request request); + + /** + * Handle when the {@link HttpTransport.Response} has been received for the {@link Command}. + * + * @param response the {@link HttpTransport.Response} + */ + abstract protected void onResponseReceived(HttpTransport.Response response); + + /** + * Handle when the execution of a {@link HttpTransport.Request} was successful. + * + * @param request the {@link HttpTransport.Request} + * @param response the {@link HttpTransport.Response} + * @param result the result + */ + abstract protected void onSuccessfulRequest(HttpTransport.Request request, + HttpTransport.Response response, + T result); + + /** + * Handle when the execution of a {@link HttpTransport.Request} was unsuccessful. + * + * @param request the {@link HttpTransport.Request} + * @param response the {@link HttpTransport.Response} + * @throws IOException should the request be unsuccessful + */ + protected void onUnsuccessfulRequest(final HttpTransport.Request request, + final HttpTransport.Response response) + throws IOException { + + throw new IOException("Request failed with " + response.statusCode() + + ". Failed to execute " + request.method() + " " + request.path()); + } + + /** + * Handle when the execution of a {@link HttpTransport.Request} failed with the specified {@link Throwable}. + * + * @param request the {@link HttpTransport.Request} + * @param throwable the {@link Throwable} + * @return the result if the request was recoverable + */ + protected T onRequestFailed(final HttpTransport.Request request, + final Throwable throwable) { + + throw (throwable instanceof RuntimeException) + ? (RuntimeException) throwable + : new RuntimeException("Request Failed", throwable); + } + + /** + * Handle when attempting to process the {@link HttpTransport.Response} failed. + * + * @param request the {@link HttpTransport.Request} + * @param response the {@link HttpTransport.Response} + * @param throwable the {@link Throwable} + */ + public void onUnprocessableResponse(final HttpTransport.Request request, + final HttpTransport.Response response, + final Throwable throwable) { + response.close(); + } + + @Override + public T submit() { + final var request = createRequest(); + + try { + onRequestCreated(request); + + final var response = transport().execute(request); + + onResponseReceived(response); + + try { + if (!response.isSuccessful()) { + onUnsuccessfulRequest(request, response); + } + + final var result = createResult(response); + + onSuccessfulRequest(request, response, result); + + return result; + + } catch (final Throwable throwable) { + onUnprocessableResponse(request, response, throwable); + throw throwable; + } + + } catch (final Throwable recoverable) { + return onRequestFailed(request, recoverable); + } + } +} diff --git a/spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/command/AbstractEventBasedBlockingCommand.java b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/AbstractEventBasedBlockingCommand.java similarity index 80% rename from spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/command/AbstractEventBasedBlockingCommand.java rename to spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/AbstractEventBasedBlockingCommand.java index 0def6e2..e1b7231 100644 --- a/spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/command/AbstractEventBasedBlockingCommand.java +++ b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/AbstractEventBasedBlockingCommand.java @@ -1,17 +1,17 @@ -package build.spawn.docker.okhttp.command; +package build.spawn.docker.jdk.command; /*- * #%L - * Spawn Docker (OkHttp Client) + * Spawn Docker (JDK 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. @@ -23,9 +23,8 @@ import build.base.flow.Publisher; import build.base.foundation.Lazy; import build.spawn.docker.Event; +import build.spawn.docker.jdk.HttpTransport; import jakarta.inject.Inject; -import okhttp3.Request; -import okhttp3.Response; import java.util.concurrent.CompletableFuture; @@ -46,7 +45,7 @@ public abstract class AbstractEventBasedBlockingCommand extends AbstractBlock private Publisher publisher; /** - * The {@link CompletableFuture} indicating when the required {@link Event}s have been received or has failed + * The {@link CompletableFuture} indicating when the required {@link Event}s have been received. */ private final Lazy> processing; @@ -65,24 +64,17 @@ protected AbstractEventBasedBlockingCommand() { */ abstract protected CompletableFuture subscribe(Publisher publisher); - @Override - protected void onRequestCreated(final Request request) { - // subscribe to the publisher for the required Events + protected void onRequestCreated(final HttpTransport.Request request) { this.processing.set(subscribe(this.publisher)); - super.onRequestCreated(request); } @Override - protected void onSuccessfulRequest(final Request request, - final Response response, + protected void onSuccessfulRequest(final HttpTransport.Request request, + final HttpTransport.Response response, final T result) { - super.onSuccessfulRequest(request, response, result); - - // now wait for the required processing to complete - this.processing.orElseThrow() - .join(); + this.processing.orElseThrow().join(); } } diff --git a/spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/command/AbstractNonBlockingCommand.java b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/AbstractNonBlockingCommand.java similarity index 60% rename from spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/command/AbstractNonBlockingCommand.java rename to spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/AbstractNonBlockingCommand.java index b8631cf..e46d22a 100644 --- a/spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/command/AbstractNonBlockingCommand.java +++ b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/AbstractNonBlockingCommand.java @@ -1,17 +1,17 @@ -package build.spawn.docker.okhttp.command; +package build.spawn.docker.jdk.command; /*- * #%L - * Spawn Docker (OkHttp Client) + * Spawn Docker (JDK 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. @@ -22,17 +22,13 @@ import build.base.foundation.AtomicEnum; import build.base.foundation.Lazy; -import okhttp3.Call; -import okhttp3.OkHttpClient; -import okhttp3.Request; -import okhttp3.Response; +import build.spawn.docker.jdk.HttpTransport; import java.io.Closeable; import java.util.concurrent.CompletableFuture; -import java.util.concurrent.TimeUnit; /** - * An abstract {@link Command} that continues to process the {@link Response} after execution, until the + * An abstract {@link Command} that continues to process the response after execution, until the * {@link Command} is closed or a processing failure occurs. * * @param the type of result produced by the {@link Command} @@ -43,14 +39,9 @@ public abstract class AbstractNonBlockingCommand extends AbstractCommand implements Closeable { /** - * The {@link Lazy} {@link Call} for the {@link Command}. - */ - protected final Lazy call; - - /** - * The {@link Lazy} {@link Response} to process. + * The active {@link HttpTransport.Response} being processed. */ - protected final Lazy response; + protected final Lazy activeResponse; /** * The {@link Lazy} result of the {@link Command}. @@ -58,8 +49,7 @@ public abstract class AbstractNonBlockingCommand extends AbstractCommand protected final Lazy result; /** - * A {@link CompletableFuture} indicating when the {@link Response} processing has completed, either exceptionally - * or otherwise. + * A {@link CompletableFuture} indicating when processing has completed. */ private final CompletableFuture processing; @@ -72,73 +62,47 @@ public abstract class AbstractNonBlockingCommand extends AbstractCommand * Constructs an {@link AbstractNonBlockingCommand}. */ protected AbstractNonBlockingCommand() { - this.call = Lazy.empty(); - this.response = Lazy.empty(); + this.activeResponse = Lazy.empty(); this.result = Lazy.empty(); this.processing = new CompletableFuture<>(); this.state = AtomicEnum.of(State.INITIALIZING); } @Override - protected OkHttpClient httpClient() { - // ensure the reading from the response never times out (so we can continuously wait for responses) - return super.httpClient().newBuilder() - .connectTimeout(0, TimeUnit.MILLISECONDS) - .readTimeout(0, TimeUnit.MILLISECONDS) - .writeTimeout(0, TimeUnit.MILLISECONDS) - .build(); - } - - @Override - protected void onRequestCreated(final Request request) { + protected void onRequestCreated(final HttpTransport.Request request) { // by default, we don't do anything when the Request is created } @Override - protected void onCallCreated(final Call call) { - this.call.set(call); - } - - @Override - protected void onResponseReceived(final Response response) { - this.response.set(response); + protected void onResponseReceived(final HttpTransport.Response response) { + this.activeResponse.set(response); this.state.compareAndSet(State.INITIALIZING, State.PROCESSING); } @Override - protected void onSuccessfulRequest(final Request request, - final Response response, + protected void onSuccessfulRequest(final HttpTransport.Request request, + final HttpTransport.Response response, final T result) { - - // capture the result so we can later complete the processing with it this.result.set(result); - // NOTE: we don't close the Response, so that we can continue to consume it } protected void onProcessingError(final Throwable throwable) { if (this.state.compareAndSet(State.PROCESSING, State.COMPLETED)) { - // complete the processing exceptionally this.processing.completeExceptionally(throwable); - - // cancel the Call - this.call.ifPresent(Call::cancel); + this.activeResponse.ifPresent(HttpTransport.Response::cancel); } } protected void onProcessingCompletion() { if (this.state.compareAndSet(State.PROCESSING, State.COMPLETED)) { - // complete the processing with the result this.processing.complete(this.result.orElse(null)); - - // cancel the Call - this.call.ifPresent(Call::cancel); + this.activeResponse.ifPresent(HttpTransport.Response::cancel); } } /** - * Obtains a {@link CompletableFuture} that will be completed when the {@link Response} processing has completed, - * either exceptionally or otherwise. + * Obtains a {@link CompletableFuture} that will be completed when processing has completed. * * @return the {@link CompletableFuture} */ @@ -156,14 +120,9 @@ protected void onClosing() { @Override public void close() { if (this.state.compareAndSet(State.PROCESSING, State.COMPLETED)) { - // notify that the Command is being closed onClosing(); - - // complete the processing with the result this.processing.complete(this.result.orElse(null)); - - // cancel the Call - this.call.ifPresent(Call::cancel); + this.activeResponse.ifPresent(HttpTransport.Response::cancel); } } diff --git a/spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/command/AttachContainer.java b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/AttachContainer.java similarity index 77% rename from spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/command/AttachContainer.java rename to spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/AttachContainer.java index 453efd6..e9a884e 100644 --- a/spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/command/AttachContainer.java +++ b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/AttachContainer.java @@ -1,8 +1,8 @@ -package build.spawn.docker.okhttp.command; +package build.spawn.docker.jdk.command; /*- * #%L - * Spawn Docker (OkHttp Client) + * Spawn Docker (JDK Client) * %% * Copyright (C) 2026 Workday, Inc. * %% @@ -24,11 +24,8 @@ import build.base.io.NullWriter; import build.base.io.Terminal; import build.spawn.docker.Container; +import build.spawn.docker.jdk.HttpTransport; import jakarta.inject.Inject; -import okhttp3.FormBody; -import okhttp3.HttpUrl; -import okhttp3.Request; -import okhttp3.Response; import java.io.IOException; import java.io.PipedReader; @@ -61,25 +58,14 @@ public AttachContainer(final Configuration configuration) { } @Override - protected Request createRequest(final HttpUrl.Builder httpUrlBuilder) { - return new Request.Builder() - .url(httpUrlBuilder - .addPathSegment("containers") - .addPathSegment(this.container.id()) - .addPathSegment("attach") - .build()) - .post(new FormBody.Builder() - .add("logs", "true") - .add("stream", "true") - .add("stdin", "false") - .add("stdout", "true") - .add("stderr", "true") - .build()) - .build(); + protected HttpTransport.Request createRequest() { + return HttpTransport.Request.post( + "/containers/" + this.container.id() + "/attach?logs=true&stream=true&stdin=false&stdout=true&stderr=true", + null); } @Override - protected Terminal createResult(final Response response) + protected Terminal createResult(final HttpTransport.Response response) throws IOException { // establish PipedReaders for stdout and stderr @@ -117,7 +103,7 @@ public CompletableFuture onClosed() { }; // establish a FrameProcessor to redirect the I/O frames to the Terminal - final var processor = new FrameProcessor(response, outputReader, errorReader); + final var processor = new FrameProcessor(response.bodyStream(), outputReader, errorReader); processor.processing() .whenComplete((_, error) -> { diff --git a/spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/command/Authenticate.java b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/Authenticate.java similarity index 74% rename from spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/command/Authenticate.java rename to spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/Authenticate.java index 02cc76e..b9f1635 100644 --- a/spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/command/Authenticate.java +++ b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/Authenticate.java @@ -1,8 +1,8 @@ -package build.spawn.docker.okhttp.command; +package build.spawn.docker.jdk.command; /*- * #%L - * Spawn Docker (OkHttp Client) + * Spawn Docker (JDK Client) * %% * Copyright (C) 2026 Workday, Inc. * %% @@ -26,18 +26,14 @@ import build.base.option.Password; import build.base.option.Username; import build.spawn.docker.Session; +import build.spawn.docker.jdk.HttpTransport; import build.spawn.docker.option.DockerRegistry; import build.spawn.docker.option.IdentityToken; import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.node.ArrayNode; -import com.fasterxml.jackson.databind.node.ObjectNode; import jakarta.inject.Inject; -import okhttp3.HttpUrl; -import okhttp3.Request; -import okhttp3.RequestBody; -import okhttp3.Response; import java.io.IOException; +import java.nio.charset.StandardCharsets; /** * The {@code Docker Daemon} {@link Command} to authenticate and validate credentials with a Docker Registry using the @@ -80,7 +76,7 @@ public class Authenticate private Configuration configuration; @Override - protected Request createRequest(final HttpUrl.Builder httpUrlBuilder) { + protected HttpTransport.Request createRequest() { // establish an ObjectNode containing the auth json final var node = this.objectMapper.createObjectNode(); @@ -91,27 +87,16 @@ protected Request createRequest(final HttpUrl.Builder httpUrlBuilder) { .ifPresent(email -> node.put("email", email)); node.put("serveraddress", this.dockerRegistry.get().toString()); - return new Request.Builder() - .url(httpUrlBuilder - .addPathSegment("auth") - .build()) - .post(RequestBody.create(node.toString(), MEDIA_TYPE_JSON)) - .build(); - } - - private ArrayNode getOrCreateArray(final ObjectNode node, final String key) { - final var child = node.get(key); - if (child instanceof ArrayNode) { - return (ArrayNode) child; - } - return node.putArray(key); + return HttpTransport.Request + .post("/auth", node.toString().getBytes(StandardCharsets.UTF_8)) + .withContentType("application/json"); } @Override - protected IdentityToken createResult(final Response response) + protected IdentityToken createResult(final HttpTransport.Response response) throws IOException { - final var body = response.body().string(); + final var body = response.bodyString(); final var json = this.objectMapper.readTree(body); final var identityToken = json.get("IdentityToken").asText(); diff --git a/spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/command/BuildImage.java b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/BuildImage.java similarity index 76% rename from spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/command/BuildImage.java rename to spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/BuildImage.java index 1cea244..00ebd9b 100644 --- a/spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/command/BuildImage.java +++ b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/BuildImage.java @@ -1,8 +1,8 @@ -package build.spawn.docker.okhttp.command; +package build.spawn.docker.jdk.command; /*- * #%L - * Spawn Docker (OkHttp Client) + * Spawn Docker (JDK Client) * %% * Copyright (C) 2026 Workday, Inc. * %% @@ -23,17 +23,14 @@ import build.base.configuration.Configuration; import build.base.flow.CompletingSubscriber; import build.spawn.docker.Image; +import build.spawn.docker.jdk.HttpTransport; import build.spawn.docker.option.ImageName; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.inject.Inject; -import okhttp3.HttpUrl; -import okhttp3.MediaType; -import okhttp3.Request; -import okhttp3.RequestBody; -import okhttp3.Response; import java.io.IOException; +import java.nio.file.Files; import java.nio.file.Path; import java.util.Objects; import java.util.Optional; @@ -49,11 +46,6 @@ public class BuildImage extends AbstractBlockingCommand> { - /** - * The {@link MediaType} for {@code tar} files. - */ - private final static MediaType MEDIATYPE = MediaType.parse("application/x-tar"); - /** * The {@link Path} to the {@code Docker Context} for building the {@link Image}. */ @@ -86,25 +78,21 @@ public BuildImage(final Path contextPath, } @Override - protected Request createRequest(final HttpUrl.Builder httpUrlBuilder) { - - // configure the HttpUrl.Builder for the request - httpUrlBuilder - .addPathSegment("build") - .addQueryParameter("q", "false"); - - // include the optionally provided ImageNames + protected HttpTransport.Request createRequest() { + final var tagParams = new StringBuilder("?q=false"); this.configuration.stream(ImageName.class) - .forEach(name -> httpUrlBuilder.addQueryParameter("t", name.get())); - - return new Request.Builder() - .url(httpUrlBuilder.build()) - .post(RequestBody.create(this.contextPath.toFile(), MEDIATYPE)) - .build(); + .forEach(name -> tagParams.append("&t=").append(name.get())); + try { + return HttpTransport.Request + .post("/build" + tagParams, Files.readAllBytes(this.contextPath)) + .withContentType("application/x-tar"); + } catch (final IOException e) { + throw new RuntimeException("Failed to read build context", e); + } } @Override - protected Optional createResult(final Response response) + protected Optional createResult(final HttpTransport.Response response) throws IOException { // establish the CompletingObserver observe when image ID has been generated @@ -120,7 +108,7 @@ protected Optional createResult(final Response response) // process the entire InputStream from the Response to essentially wait for the image to be created final JsonNodeInputStreamProcessor processor = new JsonNodeInputStreamProcessor(this.objectMapper); - processor.process(response.body().byteStream(), completingSubscriber); + processor.process(response.bodyStream(), completingSubscriber); // we've completed building when the ImageId is available and "Successful" has been observed onSuccess.join(); diff --git a/spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/command/Command.java b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/Command.java similarity index 92% rename from spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/command/Command.java rename to spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/Command.java index 6288162..be6eb1e 100644 --- a/spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/command/Command.java +++ b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/Command.java @@ -1,8 +1,8 @@ -package build.spawn.docker.okhttp.command; +package build.spawn.docker.jdk.command; /*- * #%L - * Spawn Docker (OkHttp Client) + * Spawn Docker (JDK Client) * %% * Copyright (C) 2026 Workday, Inc. * %% diff --git a/spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/command/CopyFiles.java b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/CopyFiles.java similarity index 73% rename from spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/command/CopyFiles.java rename to spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/CopyFiles.java index 6b5ff0e..cb40a5a 100644 --- a/spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/command/CopyFiles.java +++ b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/CopyFiles.java @@ -1,8 +1,8 @@ -package build.spawn.docker.okhttp.command; +package build.spawn.docker.jdk.command; /*- * #%L - * Spawn Docker (OkHttp Client) + * Spawn Docker (JDK Client) * %% * Copyright (C) 2026 Workday, Inc. * %% @@ -22,14 +22,11 @@ import build.base.archiving.TarBuilder; import build.spawn.docker.Container; +import build.spawn.docker.jdk.HttpTransport; import jakarta.inject.Inject; -import okhttp3.HttpUrl; -import okhttp3.MediaType; -import okhttp3.Request; -import okhttp3.RequestBody; -import okhttp3.Response; import java.io.IOException; +import java.nio.file.Files; import java.nio.file.Path; /** @@ -90,29 +87,26 @@ public CopyFiles(final Path archivePath, final String destinationDirectory, fina } @Override - protected Request createRequest(final HttpUrl.Builder httpUrlBuilder) { - final MediaType mType = MediaType.parse("application/octet-stream; charset=utf-8"); - final RequestBody requestBody = RequestBody.create(this.archivePath.toFile(), mType); - return new Request.Builder() - .url(httpUrlBuilder - .addPathSegment("containers") - .addPathSegment(this.container.id()) - .addPathSegment("archive") - .addQueryParameter("path", this.destinationDirectory) - .build()) - .put(requestBody) - .build(); + protected HttpTransport.Request createRequest() { + try { + return HttpTransport.Request + .put("/containers/" + this.container.id() + "/archive?path=" + this.destinationDirectory, + Files.readAllBytes(this.archivePath)) + .withContentType("application/octet-stream"); + } catch (final IOException e) { + throw new RuntimeException("Failed to read archive", e); + } } @Override - protected Container createResult(final Response response) + protected Container createResult(final HttpTransport.Response response) throws IOException { // 400 Bad Parameter // 403 permission denied (read only) // 404 no such destination path found // 500 server error - if (response.code() != 200) { - throw new RuntimeException(response.message()); + if (response.statusCode() != 200) { + throw new RuntimeException("CopyFiles failed with HTTP " + response.statusCode()); } return this.container; } diff --git a/spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/command/CreateContainer.java b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/CreateContainer.java similarity index 87% rename from spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/command/CreateContainer.java rename to spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/CreateContainer.java index 909d148..e3a2eda 100644 --- a/spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/command/CreateContainer.java +++ b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/CreateContainer.java @@ -1,8 +1,8 @@ -package build.spawn.docker.okhttp.command; +package build.spawn.docker.jdk.command; /*- * #%L - * Spawn Docker (OkHttp Client) + * Spawn Docker (JDK Client) * %% * Copyright (C) 2026 Workday, Inc. * %% @@ -26,7 +26,8 @@ import build.codemodel.injection.Context; import build.spawn.docker.Container; import build.spawn.docker.Image; -import build.spawn.docker.okhttp.model.OkHttpBasedContainer; +import build.spawn.docker.jdk.HttpTransport; +import build.spawn.docker.jdk.model.DockerContainer; import build.spawn.docker.option.ContainerName; import build.spawn.docker.option.DockerOption; import build.spawn.docker.option.ExposedPort; @@ -39,12 +40,9 @@ import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ObjectNode; import jakarta.inject.Inject; -import okhttp3.HttpUrl; -import okhttp3.Request; -import okhttp3.RequestBody; -import okhttp3.Response; import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.util.Objects; /** @@ -100,7 +98,7 @@ public CreateContainer(final Image image, } @Override - protected Request createRequest(final HttpUrl.Builder httpUrlBuilder) { + protected HttpTransport.Request createRequest() { // establish an ObjectNode containing the containers/create json final var node = this.objectMapper.createObjectNode(); @@ -138,14 +136,10 @@ protected Request createRequest(final HttpUrl.Builder httpUrlBuilder) { final var name = this.configuration.getOptionalValue(ContainerName.class) .orElse(new UniqueNameGenerator(".").next()); - return new Request.Builder() - .url(httpUrlBuilder - .addPathSegment("containers") - .addPathSegment("create") - .addQueryParameter("name", name) - .build()) - .post(RequestBody.create(node.toString(), MEDIA_TYPE_JSON)) - .build(); + return HttpTransport.Request + .post("/containers/create?name=" + name, + node.toString().getBytes(StandardCharsets.UTF_8)) + .withContentType("application/json"); } private ArrayNode getOrCreateArray(final ObjectNode node, final String key) { @@ -157,7 +151,7 @@ private ArrayNode getOrCreateArray(final ObjectNode node, final String key) { } @Override - protected Container createResult(final Response response) + protected Container createResult(final HttpTransport.Response response) throws IOException { final var context = createContext(); @@ -166,10 +160,10 @@ protected Container createResult(final Response response) context.bind(Configuration.class).to(this.configuration); // bind the JsonNode representation of the response - final var json = response.body().string(); + final var json = response.bodyString(); context.bind(JsonNode.class).to(this.objectMapper.readTree(json)); // create the Container to return - return context.create(OkHttpBasedContainer.class); + return context.create(DockerContainer.class); } } diff --git a/spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/command/CreateExecution.java b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/CreateExecution.java similarity index 83% rename from spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/command/CreateExecution.java rename to spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/CreateExecution.java index bc452ce..a928798 100644 --- a/spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/command/CreateExecution.java +++ b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/CreateExecution.java @@ -1,8 +1,8 @@ -package build.spawn.docker.okhttp.command; +package build.spawn.docker.jdk.command; /*- * #%L - * Spawn Docker (OkHttp Client) + * Spawn Docker (JDK Client) * %% * Copyright (C) 2026 Workday, Inc. * %% @@ -24,17 +24,15 @@ import build.base.io.Terminal; import build.spawn.docker.Container; import build.spawn.docker.Execution; +import build.spawn.docker.jdk.HttpTransport; import build.spawn.docker.option.DockerOption; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ObjectNode; import jakarta.inject.Inject; -import okhttp3.HttpUrl; -import okhttp3.Request; -import okhttp3.RequestBody; -import okhttp3.Response; import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.util.Objects; /** @@ -85,7 +83,7 @@ public CreateExecution(final Container container, } @Override - protected Request createRequest(final HttpUrl.Builder httpUrlBuilder) { + protected HttpTransport.Request createRequest() { // establish an ObjectNode containing the containers/create json final ObjectNode node = this.objectMapper.createObjectNode(); @@ -98,22 +96,18 @@ protected Request createRequest(final HttpUrl.Builder httpUrlBuilder) { this.configuration.stream(DockerOption.class) .forEach(option -> option.configure(node, this.objectMapper)); - return new Request.Builder() - .url(httpUrlBuilder - .addPathSegment("containers") - .addPathSegment(this.container.id()) - .addPathSegment("exec") - .build()) - .post(RequestBody.create(node.toString(), MEDIA_TYPE_JSON)) - .build(); + return HttpTransport.Request + .post("/containers/" + this.container.id() + "/exec", + node.toString().getBytes(StandardCharsets.UTF_8)) + .withContentType("application/json"); } @Override - protected String createResult(final Response response) + protected String createResult(final HttpTransport.Response response) throws IOException { // bind the JsonNode representation of the response - final String json = response.body().string(); + final String json = response.bodyString(); final JsonNode node = this.objectMapper.readTree(json); // obtain the Execution identity to return diff --git a/spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/command/CreateNetwork.java b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/CreateNetwork.java similarity index 77% rename from spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/command/CreateNetwork.java rename to spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/CreateNetwork.java index 690f896..ddae7eb 100644 --- a/spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/command/CreateNetwork.java +++ b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/CreateNetwork.java @@ -1,8 +1,8 @@ -package build.spawn.docker.okhttp.command; +package build.spawn.docker.jdk.command; /*- * #%L - * Spawn Docker (OkHttp Client) + * Spawn Docker (JDK Client) * %% * Copyright (C) 2026 Workday, Inc. * %% @@ -21,15 +21,13 @@ */ import build.base.configuration.Configuration; +import build.spawn.docker.jdk.HttpTransport; import build.spawn.docker.option.DockerOption; import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.inject.Inject; -import okhttp3.HttpUrl; -import okhttp3.Request; -import okhttp3.RequestBody; -import okhttp3.Response; import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.util.Optional; /** @@ -71,7 +69,7 @@ public CreateNetwork(final String name, final Configuration configuration) { } @Override - protected Request createRequest(final HttpUrl.Builder httpUrlBuilder) { + protected HttpTransport.Request createRequest() { final var node = this.objectMapper.createObjectNode(); node.put("Name", this.name); node.put("CheckDuplicate", true); @@ -79,20 +77,16 @@ protected Request createRequest(final HttpUrl.Builder httpUrlBuilder) { this.configuration.stream(DockerOption.class) .forEach(option -> option.configure(node, this.objectMapper)); - return new Request.Builder() - .url(httpUrlBuilder - .addPathSegment("networks") - .addPathSegment("create") - .build()) - .post(RequestBody.create(node.toString(), MEDIA_TYPE_JSON)) - .build(); + return HttpTransport.Request + .post("/networks/create", node.toString().getBytes(StandardCharsets.UTF_8)) + .withContentType("application/json"); } @Override - protected Optional createResult(final Response response) + protected Optional createResult(final HttpTransport.Response response) throws IOException { - if (response.code() == 201) { + if (response.statusCode() == 201) { return Optional.of(this.name); } return Optional.empty(); diff --git a/spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/command/DeleteContainer.java b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/DeleteContainer.java similarity index 72% rename from spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/command/DeleteContainer.java rename to spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/DeleteContainer.java index 67b715e..978b691 100644 --- a/spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/command/DeleteContainer.java +++ b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/DeleteContainer.java @@ -1,8 +1,8 @@ -package build.spawn.docker.okhttp.command; +package build.spawn.docker.jdk.command; /*- * #%L - * Spawn Docker (OkHttp Client) + * Spawn Docker (JDK Client) * %% * Copyright (C) 2026 Workday, Inc. * %% @@ -22,11 +22,9 @@ import build.base.configuration.Configuration; import build.spawn.docker.Container; +import build.spawn.docker.jdk.HttpTransport; import build.spawn.docker.option.RemoveVolumes; import jakarta.inject.Inject; -import okhttp3.HttpUrl; -import okhttp3.Request; -import okhttp3.Response; import java.io.IOException; @@ -64,20 +62,14 @@ public DeleteContainer(final Configuration configuration) { } @Override - protected Request createRequest(final HttpUrl.Builder httpUrlBuilder) { - return new Request.Builder() - .url(httpUrlBuilder - .addPathSegment("containers") - .addPathSegment(this.container.id()) - .addQueryParameter("v", String.valueOf(this.configuration.getValue(RemoveVolumes.class))) - .addQueryParameter("force", "true") - .build()) - .delete() - .build(); + protected HttpTransport.Request createRequest() { + return HttpTransport.Request.delete( + "/containers/" + this.container.id() + + "?v=" + this.configuration.getValue(RemoveVolumes.class) + "&force=true"); } @Override - protected Void createResult(final Response response) + protected Void createResult(final HttpTransport.Response response) throws IOException { return null; diff --git a/spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/command/DeleteImage.java b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/DeleteImage.java similarity index 70% rename from spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/command/DeleteImage.java rename to spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/DeleteImage.java index dda5f54..a05bae3 100644 --- a/spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/command/DeleteImage.java +++ b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/DeleteImage.java @@ -1,8 +1,8 @@ -package build.spawn.docker.okhttp.command; +package build.spawn.docker.jdk.command; /*- * #%L - * Spawn Docker (OkHttp Client) + * Spawn Docker (JDK Client) * %% * Copyright (C) 2026 Workday, Inc. * %% @@ -22,10 +22,8 @@ import build.base.configuration.Configuration; import build.spawn.docker.Image; +import build.spawn.docker.jdk.HttpTransport; import jakarta.inject.Inject; -import okhttp3.HttpUrl; -import okhttp3.Request; -import okhttp3.Response; import java.io.IOException; @@ -55,29 +53,22 @@ public DeleteImage(final Configuration configuration) { } @Override - protected Request createRequest(final HttpUrl.Builder httpUrlBuilder) { - return new Request.Builder() - .url(httpUrlBuilder - .addPathSegment("images") - .addPathSegment(this.image.id()) - .addQueryParameter("force", "true") - .build()) - .delete() - .build(); + protected HttpTransport.Request createRequest() { + return HttpTransport.Request.delete("/images/" + this.image.id() + "?force=true"); } @Override - protected void onUnsuccessfulRequest(final Request request, final Response response) + protected void onUnsuccessfulRequest(final HttpTransport.Request request, final HttpTransport.Response response) throws IOException { // 404 (missing image) is not considered a failure - if (response.code() != 404) { + if (response.statusCode() != 404) { super.onUnsuccessfulRequest(request, response); } } @Override - protected Void createResult(final Response response) + protected Void createResult(final HttpTransport.Response response) throws IOException { return null; diff --git a/spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/command/DeleteNetwork.java b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/DeleteNetwork.java similarity index 71% rename from spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/command/DeleteNetwork.java rename to spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/DeleteNetwork.java index 09e247d..b99d485 100644 --- a/spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/command/DeleteNetwork.java +++ b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/DeleteNetwork.java @@ -1,8 +1,8 @@ -package build.spawn.docker.okhttp.command; +package build.spawn.docker.jdk.command; /*- * #%L - * Spawn Docker (OkHttp Client) + * Spawn Docker (JDK Client) * %% * Copyright (C) 2026 Workday, Inc. * %% @@ -20,11 +20,9 @@ * #L% */ +import build.spawn.docker.jdk.HttpTransport; import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.inject.Inject; -import okhttp3.HttpUrl; -import okhttp3.Request; -import okhttp3.Response; import java.io.IOException; @@ -58,19 +56,13 @@ public DeleteNetwork(final String nameOrId) { } @Override - protected Request createRequest(final HttpUrl.Builder httpUrlBuilder) { - return new Request.Builder() - .url(httpUrlBuilder - .addPathSegment("networks") - .addPathSegment(this.nameOrId) - .build()) - .delete() - .build(); + protected HttpTransport.Request createRequest() { + return HttpTransport.Request.delete("/networks/" + this.nameOrId); } @Override - protected Boolean createResult(final Response response) + protected Boolean createResult(final HttpTransport.Response response) throws IOException { - return response.code() == 204; + return response.statusCode() == 204; } } diff --git a/spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/command/FileInformation.java b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/FileInformation.java similarity index 74% rename from spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/command/FileInformation.java rename to spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/FileInformation.java index d020464..ab30485 100644 --- a/spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/command/FileInformation.java +++ b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/FileInformation.java @@ -1,8 +1,8 @@ -package build.spawn.docker.okhttp.command; +package build.spawn.docker.jdk.command; /*- * #%L - * Spawn Docker (OkHttp Client) + * Spawn Docker (JDK Client) * %% * Copyright (C) 2026 Workday, Inc. * %% @@ -21,11 +21,9 @@ */ import build.spawn.docker.Container; +import build.spawn.docker.jdk.HttpTransport; import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.inject.Inject; -import okhttp3.HttpUrl; -import okhttp3.Request; -import okhttp3.Response; import java.io.IOException; import java.nio.charset.StandardCharsets; @@ -64,23 +62,17 @@ public FileInformation(final Path filePath) { } @Override - protected Request createRequest(final HttpUrl.Builder httpUrlBuilder) { - return new Request.Builder() - .url(httpUrlBuilder - .addPathSegment("containers") - .addPathSegment(this.container.id()) - .addPathSegment("archive") - .addQueryParameter("path", this.filePath.toFile().getAbsolutePath()) - .build()) - .head() - .build(); + protected HttpTransport.Request createRequest() { + return HttpTransport.Request.head( + "/containers/" + this.container.id() + + "/archive?path=" + this.filePath.toFile().getAbsolutePath()); } @SuppressWarnings("unchecked") @Override - protected Optional> createResult(final Response response) + protected Optional> createResult(final HttpTransport.Response response) throws IOException { - if (response.code() != 200) { + if (response.statusCode() != 200) { return Optional.empty(); } final String base64Encoded = response.header("x-docker-container-path-stat"); diff --git a/spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/command/FrameProcessor.java b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/FrameProcessor.java similarity index 92% rename from spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/command/FrameProcessor.java rename to spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/FrameProcessor.java index 10b232c..9093946 100644 --- a/spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/command/FrameProcessor.java +++ b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/FrameProcessor.java @@ -1,8 +1,8 @@ -package build.spawn.docker.okhttp.command; +package build.spawn.docker.jdk.command; /*- * #%L - * Spawn Docker (OkHttp Client) + * Spawn Docker (JDK Client) * %% * Copyright (C) 2026 Workday, Inc. * %% @@ -20,10 +20,10 @@ * #L% */ -import okhttp3.Response; import java.io.EOFException; import java.io.IOException; +import java.io.InputStream; import java.io.PipedReader; import java.io.PipedWriter; import java.net.SocketException; @@ -31,7 +31,7 @@ import java.util.concurrent.CompletableFuture; /** - * Asynchronously processes the {@link Response} {@code stdin}, {@code stdout} and {@code stderr} frames produced + * Asynchronously processes the {@code stdin}, {@code stdout} and {@code stderr} frames produced * by a {@link build.spawn.docker.Container}. * * @author brian.oliver @@ -46,9 +46,9 @@ class FrameProcessor private static final int HEADER_SIZE = 8; /** - * The {@link Response} to process + * The {@link InputStream} to process. */ - private final Response response; + private final InputStream inputStream; /** * The {@link CompletableFuture} when Frame processing has completed. @@ -68,16 +68,16 @@ class FrameProcessor /** * Constructs a {@link FrameProcessor}. * - * @param response the {@link Response} to process + * @param inputStream the {@link InputStream} to process * @param outputReader the {@link PipedReader} to which stdout frames will be output * @param errorReader the {@link PipedReader} to which stderr frames will be output */ - FrameProcessor(final Response response, + FrameProcessor(final InputStream inputStream, final PipedReader outputReader, final PipedReader errorReader) throws IOException { - this.response = response; + this.inputStream = inputStream; this.processing = new CompletableFuture<>(); this.outputWriter = new PipedWriter(outputReader); this.errorWriter = new PipedWriter(errorReader); @@ -95,7 +95,7 @@ public CompletableFuture processing() { @Override public void run() { - try (var inputStream = this.response.body().byteStream()) { + try (var inputStream = this.inputStream) { while (!this.processing.isDone()) { // read the frame header final var header = new byte[HEADER_SIZE]; diff --git a/spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/command/GetSystemEvents.java b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/GetSystemEvents.java similarity index 82% rename from spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/command/GetSystemEvents.java rename to spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/GetSystemEvents.java index ceed349..942f390 100644 --- a/spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/command/GetSystemEvents.java +++ b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/GetSystemEvents.java @@ -1,8 +1,8 @@ -package build.spawn.docker.okhttp.command; +package build.spawn.docker.jdk.command; /*- * #%L - * Spawn Docker (OkHttp Client) + * Spawn Docker (JDK Client) * %% * Copyright (C) 2026 Workday, Inc. * %% @@ -24,13 +24,13 @@ import build.base.flow.Subscriber; import build.base.naming.UniqueNameGenerator; import build.spawn.docker.Event; -import build.spawn.docker.okhttp.event.ActionEvent; +import build.spawn.docker.jdk.HttpTransport; +import build.spawn.docker.jdk.event.ActionEvent; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.inject.Inject; -import okhttp3.HttpUrl; -import okhttp3.Request; -import okhttp3.Response; + +import java.io.IOException; /** * The {@code Docker Daemon} {@link Command} to request a stream of system events using the @@ -55,16 +55,12 @@ public class GetSystemEvents private Publicist publisher; @Override - protected Request createRequest(final HttpUrl.Builder httpUrlBuilder) { - return new Request.Builder() - .url(httpUrlBuilder - .addPathSegment("events") - .build()) - .build(); + protected HttpTransport.Request createRequest() { + return HttpTransport.Request.get("/events"); } @Override - protected Void createResult(final Response response) { + protected Void createResult(final HttpTransport.Response response) { // create a unique name for the GetSystemEvents thread final var uniqueNameGenerator = new UniqueNameGenerator("."); @@ -102,7 +98,11 @@ public void onComplete() { // process the entire InputStream from the Response to essentially wait for the image to be created final var processor = new JsonNodeInputStreamProcessor(this.objectMapper); - processor.process(response.body().byteStream(), jsonSubscriber); + try { + processor.process(response.bodyStream(), jsonSubscriber); + } catch (final IOException e) { + GetSystemEvents.this.onProcessingError(e); + } }; Thread.ofVirtual() @@ -113,10 +113,9 @@ public void onComplete() { } @Override - protected void onSuccessfulRequest(final Request request, - final Response response, + protected void onSuccessfulRequest(final HttpTransport.Request request, + final HttpTransport.Response response, final Void result) { - // no result to capture for a Void command — processing continues via the virtual thread above } } diff --git a/spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/command/GetSystemInformation.java b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/GetSystemInformation.java similarity index 77% rename from spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/command/GetSystemInformation.java rename to spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/GetSystemInformation.java index 4b30d6d..de1a1fd 100644 --- a/spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/command/GetSystemInformation.java +++ b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/GetSystemInformation.java @@ -1,8 +1,8 @@ -package build.spawn.docker.okhttp.command; +package build.spawn.docker.jdk.command; /*- * #%L - * Spawn Docker (OkHttp Client) + * Spawn Docker (JDK Client) * %% * Copyright (C) 2026 Workday, Inc. * %% @@ -21,13 +21,11 @@ */ import build.spawn.docker.Session; -import build.spawn.docker.okhttp.model.AbstractJsonBasedResult; +import build.spawn.docker.jdk.HttpTransport; +import build.spawn.docker.jdk.model.AbstractJsonBasedResult; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.inject.Inject; -import okhttp3.HttpUrl; -import okhttp3.Request; -import okhttp3.Response; import java.io.IOException; @@ -48,23 +46,19 @@ public class GetSystemInformation private ObjectMapper objectMapper; @Override - protected Request createRequest(final HttpUrl.Builder httpUrlBuilder) { - return new Request.Builder() - .url(httpUrlBuilder - .addPathSegment("info") - .build()) - .build(); + protected HttpTransport.Request createRequest() { + return HttpTransport.Request.get("/info"); } @Override - protected Session.Information createResult(final Response response) + protected Session.Information createResult(final HttpTransport.Response response) throws IOException { // establish a new Context to create the Result final var context = createContext(); // bind the JsonNode representation of the response - final var json = response.body().string(); + final var json = response.bodyString(); final var jsonNode = this.objectMapper.readTree(json); context.bind(JsonNode.class).to(jsonNode); diff --git a/spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/command/InputStreamProcessor.java b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/InputStreamProcessor.java similarity index 94% rename from spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/command/InputStreamProcessor.java rename to spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/InputStreamProcessor.java index c312252..92b3c87 100644 --- a/spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/command/InputStreamProcessor.java +++ b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/InputStreamProcessor.java @@ -1,8 +1,8 @@ -package build.spawn.docker.okhttp.command; +package build.spawn.docker.jdk.command; /*- * #%L - * Spawn Docker (OkHttp Client) + * Spawn Docker (JDK Client) * %% * Copyright (C) 2026 Workday, Inc. * %% diff --git a/spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/command/InspectContainer.java b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/InspectContainer.java similarity index 75% rename from spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/command/InspectContainer.java rename to spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/InspectContainer.java index 9d9b826..0e02c10 100644 --- a/spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/command/InspectContainer.java +++ b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/InspectContainer.java @@ -1,8 +1,8 @@ -package build.spawn.docker.okhttp.command; +package build.spawn.docker.jdk.command; /*- * #%L - * Spawn Docker (OkHttp Client) + * Spawn Docker (JDK Client) * %% * Copyright (C) 2026 Workday, Inc. * %% @@ -22,12 +22,10 @@ import build.codemodel.injection.Context; import build.spawn.docker.Container; +import build.spawn.docker.jdk.HttpTransport; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.inject.Inject; -import okhttp3.HttpUrl; -import okhttp3.Request; -import okhttp3.Response; import java.io.IOException; import java.util.Objects; @@ -64,31 +62,24 @@ public InspectContainer(final Container container) { } @Override - protected Request createRequest(final HttpUrl.Builder httpUrlBuilder) { - return new Request.Builder() - .url(httpUrlBuilder - .addPathSegment("containers") - .addPathSegment(this.container.id()) - .addPathSegment("json") - .build()) - .get() - .build(); + protected HttpTransport.Request createRequest() { + return HttpTransport.Request.get("/containers/" + this.container.id() + "/json"); } @Override - protected void onUnsuccessfulRequest(final Request request, final Response response) + protected void onUnsuccessfulRequest(final HttpTransport.Request request, final HttpTransport.Response response) throws IOException { - if (response.code() != 404) { + if (response.statusCode() != 404) { super.onUnsuccessfulRequest(request, response); } } @Override - protected Optional createResult(final Response response) + protected Optional createResult(final HttpTransport.Response response) throws IOException { - if (response.code() == 404) { + if (response.statusCode() == 404) { return Optional.empty(); } @@ -98,10 +89,10 @@ protected Optional createResult(final Response response) context.bind(Context.class).to(context); // bind the JsonNode representation of the response - final var json = response.body().string(); + final var json = response.bodyString(); context.bind(JsonNode.class).to(this.objectMapper.readTree(json)); // establish the Container.Information based on the Response - return Optional.of(context.create(build.spawn.docker.okhttp.model.ContainerInformation.class)); + return Optional.of(context.create(build.spawn.docker.jdk.model.ContainerInformation.class)); } } diff --git a/spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/command/InspectExecution.java b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/InspectExecution.java similarity index 75% rename from spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/command/InspectExecution.java rename to spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/InspectExecution.java index 23ddff5..e6b86e4 100644 --- a/spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/command/InspectExecution.java +++ b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/InspectExecution.java @@ -1,8 +1,8 @@ -package build.spawn.docker.okhttp.command; +package build.spawn.docker.jdk.command; /*- * #%L - * Spawn Docker (OkHttp Client) + * Spawn Docker (JDK Client) * %% * Copyright (C) 2026 Workday, Inc. * %% @@ -21,13 +21,11 @@ */ import build.spawn.docker.Execution; -import build.spawn.docker.okhttp.model.ExecutionInformation; +import build.spawn.docker.jdk.HttpTransport; +import build.spawn.docker.jdk.model.ExecutionInformation; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.inject.Inject; -import okhttp3.HttpUrl; -import okhttp3.Request; -import okhttp3.Response; import java.io.IOException; import java.util.Objects; @@ -64,30 +62,24 @@ public InspectExecution(final String id) { } @Override - protected Request createRequest(final HttpUrl.Builder httpUrlBuilder) { - return new Request.Builder() - .url(httpUrlBuilder - .addPathSegment("exec") - .addPathSegment(this.id) - .addPathSegment("json") - .build()) - .build(); + protected HttpTransport.Request createRequest() { + return HttpTransport.Request.get("/exec/" + this.id + "/json"); } @Override - protected void onUnsuccessfulRequest(final Request request, final Response response) + protected void onUnsuccessfulRequest(final HttpTransport.Request request, final HttpTransport.Response response) throws IOException { - if (response.code() != 404) { + if (response.statusCode() != 404) { super.onUnsuccessfulRequest(request, response); } } @Override - protected Optional createResult(final Response response) + protected Optional createResult(final HttpTransport.Response response) throws IOException { - if (response.code() == 404) { + if (response.statusCode() == 404) { return Optional.empty(); } @@ -95,7 +87,7 @@ protected Optional createResult(final Response response) final var context = createContext(); // bind the JsonNode representation of the response - final var json = response.body().string(); + final var json = response.bodyString(); context.bind(JsonNode.class).to(this.objectMapper.readTree(json)); return Optional.of(context.create(ExecutionInformation.class)); diff --git a/spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/command/InspectImage.java b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/InspectImage.java similarity index 77% rename from spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/command/InspectImage.java rename to spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/InspectImage.java index 3f4d0f1..b695049 100644 --- a/spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/command/InspectImage.java +++ b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/InspectImage.java @@ -1,8 +1,8 @@ -package build.spawn.docker.okhttp.command; +package build.spawn.docker.jdk.command; /*- * #%L - * Spawn Docker (OkHttp Client) + * Spawn Docker (JDK Client) * %% * Copyright (C) 2026 Workday, Inc. * %% @@ -22,14 +22,12 @@ import build.base.configuration.Configuration; import build.spawn.docker.Image; -import build.spawn.docker.okhttp.model.ImageInformation; +import build.spawn.docker.jdk.HttpTransport; +import build.spawn.docker.jdk.model.ImageInformation; import build.spawn.docker.option.ImageName; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.inject.Inject; -import okhttp3.HttpUrl; -import okhttp3.Request; -import okhttp3.Response; import java.io.IOException; import java.util.Objects; @@ -84,33 +82,25 @@ public InspectImage(final String nameOrId) { } @Override - protected Request createRequest(final HttpUrl.Builder httpUrlBuilder) { + protected HttpTransport.Request createRequest() { final var registryName = ImageName.namesWithDockerRegistry(this.nameOrId, this.configuration); - httpUrlBuilder.addPathSegment("images"); - registryName.forEach(httpUrlBuilder::addPathSegment); - - final var httpUrl = httpUrlBuilder - .addPathSegment("json") - .build(); - - return new Request.Builder() - .url(httpUrl) - .build(); + final var path = "/images/" + String.join("/", registryName) + "/json"; + return HttpTransport.Request.get(path); } @Override - protected void onUnsuccessfulRequest(final Request request, final Response response) + protected void onUnsuccessfulRequest(final HttpTransport.Request request, final HttpTransport.Response response) throws IOException { - if (response.code() != 404) { + if (response.statusCode() != 404) { super.onUnsuccessfulRequest(request, response); } } @Override - protected Optional createResult(final Response response) + protected Optional createResult(final HttpTransport.Response response) throws IOException { - if (response.code() == 404) { + if (response.statusCode() == 404) { return Optional.empty(); } @@ -118,7 +108,7 @@ protected Optional createResult(final Response response) final var context = createContext(); // bind the JsonNode representation of the response - final String json = response.body().string(); + final String json = response.bodyString(); context.bind(JsonNode.class).to(this.objectMapper.readTree(json)); return Optional.of(context.create(ImageInformation.class)); diff --git a/spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/command/InspectNetwork.java b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/InspectNetwork.java similarity index 72% rename from spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/command/InspectNetwork.java rename to spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/InspectNetwork.java index 89d077f..cc69815 100644 --- a/spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/command/InspectNetwork.java +++ b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/InspectNetwork.java @@ -1,8 +1,8 @@ -package build.spawn.docker.okhttp.command; +package build.spawn.docker.jdk.command; /*- * #%L - * Spawn Docker (OkHttp Client) + * Spawn Docker (JDK Client) * %% * Copyright (C) 2026 Workday, Inc. * %% @@ -21,13 +21,11 @@ */ import build.spawn.docker.Network; -import build.spawn.docker.okhttp.model.NetworkInformation; +import build.spawn.docker.jdk.HttpTransport; +import build.spawn.docker.jdk.model.NetworkInformation; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.inject.Inject; -import okhttp3.HttpUrl; -import okhttp3.Request; -import okhttp3.Response; import java.io.IOException; import java.util.Optional; @@ -59,20 +57,14 @@ public InspectNetwork(final String nameOrId) { private ObjectMapper objectMapper; @Override - protected Request createRequest(final HttpUrl.Builder httpUrlBuilder) { - return new Request.Builder() - .url(httpUrlBuilder - .addPathSegment("networks") - .addPathSegment(this.nameOrId) - .build()) - .get() - .build(); + protected HttpTransport.Request createRequest() { + return HttpTransport.Request.get("/networks/" + this.nameOrId); } @Override - protected Optional createResult(final Response response) + protected Optional createResult(final HttpTransport.Response response) throws IOException { - if (response.code() == 404) { + if (response.statusCode() == 404) { return Optional.empty(); } @@ -80,7 +72,7 @@ protected Optional createResult(final Response response) final var context = createContext(); // bind the JsonNode representation of the response - final var json = response.body().string(); + final var json = response.bodyString(); context.bind(JsonNode.class).to(this.objectMapper.readTree(json)); return Optional.of(context.create(NetworkInformation.class)); diff --git a/spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/command/JsonNodeInputStreamProcessor.java b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/JsonNodeInputStreamProcessor.java similarity index 98% rename from spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/command/JsonNodeInputStreamProcessor.java rename to spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/JsonNodeInputStreamProcessor.java index 61dc0ec..34dde1c 100644 --- a/spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/command/JsonNodeInputStreamProcessor.java +++ b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/JsonNodeInputStreamProcessor.java @@ -1,8 +1,8 @@ -package build.spawn.docker.okhttp.command; +package build.spawn.docker.jdk.command; /*- * #%L - * Spawn Docker (OkHttp Client) + * Spawn Docker (JDK Client) * %% * Copyright (C) 2026 Workday, Inc. * %% diff --git a/spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/command/KillContainer.java b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/KillContainer.java similarity index 71% rename from spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/command/KillContainer.java rename to spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/KillContainer.java index 1ca9740..53519dd 100644 --- a/spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/command/KillContainer.java +++ b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/KillContainer.java @@ -1,8 +1,8 @@ -package build.spawn.docker.okhttp.command; +package build.spawn.docker.jdk.command; /*- * #%L - * Spawn Docker (OkHttp Client) + * Spawn Docker (JDK Client) * %% * Copyright (C) 2026 Workday, Inc. * %% @@ -22,11 +22,9 @@ import build.base.configuration.Configuration; import build.spawn.docker.Container; +import build.spawn.docker.jdk.HttpTransport; import build.spawn.docker.option.KillSignal; import jakarta.inject.Inject; -import okhttp3.HttpUrl; -import okhttp3.Request; -import okhttp3.Response; import java.io.IOException; @@ -63,33 +61,25 @@ public KillContainer(final Configuration configuration) { } @Override - protected Request createRequest(final HttpUrl.Builder httpUrlBuilder) { + protected HttpTransport.Request createRequest() { 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(); + return HttpTransport.Request.post( + "/containers/" + this.container.id() + "/kill?signal=" + signal.signalName(), null); } @Override - protected void onUnsuccessfulRequest(final Request request, final Response response) + protected void onUnsuccessfulRequest(final HttpTransport.Request request, final HttpTransport.Response response) throws IOException { // a response of 204 means success, 404 or 409 means the Container doesn't exist or isn't running - if (response.code() != 204 && response.code() != 404 && response.code() != 409) { + if (response.statusCode() != 204 && response.statusCode() != 404 && response.statusCode() != 409) { super.onUnsuccessfulRequest(request, response); } } @Override - protected Container createResult(final Response response) { + protected Container createResult(final HttpTransport.Response response) { return this.container; } } diff --git a/spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/command/PauseContainer.java b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/PauseContainer.java similarity index 71% rename from spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/command/PauseContainer.java rename to spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/PauseContainer.java index 5497c08..4cffa06 100644 --- a/spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/command/PauseContainer.java +++ b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/PauseContainer.java @@ -1,8 +1,8 @@ -package build.spawn.docker.okhttp.command; +package build.spawn.docker.jdk.command; /*- * #%L - * Spawn Docker (OkHttp Client) + * Spawn Docker (JDK Client) * %% * Copyright (C) 2026 Workday, Inc. * %% @@ -23,10 +23,8 @@ import build.base.flow.CompletingSubscriber; import build.spawn.docker.Container; import build.spawn.docker.Event; +import build.spawn.docker.jdk.HttpTransport; import jakarta.inject.Inject; -import okhttp3.HttpUrl; -import okhttp3.Request; -import okhttp3.Response; /** * The {@code Docker Engine} {@link Command} to pause a {@code Container} using the @@ -51,20 +49,14 @@ public class PauseContainer private CompletingSubscriber eventSubscriber; @Override - protected Request createRequest(final HttpUrl.Builder httpUrlBuilder) { + protected HttpTransport.Request createRequest() { - return new Request.Builder() - .url(httpUrlBuilder - .addPathSegment("containers") - .addPathSegment(this.container.id()) - .addPathSegment("pause") - .build()) - .post(EMPTY_BODY) - .build(); + return HttpTransport.Request.post( + "/containers/" + this.container.id() + "/pause", null); } @Override - protected Container createResult(final Response response) { + protected Container createResult(final HttpTransport.Response response) { return this.container; } } diff --git a/spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/command/Ping.java b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/Ping.java similarity index 67% rename from spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/command/Ping.java rename to spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/Ping.java index 960ea52..8d915a2 100644 --- a/spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/command/Ping.java +++ b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/Ping.java @@ -1,17 +1,17 @@ -package build.spawn.docker.okhttp.command; +package build.spawn.docker.jdk.command; /*- * #%L - * Spawn Docker (OkHttp Client) + * Spawn Docker (JDK 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. @@ -20,9 +20,7 @@ * #L% */ -import okhttp3.HttpUrl; -import okhttp3.Request; -import okhttp3.Response; +import build.spawn.docker.jdk.HttpTransport; /** * The {@code Docker Daemon} {@link Command} to execute a @@ -35,17 +33,12 @@ public class Ping extends AbstractBlockingCommand { @Override - protected Request createRequest(final HttpUrl.Builder httpUrlBuilder) { - - return new Request.Builder() - .url(httpUrlBuilder - .addPathSegment("_ping") - .build()) - .build(); + protected HttpTransport.Request createRequest() { + return HttpTransport.Request.get("/_ping"); } @Override - protected Void createResult(final Response response) { + protected Void createResult(final HttpTransport.Response response) { return null; } } diff --git a/spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/command/PullImage.java b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/PullImage.java similarity index 79% rename from spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/command/PullImage.java rename to spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/PullImage.java index 33a6334..26b0b0b 100644 --- a/spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/command/PullImage.java +++ b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/PullImage.java @@ -1,8 +1,8 @@ -package build.spawn.docker.okhttp.command; +package build.spawn.docker.jdk.command; /*- * #%L - * Spawn Docker (OkHttp Client) + * Spawn Docker (JDK Client) * %% * Copyright (C) 2026 Workday, Inc. * %% @@ -24,16 +24,14 @@ import build.base.flow.CompletingSubscriber; import build.base.flow.Publisher; import build.spawn.docker.Event; -import build.spawn.docker.okhttp.Authenticator; -import build.spawn.docker.okhttp.event.ActionEvent; +import build.spawn.docker.jdk.Authenticator; +import build.spawn.docker.jdk.HttpTransport; +import build.spawn.docker.jdk.event.ActionEvent; import build.spawn.docker.option.ImageName; import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.inject.Inject; -import okhttp3.FormBody; -import okhttp3.HttpUrl; -import okhttp3.Request; -import okhttp3.Response; +import java.nio.charset.StandardCharsets; import java.util.Optional; import java.util.concurrent.CompletableFuture; @@ -89,22 +87,18 @@ public PullImage(final String nameOrId, final Configuration configuration) { } @Override - protected Request createRequest(final HttpUrl.Builder httpUrlBuilder) { + protected HttpTransport.Request createRequest() { final var names = ImageName.namesWithDockerRegistry(this.nameOrId, this.configuration); final var imageName = String.join("/", names); - return this.authenticator.apply(new Request.Builder() - .url(httpUrlBuilder - .addPathSegment("images") - .addPathSegment("create") - .build()) - .post(new FormBody.Builder() - .add("fromImage", imageName) - .build())) - .build(); + final var body = ("fromImage=" + imageName).getBytes(StandardCharsets.UTF_8); + return this.authenticator.apply( + HttpTransport.Request + .post("/images/create", body) + .withContentType("application/x-www-form-urlencoded")); } @Override - protected Optional createResult(final Response response) { + protected Optional createResult(final HttpTransport.Response response) { return Optional.of(this.nameOrId); } diff --git a/spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/command/StartContainer.java b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/StartContainer.java similarity index 75% rename from spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/command/StartContainer.java rename to spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/StartContainer.java index 80752a7..0395d8d 100644 --- a/spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/command/StartContainer.java +++ b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/StartContainer.java @@ -1,8 +1,8 @@ -package build.spawn.docker.okhttp.command; +package build.spawn.docker.jdk.command; /*- * #%L - * Spawn Docker (OkHttp Client) + * Spawn Docker (JDK Client) * %% * Copyright (C) 2026 Workday, Inc. * %% @@ -23,10 +23,8 @@ import build.base.flow.CompletingSubscriber; import build.spawn.docker.Container; import build.spawn.docker.Event; +import build.spawn.docker.jdk.HttpTransport; import jakarta.inject.Inject; -import okhttp3.HttpUrl; -import okhttp3.Request; -import okhttp3.Response; import java.util.Objects; @@ -61,19 +59,13 @@ public StartContainer(final Container container) { } @Override - protected Request createRequest(final HttpUrl.Builder httpUrlBuilder) { - return new Request.Builder() - .url(httpUrlBuilder - .addPathSegment("containers") - .addPathSegment(this.container.id()) - .addPathSegment("start") - .build()) - .post(EMPTY_BODY) - .build(); + protected HttpTransport.Request createRequest() { + return HttpTransport.Request.post( + "/containers/" + this.container.id() + "/start", null); } @Override - protected Container createResult(final Response response) { + protected Container createResult(final HttpTransport.Response response) { return this.container; } } diff --git a/spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/command/StartExecution.java b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/StartExecution.java similarity index 89% rename from spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/command/StartExecution.java rename to spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/StartExecution.java index 1b28f95..8dda4f1 100644 --- a/spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/command/StartExecution.java +++ b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/StartExecution.java @@ -1,8 +1,8 @@ -package build.spawn.docker.okhttp.command; +package build.spawn.docker.jdk.command; /*- * #%L - * Spawn Docker (OkHttp Client) + * Spawn Docker (JDK Client) * %% * Copyright (C) 2026 Workday, Inc. * %% @@ -25,17 +25,15 @@ import build.base.io.Terminal; import build.spawn.docker.Container; import build.spawn.docker.Execution; +import build.spawn.docker.jdk.HttpTransport; import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.inject.Inject; -import okhttp3.HttpUrl; -import okhttp3.Request; -import okhttp3.RequestBody; -import okhttp3.Response; import java.io.IOException; import java.io.PipedReader; import java.io.Reader; import java.io.Writer; +import java.nio.charset.StandardCharsets; import java.util.Objects; import java.util.Optional; import java.util.concurrent.CompletableFuture; @@ -97,25 +95,20 @@ public StartExecution(final Container container, } @Override - protected Request createRequest(final HttpUrl.Builder httpUrlBuilder) { + protected HttpTransport.Request createRequest() { // establish an ObjectNode containing the containers/create json final var node = this.objectMapper.createObjectNode(); node.put("Detach", !this.terminalRequired); node.put("Tty", false); - return new Request.Builder() - .url(httpUrlBuilder - .addPathSegment("exec") - .addPathSegment(this.id) - .addPathSegment("start") - .build()) - .post(RequestBody.create(node.toString(), MEDIA_TYPE_JSON)) - .build(); + return HttpTransport.Request + .post("/exec/" + this.id + "/start", node.toString().getBytes(StandardCharsets.UTF_8)) + .withContentType("application/json"); } @Override - protected Execution createResult(final Response response) + protected Execution createResult(final HttpTransport.Response response) throws IOException { // establish PipedReaders for stdout and stderr @@ -153,7 +146,7 @@ public CompletableFuture onClosed() { }; // establish a FrameProcessor to redirect the I/O frames to the Terminal - final var processor = new FrameProcessor(response, outputReader, errorReader); + final var processor = new FrameProcessor(response.bodyStream(), outputReader, errorReader); processor.processing() .whenComplete((_, error) -> { diff --git a/spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/command/StopContainer.java b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/StopContainer.java similarity index 70% rename from spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/command/StopContainer.java rename to spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/StopContainer.java index 35d7b4e..f731397 100644 --- a/spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/command/StopContainer.java +++ b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/StopContainer.java @@ -1,8 +1,8 @@ -package build.spawn.docker.okhttp.command; +package build.spawn.docker.jdk.command; /*- * #%L - * Spawn Docker (OkHttp Client) + * Spawn Docker (JDK Client) * %% * Copyright (C) 2026 Workday, Inc. * %% @@ -23,10 +23,8 @@ import build.base.configuration.Configuration; import build.base.option.Timeout; import build.spawn.docker.Container; +import build.spawn.docker.jdk.HttpTransport; import jakarta.inject.Inject; -import okhttp3.HttpUrl; -import okhttp3.Request; -import okhttp3.Response; import java.io.IOException; @@ -63,35 +61,25 @@ public StopContainer(final Configuration configuration) { } @Override - protected Request createRequest(final HttpUrl.Builder httpUrlBuilder) { + protected HttpTransport.Request createRequest() { - httpUrlBuilder - .addPathSegment("containers") - .addPathSegment(this.container.id()) - .addPathSegment("stop"); - - // include the Timeout before killing final Timeout timeout = this.configuration.get(Timeout.class); - httpUrlBuilder.addQueryParameter("t", Long.toString(timeout.get().getSeconds())); - - return new Request.Builder() - .url(httpUrlBuilder.build()) - .post(EMPTY_BODY) - .build(); + return HttpTransport.Request.post( + "/containers/" + this.container.id() + "/stop?t=" + timeout.get().getSeconds(), null); } @Override - protected void onUnsuccessfulRequest(final Request request, final Response response) + protected void onUnsuccessfulRequest(final HttpTransport.Request request, final HttpTransport.Response response) throws IOException { // a response of 304 or 404 means the Container is already stopped or doesn't exist - if (response.code() != 304 && response.code() != 404) { + if (response.statusCode() != 304 && response.statusCode() != 404) { super.onUnsuccessfulRequest(request, response); } } @Override - protected Container createResult(final Response response) { + protected Container createResult(final HttpTransport.Response response) { return this.container; } } diff --git a/spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/command/UnpauseContainer.java b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/UnpauseContainer.java similarity index 71% rename from spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/command/UnpauseContainer.java rename to spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/UnpauseContainer.java index fac674b..d70b121 100644 --- a/spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/command/UnpauseContainer.java +++ b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/UnpauseContainer.java @@ -1,8 +1,8 @@ -package build.spawn.docker.okhttp.command; +package build.spawn.docker.jdk.command; /*- * #%L - * Spawn Docker (OkHttp Client) + * Spawn Docker (JDK Client) * %% * Copyright (C) 2026 Workday, Inc. * %% @@ -23,10 +23,8 @@ import build.base.flow.CompletingSubscriber; import build.spawn.docker.Container; import build.spawn.docker.Event; +import build.spawn.docker.jdk.HttpTransport; import jakarta.inject.Inject; -import okhttp3.HttpUrl; -import okhttp3.Request; -import okhttp3.Response; /** * The {@code Docker Engine} {@link Command} to unpause a {@code Container} using the @@ -51,20 +49,14 @@ public class UnpauseContainer private CompletingSubscriber eventSubscriber; @Override - protected Request createRequest(final HttpUrl.Builder httpUrlBuilder) { + protected HttpTransport.Request createRequest() { - return new Request.Builder() - .url(httpUrlBuilder - .addPathSegment("containers") - .addPathSegment(this.container.id()) - .addPathSegment("unpause") - .build()) - .post(EMPTY_BODY) - .build(); + return HttpTransport.Request.post( + "/containers/" + this.container.id() + "/unpause", null); } @Override - protected Container createResult(final Response response) { + protected Container createResult(final HttpTransport.Response response) { return this.container; } } diff --git a/spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/event/AbstractEvent.java b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/event/AbstractEvent.java similarity index 89% rename from spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/event/AbstractEvent.java rename to spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/event/AbstractEvent.java index 079f4ba..1d3c1b2 100644 --- a/spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/event/AbstractEvent.java +++ b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/event/AbstractEvent.java @@ -1,8 +1,8 @@ -package build.spawn.docker.okhttp.event; +package build.spawn.docker.jdk.event; /*- * #%L - * Spawn Docker (OkHttp Client) + * Spawn Docker (JDK Client) * %% * Copyright (C) 2026 Workday, Inc. * %% @@ -21,7 +21,7 @@ */ import build.spawn.docker.Event; -import build.spawn.docker.okhttp.model.AbstractJsonBasedResult; +import build.spawn.docker.jdk.model.AbstractJsonBasedResult; import com.fasterxml.jackson.databind.JsonNode; import java.util.Optional; diff --git a/spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/event/ActionEvent.java b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/event/ActionEvent.java similarity index 94% rename from spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/event/ActionEvent.java rename to spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/event/ActionEvent.java index 79d9c51..fca992f 100644 --- a/spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/event/ActionEvent.java +++ b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/event/ActionEvent.java @@ -1,8 +1,8 @@ -package build.spawn.docker.okhttp.event; +package build.spawn.docker.jdk.event; /*- * #%L - * Spawn Docker (OkHttp Client) + * Spawn Docker (JDK Client) * %% * Copyright (C) 2026 Workday, Inc. * %% diff --git a/spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/model/AbstractJsonBasedResult.java b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/model/AbstractJsonBasedResult.java similarity index 94% rename from spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/model/AbstractJsonBasedResult.java rename to spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/model/AbstractJsonBasedResult.java index a7b0665..cfe2c9c 100644 --- a/spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/model/AbstractJsonBasedResult.java +++ b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/model/AbstractJsonBasedResult.java @@ -1,8 +1,8 @@ -package build.spawn.docker.okhttp.model; +package build.spawn.docker.jdk.model; /*- * #%L - * Spawn Docker (OkHttp Client) + * Spawn Docker (JDK Client) * %% * Copyright (C) 2026 Workday, Inc. * %% @@ -21,7 +21,7 @@ */ import build.spawn.docker.Session; -import build.spawn.docker.okhttp.command.Command; +import build.spawn.docker.jdk.command.Command; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.inject.Inject; diff --git a/spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/model/ContainerInformation.java b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/model/ContainerInformation.java similarity index 98% rename from spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/model/ContainerInformation.java rename to spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/model/ContainerInformation.java index 5521373..0c698a2 100644 --- a/spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/model/ContainerInformation.java +++ b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/model/ContainerInformation.java @@ -1,8 +1,8 @@ -package build.spawn.docker.okhttp.model; +package build.spawn.docker.jdk.model; /*- * #%L - * Spawn Docker (OkHttp Client) + * Spawn Docker (JDK Client) * %% * Copyright (C) 2026 Workday, Inc. * %% diff --git a/spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/model/OkHttpBasedContainer.java b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/model/DockerContainer.java similarity index 90% rename from spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/model/OkHttpBasedContainer.java rename to spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/model/DockerContainer.java index b3d93b3..ee58f36 100644 --- a/spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/model/OkHttpBasedContainer.java +++ b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/model/DockerContainer.java @@ -1,8 +1,8 @@ -package build.spawn.docker.okhttp.model; +package build.spawn.docker.jdk.model; /*- * #%L - * Spawn Docker (OkHttp Client) + * Spawn Docker (JDK Client) * %% * Copyright (C) 2026 Workday, Inc. * %% @@ -35,18 +35,18 @@ import build.spawn.docker.Executable; import build.spawn.docker.Execution; import build.spawn.docker.Image; -import build.spawn.docker.okhttp.command.AttachContainer; -import build.spawn.docker.okhttp.command.CopyFiles; -import build.spawn.docker.okhttp.command.CreateExecution; -import build.spawn.docker.okhttp.command.DeleteContainer; -import build.spawn.docker.okhttp.command.FileInformation; -import build.spawn.docker.okhttp.command.InspectContainer; -import build.spawn.docker.okhttp.command.KillContainer; -import build.spawn.docker.okhttp.command.PauseContainer; -import build.spawn.docker.okhttp.command.StartExecution; -import build.spawn.docker.okhttp.command.StopContainer; -import build.spawn.docker.okhttp.command.UnpauseContainer; -import build.spawn.docker.okhttp.event.ActionEvent; +import build.spawn.docker.jdk.command.AttachContainer; +import build.spawn.docker.jdk.command.CopyFiles; +import build.spawn.docker.jdk.command.CreateExecution; +import build.spawn.docker.jdk.command.DeleteContainer; +import build.spawn.docker.jdk.command.FileInformation; +import build.spawn.docker.jdk.command.InspectContainer; +import build.spawn.docker.jdk.command.KillContainer; +import build.spawn.docker.jdk.command.PauseContainer; +import build.spawn.docker.jdk.command.StartExecution; +import build.spawn.docker.jdk.command.StopContainer; +import build.spawn.docker.jdk.command.UnpauseContainer; +import build.spawn.docker.jdk.event.ActionEvent; import build.spawn.docker.option.Command; import jakarta.inject.Inject; @@ -62,7 +62,7 @@ * @author brian.oliver * @since Jun-2021 */ -public class OkHttpBasedContainer +public class DockerContainer extends AbstractJsonBasedResult implements Container { @@ -254,12 +254,12 @@ public Execution execute() { // attempt to create a Docker exec instance final var id = createContext() - .inject(new CreateExecution(OkHttpBasedContainer.this, terminalRequired, configuration)) + .inject(new CreateExecution(DockerContainer.this, terminalRequired, configuration)) .submit(); // attempt to start the exec instance return createContext() - .inject(new StartExecution(OkHttpBasedContainer.this, id, terminalRequired, configuration)) + .inject(new StartExecution(DockerContainer.this, id, terminalRequired, configuration)) .submit(); } }; diff --git a/spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/model/OkHttpBasedImage.java b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/model/DockerImage.java similarity index 87% rename from spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/model/OkHttpBasedImage.java rename to spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/model/DockerImage.java index 70a25fa..aa2b841 100644 --- a/spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/model/OkHttpBasedImage.java +++ b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/model/DockerImage.java @@ -1,8 +1,8 @@ -package build.spawn.docker.okhttp.model; +package build.spawn.docker.jdk.model; /*- * #%L - * Spawn Docker (OkHttp Client) + * Spawn Docker (JDK Client) * %% * Copyright (C) 2026 Workday, Inc. * %% @@ -24,10 +24,10 @@ import build.codemodel.injection.Context; import build.spawn.docker.Container; import build.spawn.docker.Image; -import build.spawn.docker.okhttp.command.CreateContainer; -import build.spawn.docker.okhttp.command.DeleteImage; -import build.spawn.docker.okhttp.command.InspectImage; -import build.spawn.docker.okhttp.command.StartContainer; +import build.spawn.docker.jdk.command.CreateContainer; +import build.spawn.docker.jdk.command.DeleteImage; +import build.spawn.docker.jdk.command.InspectImage; +import build.spawn.docker.jdk.command.StartContainer; import build.spawn.docker.option.ImageName; import com.fasterxml.jackson.databind.JsonNode; import jakarta.inject.Inject; @@ -45,7 +45,7 @@ * @author brian.oliver * @since Jun-2021 */ -public class OkHttpBasedImage +public class DockerImage implements Image { /** @@ -60,17 +60,17 @@ public class OkHttpBasedImage private final String id; /** - * Constructs an {@link OkHttpBasedImage} with the specified identity. + * Constructs an {@link DockerImage} with the specified identity. * * @param id the {@link Image} identity */ - public OkHttpBasedImage(final String id) { + public DockerImage(final String id) { this.id = Objects.requireNonNull(id, "The Image identity must not be null"); } /** * Create a {@link Context} that can be used by the {@link Image} to create - * {@link build.spawn.docker.okhttp.command.Command}s. + * {@link build.spawn.docker.jdk.command.Command}s. * * @return the new {@link Context} */ diff --git a/spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/model/ExecutionInformation.java b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/model/ExecutionInformation.java similarity index 95% rename from spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/model/ExecutionInformation.java rename to spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/model/ExecutionInformation.java index 6807723..a1cd466 100644 --- a/spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/model/ExecutionInformation.java +++ b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/model/ExecutionInformation.java @@ -1,8 +1,8 @@ -package build.spawn.docker.okhttp.model; +package build.spawn.docker.jdk.model; /*- * #%L - * Spawn Docker (OkHttp Client) + * Spawn Docker (JDK Client) * %% * Copyright (C) 2026 Workday, Inc. * %% diff --git a/spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/model/ImageInformation.java b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/model/ImageInformation.java similarity index 95% rename from spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/model/ImageInformation.java rename to spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/model/ImageInformation.java index aba126e..676966c 100644 --- a/spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/model/ImageInformation.java +++ b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/model/ImageInformation.java @@ -1,8 +1,8 @@ -package build.spawn.docker.okhttp.model; +package build.spawn.docker.jdk.model; /*- * #%L - * Spawn Docker (OkHttp Client) + * Spawn Docker (JDK Client) * %% * Copyright (C) 2026 Workday, Inc. * %% diff --git a/spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/model/NetworkInformation.java b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/model/NetworkInformation.java similarity index 94% rename from spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/model/NetworkInformation.java rename to spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/model/NetworkInformation.java index 0253d10..7fab59d 100644 --- a/spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/model/NetworkInformation.java +++ b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/model/NetworkInformation.java @@ -1,8 +1,8 @@ -package build.spawn.docker.okhttp.model; +package build.spawn.docker.jdk.model; /*- * #%L - * Spawn Docker (OkHttp Client) + * Spawn Docker (JDK Client) * %% * Copyright (C) 2026 Workday, Inc. * %% diff --git a/spawn-docker-okhttp/src/main/java/module-info.java b/spawn-docker-jdk/src/main/java/module-info.java similarity index 69% rename from spawn-docker-okhttp/src/main/java/module-info.java rename to spawn-docker-jdk/src/main/java/module-info.java index b879c1b..a83309f 100644 --- a/spawn-docker-okhttp/src/main/java/module-info.java +++ b/spawn-docker-jdk/src/main/java/module-info.java @@ -1,6 +1,6 @@ /*- * #%L - * Spawn Docker (OkHttp Client) + * Spawn Docker (JDK Client) * %% * Copyright (C) 2026 Workday, Inc. * %% @@ -23,7 +23,7 @@ * @author brian.oliver * @since Nov-2024 */ -open module build.spawn.docker.okhttp { +open module build.spawn.docker.jdk { requires transitive build.spawn.option; requires transitive build.spawn.docker; @@ -33,10 +33,9 @@ requires transitive build.base.naming; - requires okhttp3; - requires kotlin.stdlib; // this is to prevent okhttp3 deprecation errors in the compiler + requires java.net.http; + requires jdk.httpserver; requires jakarta.inject; - requires org.newsclub.net.unix; requires build.base.io; requires com.fasterxml.jackson.databind; requires build.base.archiving; @@ -44,11 +43,11 @@ requires build.base.flow; requires java.logging; - exports build.spawn.docker.okhttp; + exports build.spawn.docker.jdk; provides build.spawn.docker.Session.Factory with - build.spawn.docker.okhttp.UnixDomainSocketBasedSession.Factory, - build.spawn.docker.okhttp.LocalHostBasedSessionFactory, - build.spawn.docker.okhttp.DockerHostVariableBasedSessionFactory, - build.spawn.docker.okhttp.InternalDockerHostBasedSessionFactory; + build.spawn.docker.jdk.UnixDomainSocketBasedSession.Factory, + build.spawn.docker.jdk.LocalHostBasedSessionFactory, + build.spawn.docker.jdk.DockerHostVariableBasedSessionFactory, + build.spawn.docker.jdk.InternalDockerHostBasedSessionFactory; } diff --git a/spawn-docker-jdk/src/main/resources/META-INF/services/build.spawn.docker.Session$Factory b/spawn-docker-jdk/src/main/resources/META-INF/services/build.spawn.docker.Session$Factory new file mode 100644 index 0000000..45be446 --- /dev/null +++ b/spawn-docker-jdk/src/main/resources/META-INF/services/build.spawn.docker.Session$Factory @@ -0,0 +1,4 @@ +build.spawn.docker.jdk.UnixDomainSocketBasedSession$Factory +build.spawn.docker.jdk.LocalHostBasedSessionFactory +build.spawn.docker.jdk.DockerHostVariableBasedSessionFactory +build.spawn.docker.jdk.InternalDockerHostBasedSessionFactory diff --git a/spawn-docker-okhttp/src/test/java/build/spawn/docker/okhttp/CompletableFuturesNamingTests.java b/spawn-docker-jdk/src/test/java/build/spawn/docker/jdk/CompletableFuturesNamingTests.java similarity index 75% rename from spawn-docker-okhttp/src/test/java/build/spawn/docker/okhttp/CompletableFuturesNamingTests.java rename to spawn-docker-jdk/src/test/java/build/spawn/docker/jdk/CompletableFuturesNamingTests.java index cf10247..5b3a284 100644 --- a/spawn-docker-okhttp/src/test/java/build/spawn/docker/okhttp/CompletableFuturesNamingTests.java +++ b/spawn-docker-jdk/src/test/java/build/spawn/docker/jdk/CompletableFuturesNamingTests.java @@ -1,11 +1,11 @@ -package build.spawn.docker.okhttp; +package build.spawn.docker.jdk; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThatCode; /** - * Tests that verify the naming of utility classes in {@code build.spawn.docker.okhttp}. + * Tests that verify the naming of utility classes in {@code build.spawn.docker.jdk}. * * @author reed.vonredwitz * @since Mar-2026 @@ -17,7 +17,7 @@ class CompletableFuturesNamingTests { */ @Test void classNameShouldBeSpelledCorrectly() { - assertThatCode(() -> Class.forName("build.spawn.docker.okhttp.CompletableFutures")) + assertThatCode(() -> Class.forName("build.spawn.docker.jdk.CompletableFutures")) .doesNotThrowAnyException(); } } diff --git a/spawn-docker-jdk/src/test/java/build/spawn/docker/jdk/Http11ParserTest.java b/spawn-docker-jdk/src/test/java/build/spawn/docker/jdk/Http11ParserTest.java new file mode 100644 index 0000000..a617e94 --- /dev/null +++ b/spawn-docker-jdk/src/test/java/build/spawn/docker/jdk/Http11ParserTest.java @@ -0,0 +1,56 @@ +package build.spawn.docker.jdk; + +import org.junit.jupiter.api.Test; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +import static org.assertj.core.api.Assertions.assertThat; + +class Http11ParserTest { + + @Test + void shouldParseStatusCode200() throws IOException { + final var raw = "HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n"; + final var response = Http11Parser.parse(stream(raw), () -> {}); + assertThat(response.statusCode()).isEqualTo(200); + response.close(); + } + + @Test + void shouldParseBodyWithContentLength() throws IOException { + final var raw = "HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\nhello"; + final var response = Http11Parser.parse(stream(raw), () -> {}); + assertThat(response.bodyString()).isEqualTo("hello"); + response.close(); + } + + @Test + void shouldParseChunkedBody() throws IOException { + final var raw = "HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n5\r\nhello\r\n0\r\n\r\n"; + final var response = Http11Parser.parse(stream(raw), () -> {}); + assertThat(response.bodyString()).isEqualTo("hello"); + response.close(); + } + + @Test + void shouldParse204NoContent() throws IOException { + final var raw = "HTTP/1.1 204 No Content\r\n\r\n"; + final var response = Http11Parser.parse(stream(raw), () -> {}); + assertThat(response.statusCode()).isEqualTo(204); + response.close(); + } + + @Test + void shouldParseMultiChunkBody() throws IOException { + final var raw = "HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n3\r\nabc\r\n3\r\ndef\r\n0\r\n\r\n"; + final var response = Http11Parser.parse(stream(raw), () -> {}); + assertThat(response.bodyString()).isEqualTo("abcdef"); + response.close(); + } + + private static ByteArrayInputStream stream(final String s) { + return new ByteArrayInputStream(s.getBytes(StandardCharsets.UTF_8)); + } +} diff --git a/spawn-docker-jdk/src/test/java/build/spawn/docker/jdk/JavaHttpClientTransportTest.java b/spawn-docker-jdk/src/test/java/build/spawn/docker/jdk/JavaHttpClientTransportTest.java new file mode 100644 index 0000000..15f17e0 --- /dev/null +++ b/spawn-docker-jdk/src/test/java/build/spawn/docker/jdk/JavaHttpClientTransportTest.java @@ -0,0 +1,81 @@ +package build.spawn.docker.jdk; + +import com.sun.net.httpserver.HttpServer; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.net.HttpURLConnection; +import java.net.InetSocketAddress; +import java.net.http.HttpClient; +import java.nio.charset.StandardCharsets; +import java.time.Duration; + +import static org.assertj.core.api.Assertions.assertThat; + +class JavaHttpClientTransportTest { + + private HttpServer server; + private JavaHttpClientTransport transport; + + @BeforeEach + void setUp() throws IOException { + this.server = HttpServer.create(new InetSocketAddress(0), 0); + this.server.start(); + final var port = this.server.getAddress().getPort(); + final var httpClient = HttpClient.newBuilder() + .connectTimeout(Duration.ofSeconds(5)) + .build(); + this.transport = new JavaHttpClientTransport(httpClient, "http://localhost:" + port); + } + + @AfterEach + void tearDown() { + this.server.stop(0); + } + + @Test + void shouldExecuteGetRequest() throws IOException { + this.server.createContext("/ping", exchange -> { + final var body = "pong".getBytes(StandardCharsets.UTF_8); + exchange.sendResponseHeaders(HttpURLConnection.HTTP_OK, body.length); + exchange.getResponseBody().write(body); + exchange.close(); + }); + + try (final var response = this.transport.execute(HttpTransport.Request.get("/ping"))) { + assertThat(response.statusCode()).isEqualTo(200); + assertThat(response.bodyString()).isEqualTo("pong"); + } + } + + @Test + void shouldExecutePostWithBody() throws IOException { + this.server.createContext("/echo", exchange -> { + final var received = exchange.getRequestBody().readAllBytes(); + exchange.sendResponseHeaders(HttpURLConnection.HTTP_OK, received.length); + exchange.getResponseBody().write(received); + exchange.close(); + }); + + final var body = "hello".getBytes(StandardCharsets.UTF_8); + try (final var response = this.transport.execute(HttpTransport.Request.post("/echo", body))) { + assertThat(response.statusCode()).isEqualTo(200); + assertThat(response.bodyString()).isEqualTo("hello"); + } + } + + @Test + void shouldReportNon2xxAsNotSuccessful() throws IOException { + this.server.createContext("/fail", exchange -> { + exchange.sendResponseHeaders(404, -1); + exchange.close(); + }); + + try (final var response = this.transport.execute(HttpTransport.Request.get("/fail"))) { + assertThat(response.isSuccessful()).isFalse(); + assertThat(response.statusCode()).isEqualTo(404); + } + } +} diff --git a/spawn-docker-okhttp/src/test/java/build/spawn/docker/okhttp/SessionTests.java b/spawn-docker-jdk/src/test/java/build/spawn/docker/jdk/SessionTests.java similarity index 99% rename from spawn-docker-okhttp/src/test/java/build/spawn/docker/okhttp/SessionTests.java rename to spawn-docker-jdk/src/test/java/build/spawn/docker/jdk/SessionTests.java index a96efaa..31295e4 100644 --- a/spawn-docker-okhttp/src/test/java/build/spawn/docker/okhttp/SessionTests.java +++ b/spawn-docker-jdk/src/test/java/build/spawn/docker/jdk/SessionTests.java @@ -1,4 +1,4 @@ -package build.spawn.docker.okhttp; +package build.spawn.docker.jdk; import build.base.assertion.Eventually; import build.base.configuration.Configuration; diff --git a/spawn-docker-okhttp/src/test/java/build/spawn/docker/okhttp/command/KillContainerRequestTests.java b/spawn-docker-jdk/src/test/java/build/spawn/docker/jdk/command/KillContainerRequestTests.java similarity index 90% rename from spawn-docker-okhttp/src/test/java/build/spawn/docker/okhttp/command/KillContainerRequestTests.java rename to spawn-docker-jdk/src/test/java/build/spawn/docker/jdk/command/KillContainerRequestTests.java index 9e6a7d6..480aa08 100644 --- a/spawn-docker-okhttp/src/test/java/build/spawn/docker/okhttp/command/KillContainerRequestTests.java +++ b/spawn-docker-jdk/src/test/java/build/spawn/docker/jdk/command/KillContainerRequestTests.java @@ -1,4 +1,4 @@ -package build.spawn.docker.okhttp.command; +package build.spawn.docker.jdk.command; import build.base.configuration.Configuration; import build.base.io.Terminal; @@ -7,7 +7,6 @@ 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; @@ -148,10 +147,9 @@ void requestShouldIncludeConfiguredSignal() throws Exception { injectContainer(command, "abc123"); - final var url = HttpUrl.parse("http://localhost/"); - final var request = command.createRequest(url.newBuilder()); + final var request = command.createRequest(); - assertThat(request.url().queryParameter("signal")).isEqualTo("SIGTERM"); + assertThat(request.path()).contains("signal=SIGTERM"); } /** @@ -164,9 +162,8 @@ void requestShouldDefaultToSigkill() throws Exception { injectContainer(command, "abc123"); - final var url = HttpUrl.parse("http://localhost/"); - final var request = command.createRequest(url.newBuilder()); + final var request = command.createRequest(); - assertThat(request.url().queryParameter("signal")).isEqualTo("SIGKILL"); + assertThat(request.path()).contains("signal=SIGKILL"); } } diff --git a/spawn-docker-okhttp/src/test/java/build/spawn/docker/okhttp/model/ContainerInformationTests.java b/spawn-docker-jdk/src/test/java/build/spawn/docker/jdk/model/ContainerInformationTests.java similarity index 96% rename from spawn-docker-okhttp/src/test/java/build/spawn/docker/okhttp/model/ContainerInformationTests.java rename to spawn-docker-jdk/src/test/java/build/spawn/docker/jdk/model/ContainerInformationTests.java index 4896853..f928cc1 100644 --- a/spawn-docker-okhttp/src/test/java/build/spawn/docker/okhttp/model/ContainerInformationTests.java +++ b/spawn-docker-jdk/src/test/java/build/spawn/docker/jdk/model/ContainerInformationTests.java @@ -1,4 +1,4 @@ -package build.spawn.docker.okhttp.model; +package build.spawn.docker.jdk.model; import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.jupiter.api.Test; diff --git a/spawn-docker-okhttp/src/test/resources/gdaymate.tar b/spawn-docker-jdk/src/test/resources/gdaymate.tar similarity index 100% rename from spawn-docker-okhttp/src/test/resources/gdaymate.tar rename to spawn-docker-jdk/src/test/resources/gdaymate.tar diff --git a/spawn-docker-okhttp/src/test/resources/gdaymate/Dockerfile b/spawn-docker-jdk/src/test/resources/gdaymate/Dockerfile similarity index 100% rename from spawn-docker-okhttp/src/test/resources/gdaymate/Dockerfile rename to spawn-docker-jdk/src/test/resources/gdaymate/Dockerfile diff --git a/spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/TCPSocketBasedSession.java b/spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/TCPSocketBasedSession.java deleted file mode 100644 index 9c75e2c..0000000 --- a/spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/TCPSocketBasedSession.java +++ /dev/null @@ -1,87 +0,0 @@ -package build.spawn.docker.okhttp; - -/*- - * #%L - * Spawn Docker (OkHttp 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.Configuration; -import build.codemodel.injection.InjectionFramework; -import build.spawn.docker.okhttp.option.ConnectTimeout; -import build.spawn.docker.okhttp.option.ReadTimeout; -import build.spawn.docker.okhttp.option.WriteTimeout; -import okhttp3.ConnectionPool; -import okhttp3.HttpUrl; -import okhttp3.OkHttpClient; - -import java.net.InetSocketAddress; -import java.net.Socket; -import java.time.Duration; -import java.time.temporal.ChronoUnit; -import java.util.concurrent.TimeUnit; - -/** - * A TCP-{@link Socket} based {@code Docker} {@link build.spawn.docker.Session}. - * - * @author brian.oliver - * @since Aug-2022 - */ -public class TCPSocketBasedSession - extends AbstractSession { - - /** - * Constructs a {@link TCPSocketBasedSession} for the specified API version using the {@link InetSocketAddress}. - * - * @param injectionFramework the {@link InjectionFramework} - * @param socketAddress the {@link InetSocketAddress} - * @param configuration the {@link Configuration} - */ - public TCPSocketBasedSession(final InjectionFramework injectionFramework, - final InetSocketAddress socketAddress, - final Configuration configuration) { - - super(injectionFramework, - () -> { - final Duration connectTimeout = configuration - .getOptionalValue(ConnectTimeout.class) - .orElse(Duration.of(10, ChronoUnit.SECONDS)); - - final Duration readTimeout = configuration - .getOptionalValue(ReadTimeout.class) - .orElse(Duration.of(30, ChronoUnit.SECONDS)); - - final Duration writeTimeout = configuration - .getOptionalValue(WriteTimeout.class) - .orElse(Duration.of(10, ChronoUnit.SECONDS)); - - final OkHttpClient.Builder builder = new OkHttpClient.Builder() - .retryOnConnectionFailure(true) - .connectTimeout(connectTimeout.getSeconds(), TimeUnit.SECONDS) - .writeTimeout(writeTimeout.getSeconds(), TimeUnit.SECONDS) - .readTimeout(readTimeout.getSeconds(), TimeUnit.SECONDS) - .connectionPool(new ConnectionPool(0, 1, TimeUnit.SECONDS)); - - return builder.build(); - }, - () -> new HttpUrl.Builder() - .scheme("http") - .host(socketAddress.getHostString()) - .port(socketAddress.getPort()), - configuration); - } -} diff --git a/spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/UnixDomainSocketBasedSession.java b/spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/UnixDomainSocketBasedSession.java deleted file mode 100644 index 09776ea..0000000 --- a/spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/UnixDomainSocketBasedSession.java +++ /dev/null @@ -1,470 +0,0 @@ -package build.spawn.docker.okhttp; - -/*- - * #%L - * Spawn Docker (OkHttp 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.Configuration; -import build.codemodel.injection.InjectionFramework; -import build.spawn.docker.Session; -import jakarta.inject.Inject; -import okhttp3.ConnectionPool; -import okhttp3.Dns; -import okhttp3.HttpUrl; -import okhttp3.OkHttpClient; -import org.newsclub.net.unix.AFUNIXSocket; -import org.newsclub.net.unix.AFUNIXSocketAddress; - -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.net.InetAddress; -import java.net.Socket; -import java.net.SocketAddress; -import java.net.SocketException; -import java.nio.channels.SocketChannel; -import java.util.Collections; -import java.util.Objects; -import java.util.Optional; -import java.util.concurrent.TimeUnit; -import javax.net.SocketFactory; - -/** - * A Unix Domain Socket-based {@code Docker} {@link Session}. - * - * @author brian.oliver - * @since Jun-2021 - */ -public class UnixDomainSocketBasedSession - extends AbstractSession { - - /** - * The {@code docker.sock} {@link File}. - */ - private static final File DOCKER_SOCK_FILE = resolveDockerSockFile(); - - /** - * Resolves the {@code docker.sock} {@link File} by checking known locations in order of preference. - * - * @return the {@code docker.sock} {@link File} - */ - private static File resolveDockerSockFile() { - final var candidates = new java.util.ArrayList(); - - // Docker Desktop on Linux - candidates.add(new File(System.getProperty("user.home") + "/.docker/desktop/docker.sock")); - - // standard Docker Engine location - candidates.add(new File("/var/run/docker.sock")); - - return candidates.stream() - .filter(File::exists) - .findFirst() - .orElse(new File("/var/run/docker.sock")); - } - - /** - * Constructs a {@link UnixDomainSocketBasedSession} using the specified {@link Configuration}. - * - * @param injectionFramework the {@link InjectionFramework} for Dependency Injection - * @param configuration the {@link Configuration} - */ - public UnixDomainSocketBasedSession(final InjectionFramework injectionFramework, - final Configuration configuration) { - - this(injectionFramework, DOCKER_SOCK_FILE, configuration); - } - - /** - * Constructs a {@link UnixDomainSocketBasedSession} for the specified API version using the provided Unix Socket {@link File}, - * which is typically something like {@code /var/run/docker.sock}. - * - * @param injectionFramework the {@link InjectionFramework} for Dependency Injection - * @param socketFile the Unix Socket {@link File} - * @param configuration the {@link Configuration} - */ - public UnixDomainSocketBasedSession(final InjectionFramework injectionFramework, - final File socketFile, - final Configuration configuration) { - - super(injectionFramework, - () -> { - final var builder = new OkHttpClient.Builder() - .retryOnConnectionFailure(true) - .socketFactory(new UnixDomainSocketFactory(socketFile)) - .connectionPool(new ConnectionPool(0, 1, TimeUnit.SECONDS)) - .dns(hostname -> { - if (hostname.endsWith(".sock")) { - try { - return Collections.singletonList(AFUNIXSocketAddress.of(socketFile).getAddress()); - } catch (final IOException e) { - throw new RuntimeException("Failed to open socket" + hostname, e); - } - } else { - return Dns.SYSTEM.lookup(hostname); - } - }); - - return builder.build(); - }, - () -> new HttpUrl.Builder() - .scheme("http") - .host("docker.sock"), - configuration); - } - - /** - * A {@link SocketFactory} for connecting to {@code unix://}-based {@link Socket} {@link File}s. - */ - private static class UnixDomainSocketFactory - extends SocketFactory { - - /** - * The Unix {@link Socket}-based {@link File} to which to connect. - */ - private final File socketFile; - - /** - * Constructs a {@link UnixDomainSocketFactory} for the specified Unix {@link Socket}-based {@link File}. - * - * @param socketFile the Unix {@link Socket}-based {@link File} - */ - private UnixDomainSocketFactory(final File socketFile) { - this.socketFile = Objects.requireNonNull(socketFile, "The Socket File must not be null"); - } - - @Override - public Socket createSocket() - throws IOException { - - // create and connect the Unix-based Socket - final var socket = AFUNIXSocket.newInstance(); - socket.connect(AFUNIXSocketAddress.of(this.socketFile)); - - return new ConnectedDomainSocket(socket); - } - - @Override - public Socket createSocket(final String host, final int port) { - - throw new UnsupportedOperationException( - "UnixDomainSocketFactory does not support InetAddress-based requests"); - } - - @Override - public Socket createSocket(final String host, - final int port, - final InetAddress localHost, - final int localPort) { - - throw new UnsupportedOperationException( - "UnixDomainSocketFactory does not support InetAddress-based requests"); - } - - @Override - public Socket createSocket(final InetAddress host, final int port) { - - throw new UnsupportedOperationException( - "UnixDomainSocketFactory does not support InetAddress-based requests"); - } - - @Override - public Socket createSocket(final InetAddress address, - final int port, - final InetAddress localAddress, - final int localPort) { - - throw new UnsupportedOperationException( - "UnixDomainSocketFactory does not support InetAddress-based requests"); - } - - /** - * A {@link Socket} adapter that prevents re-connection and re-binding. - */ - private static class ConnectedDomainSocket - extends Socket { - - private final Socket socket; - - private ConnectedDomainSocket(final Socket socket) { - this.socket = socket; - } - - @Override - public void connect(final SocketAddress endpoint) { - // ignore reconnection - } - - @Override - public void connect(final SocketAddress endpoint, final int timeout) { - // ignore reconnection - } - - @Override - public void bind(final SocketAddress bindpoint) { - // ignore reconnection - } - - @Override - public InetAddress getInetAddress() { - return this.socket.getInetAddress(); - } - - @Override - public InetAddress getLocalAddress() { - return this.socket.getLocalAddress(); - } - - @Override - public int getPort() { - return this.socket.getPort(); - } - - @Override - public int getLocalPort() { - return this.socket.getLocalPort(); - } - - @Override - public SocketAddress getRemoteSocketAddress() { - return this.socket.getRemoteSocketAddress(); - } - - @Override - public SocketAddress getLocalSocketAddress() { - return this.socket.getLocalSocketAddress(); - } - - @Override - public SocketChannel getChannel() { - return this.socket.getChannel(); - } - - @Override - public InputStream getInputStream() - throws IOException { - return this.socket.getInputStream(); - } - - @Override - public OutputStream getOutputStream() - throws IOException { - return this.socket.getOutputStream(); - } - - @Override - public void setTcpNoDelay(final boolean on) - throws SocketException { - this.socket.setTcpNoDelay(on); - } - - @Override - public boolean getTcpNoDelay() - throws SocketException { - return this.socket.getTcpNoDelay(); - } - - @Override - public void setSoLinger(final boolean on, final int linger) - throws SocketException { - this.socket.setSoLinger(on, linger); - } - - @Override - public int getSoLinger() - throws SocketException { - return this.socket.getSoLinger(); - } - - @Override - public void sendUrgentData(final int data) - throws IOException { - this.socket.sendUrgentData(data); - } - - @Override - public void setOOBInline(final boolean on) - throws SocketException { - this.socket.setOOBInline(on); - } - - @Override - public boolean getOOBInline() - throws SocketException { - return this.socket.getOOBInline(); - } - - @Override - public synchronized void setSoTimeout(final int timeout) - throws SocketException { - this.socket.setSoTimeout(timeout); - } - - @Override - public synchronized int getSoTimeout() - throws SocketException { - return this.socket.getSoTimeout(); - } - - @Override - public synchronized void setSendBufferSize(final int size) - throws SocketException { - this.socket.setSendBufferSize(size); - } - - @Override - public synchronized int getSendBufferSize() - throws SocketException { - return this.socket.getSendBufferSize(); - } - - @Override - public synchronized void setReceiveBufferSize(final int size) - throws SocketException { - this.socket.setReceiveBufferSize(size); - } - - @Override - public synchronized int getReceiveBufferSize() - throws SocketException { - return this.socket.getReceiveBufferSize(); - } - - @Override - public void setKeepAlive(final boolean on) - throws SocketException { - this.socket.setKeepAlive(on); - } - - @Override - public boolean getKeepAlive() - throws SocketException { - return this.socket.getKeepAlive(); - } - - @Override - public void setTrafficClass(final int tc) - throws SocketException { - this.socket.setTrafficClass(tc); - } - - @Override - public int getTrafficClass() - throws SocketException { - return this.socket.getTrafficClass(); - } - - @Override - public void setReuseAddress(final boolean on) - throws SocketException { - this.socket.setReuseAddress(on); - } - - @Override - public boolean getReuseAddress() - throws SocketException { - return this.socket.getReuseAddress(); - } - - @Override - public synchronized void close() - throws IOException { - this.socket.close(); - } - - @Override - public void shutdownInput() - throws IOException { - this.socket.shutdownInput(); - } - - @Override - public void shutdownOutput() - throws IOException { - this.socket.shutdownOutput(); - } - - @Override - public String toString() { - return this.socket.toString(); - } - - @Override - public boolean isConnected() { - return this.socket.isConnected(); - } - - @Override - public boolean isBound() { - return this.socket.isBound(); - } - - @Override - public boolean isClosed() { - return this.socket.isClosed(); - } - - @Override - public boolean isInputShutdown() { - return this.socket.isInputShutdown(); - } - - @Override - public boolean isOutputShutdown() { - return this.socket.isOutputShutdown(); - } - - @Override - public void setPerformancePreferences(final int connectionTime, final int latency, final int bandwidth) { - this.socket.setPerformancePreferences(connectionTime, latency, bandwidth); - } - } - } - - /** - * The {@link Session.Factory} for the {@link UnixDomainSocketBasedSession}s. - */ - public static class Factory - implements Session.Factory { - - /** - * The {@link InjectionFramework} to use for Dependency Injection. - */ - @Inject - private InjectionFramework injectionFramework; - - @Override - public boolean isOperational() { - // attempt to connect to the docker.sock file - try (var _ = new UnixDomainSocketFactory(DOCKER_SOCK_FILE).createSocket()) { - return true; - } catch (final Exception _) { - return false; - } - } - - @Override - public Optional create(final Configuration configuration) { - return isOperational() - ? Optional.of(new UnixDomainSocketBasedSession(this.injectionFramework, configuration)) - : Optional.empty(); - } - } -} diff --git a/spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/command/AbstractCommand.java b/spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/command/AbstractCommand.java deleted file mode 100644 index 52d0654..0000000 --- a/spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/command/AbstractCommand.java +++ /dev/null @@ -1,250 +0,0 @@ -package build.spawn.docker.okhttp.command; - -/*- - * #%L - * Spawn Docker (OkHttp 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.codemodel.injection.Context; -import jakarta.inject.Inject; -import okhttp3.Call; -import okhttp3.HttpUrl; -import okhttp3.MediaType; -import okhttp3.OkHttpClient; -import okhttp3.Request; -import okhttp3.RequestBody; -import okhttp3.Response; - -import java.io.IOException; - -/** - * An abstract {@link Command} that uses an {@link OkHttpClient} to submit request(s) and receive responses, - * that of which may be processed to produce a result. - * - * @param the type of result produced by the {@link Command} - * @author brian.oliver - * @since Jun-2021 - */ -public abstract class AbstractCommand - implements Command { - - /** - * The {@link MediaType} for JSON. - */ - protected final static MediaType MEDIA_TYPE_JSON = MediaType.parse("application/json"); - - /** - * An empty {@link RequestBody}. - */ - protected final static RequestBody EMPTY_BODY = RequestBody.create("", null); - - /** - * The {@link OkHttpClient} to use for executing the {@link Command}. - */ - @Inject - private OkHttpClient httpClient; - - /** - * The {@link HttpUrl.Builder} to use for creating the {@link Command}. - */ - @Inject - private HttpUrl.Builder httpUrlBuilder; - - /** - * The dependency injection {@link Context} for the {@link Command}. - */ - @Inject - private Context context; - - /** - * Obtains the {@link OkHttpClient} to use for executing {@link Command}s. - * - * @return the {@link OkHttpClient} - */ - protected OkHttpClient httpClient() { - return this.httpClient; - } - - /** - * Creates a new dependency injection {@link Context}, based on the {@link Context} used to create the - * {@link Command}. - * - * @return a new {@link Context} - */ - protected Context createContext() { - return this.context.newContext(); - } - - /** - * Obtains the {@link Request} to execute for the {@link Command}. - * - * @param httpUrlBuilder the {@link HttpUrl.Builder} for constructing the {@link Request} {@link HttpUrl} - * @return the {@link Request} - */ - abstract protected Request createRequest(HttpUrl.Builder httpUrlBuilder); - - /** - * Create the result for the {@link Command} based on the successful {@link Response}. - * - * @param response the {@link Response} - * @return the result - * @throws IOException should processing the {@link Response} fail - */ - abstract protected T createResult(Response response) - throws IOException; - - /** - * Handle when the {@link Request} has been created for the {@link Command}, but not yet sent for execution. - * - * @param request the {@link Request} - */ - abstract protected void onRequestCreated(Request request); - - /** - * Handle when the {@link Call} has been created for the {@link Command}, but not yet sent for execution. - * - * @param call the {@link Call} - */ - abstract protected void onCallCreated(Call call); - - /** - * Handle when the {@link Response} has been received for the {@link Request} representing the {@link Command}, - * prior to it being processed to establish a result. - * - * @param response the {@link Response} - */ - abstract protected void onResponseReceived(Response response); - - /** - * Handle when the execution of a {@link Request} was successful. - *

- * By default, this method closes the {@link Response}. - * - * @param request the {@link Request} - * @param response the {@link Response} - * @param result the result - */ - abstract protected void onSuccessfulRequest(Request request, - Response response, - T result); - - /** - * Handle when the execution of a {@link Request} was unsuccessful as indicated by {@link Response#isSuccessful()} - * returning {@code false}. - *

- * By default, this method throws a {@link IOException} containing the {@link Response#code()} for the - * {@link Request}. Should this method return without throwing an {@link IOException}, an attempt while be made - * to process and create the result for the {@link Command} using {@link #createResult(Response)}. - * - * @param request the {@link Request} - * @param response the {@link Response} - * @throws IOException should the {@link Request} be unsuccessful - */ - protected void onUnsuccessfulRequest(final Request request, - final Response response) - throws IOException { - - throw new IOException("Request failed with " + response.code() + ". Failed to execute " + request); - } - - /** - * Handle when the execution of a {@link Request} failed with the specified {@link Throwable}, allowing - * implementations to gracefully recover from said failures. - *

- * By default, this method simply re-throws the specified {@link Throwable} as a {@link RuntimeException}, - * if not already a {@link RuntimeException}. - * - * @param request the {@link Request} - * @param throwable the {@link Throwable} - * @return the result if the {@link Request} was recoverable - */ - protected T onRequestFailed(final Request request, - final Throwable throwable) { - - throw (throwable instanceof RuntimeException) - ? (RuntimeException) throwable - : new RuntimeException("Request Failed", throwable); - } - - /** - * Handle when the attempting to process the {@link Response}, including creating a result using - * {@link #createResult(Response)} failed. - *

- * By default, this method closes the {@link Response}. - * - * @param request the {@link Request} - * @param response the {@link Response} - * @param throwable the {@link Throwable} that occurred while processing the {@link Response} - */ - public void onUnprocessableResponse(final Request request, - final Response response, - final Throwable throwable) { - - // by default, we close the Response - response.close(); - } - - @Override - public T submit() { - // obtain the Request to submit - final var request = createRequest(this.httpUrlBuilder); - - // attempt to execute the request representing the command - try { - // notify that the request has been created - onRequestCreated(request); - - // create the http call for the request - final var call = httpClient() - .newCall(request); - - // notify that the call has been created - onCallCreated(call); - - // execute the call - final var response = call.execute(); - - // notify that the response has been received - onResponseReceived(response); - - // attempt to process the response - try { - // handle when the response is considered unsuccessful - if (!response.isSuccessful()) { - onUnsuccessfulRequest(request, response); - } - - // attempt to create the result from the response - final var result = createResult(response); - - // notify that the request was successful - onSuccessfulRequest(request, response, result); - - return result; - } catch (final Throwable throwable) { - // notify that the response is unprocessable - onUnprocessableResponse(request, response, throwable); - - throw throwable; - } - } catch (final Throwable recoverable) { - // attempt to recover from the exception - return onRequestFailed(request, recoverable); - } - } -} diff --git a/spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/option/ConnectTimeout.java b/spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/option/ConnectTimeout.java deleted file mode 100644 index f2cb93e..0000000 --- a/spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/option/ConnectTimeout.java +++ /dev/null @@ -1,44 +0,0 @@ -package build.spawn.docker.okhttp.option; - -/*- - * #%L - * Spawn Docker (OkHttp 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.AbstractValueOption; - -import java.time.Duration; - -/** - * A {@link ConnectTimeout} for OkHttp. - * - * @author anand.sankaran - * @since Aug-2022 - */ -public class ConnectTimeout - extends AbstractValueOption { - - /** - * Constructs an {@link ConnectTimeout}. - * - * @param value the non-{@code null} value - */ - public ConnectTimeout(final Duration value) { - super(value); - } -} diff --git a/spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/option/ReadTimeout.java b/spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/option/ReadTimeout.java deleted file mode 100644 index 134ade5..0000000 --- a/spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/option/ReadTimeout.java +++ /dev/null @@ -1,44 +0,0 @@ -package build.spawn.docker.okhttp.option; - -/*- - * #%L - * Spawn Docker (OkHttp 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.AbstractValueOption; - -import java.time.Duration; - -/** - * A {@link ReadTimeout} for OkHttp. - * - * @author anand.sankaran - * @since Aug-2022 - */ -public class ReadTimeout - extends AbstractValueOption { - - /** - * Constructs an {@link ReadTimeout}. - * - * @param value the non-{@code null} value - */ - public ReadTimeout(final Duration value) { - super(value); - } -} diff --git a/spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/option/WriteTimeout.java b/spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/option/WriteTimeout.java deleted file mode 100644 index 5d58f05..0000000 --- a/spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/option/WriteTimeout.java +++ /dev/null @@ -1,44 +0,0 @@ -package build.spawn.docker.okhttp.option; - -/*- - * #%L - * Spawn Docker (OkHttp 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.AbstractValueOption; - -import java.time.Duration; - -/** - * A {@link WriteTimeout} for OkHttp. - * - * @author anand.sankaran - * @since Aug-2022 - */ -public class WriteTimeout - extends AbstractValueOption { - - /** - * Constructs an {@link WriteTimeout}. - * - * @param value the non-{@code null} value - */ - public WriteTimeout(final Duration value) { - super(value); - } -} diff --git a/spawn-docker-okhttp/src/main/resources/META-INF/services/build.spawn.docker.Session$Factory b/spawn-docker-okhttp/src/main/resources/META-INF/services/build.spawn.docker.Session$Factory deleted file mode 100644 index 03fd846..0000000 --- a/spawn-docker-okhttp/src/main/resources/META-INF/services/build.spawn.docker.Session$Factory +++ /dev/null @@ -1,4 +0,0 @@ -build.spawn.docker.okhttp.UnixDomainSocketBasedSession$Factory -build.spawn.docker.okhttp.LocalHostBasedSessionFactory -build.spawn.docker.okhttp.DockerHostVariableBasedSessionFactory -build.spawn.docker.okhttp.InternalDockerHostBasedSessionFactory