Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -14,18 +14,18 @@ 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).

## Build

```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

Expand Down
39 changes: 18 additions & 21 deletions docs/CODEBASE_MAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand Down Expand Up @@ -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.okhttpOkHttp implementation
├── spawn-docker-jdk/ # JPMS: build.spawn.docker.jdkJDK HTTP Client implementation
└── pom.xml # Parent POM; manages versions, Checkstyle, Surefire
```

Expand Down Expand Up @@ -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`
Expand All @@ -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
Expand Down
4 changes: 1 addition & 3 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -73,10 +73,8 @@
<byte-buddy.version>1.18.4</byte-buddy.version>
<jackson-core.version>2.21.2</jackson-core.version>
<junit.version>6.0.3</junit.version>
<junixsocket.version>2.10.1</junixsocket.version>
<jakarta-inject.version>2.0.1</jakarta-inject.version>
<mockito.version>5.23.0</mockito.version>
<okhttp.version>4.12.0</okhttp.version>
<codemodel.version>0.19.0</codemodel.version>

<!-- Plugin Dependency Versions -->
Expand Down Expand Up @@ -107,7 +105,7 @@
<module>spawn-option</module>

<module>spawn-docker</module>
<module>spawn-docker-okhttp</module>
<module>spawn-docker-jdk</module>
</modules>

<dependencyManagement>
Expand Down
35 changes: 12 additions & 23 deletions spawn-docker-okhttp/pom.xml → spawn-docker-jdk/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@
<version>${revision}</version>
</parent>

<artifactId>spawn-docker-okhttp</artifactId>
<artifactId>spawn-docker-jdk</artifactId>

<name>Spawn Docker (OkHttp Client)</name>
<name>Spawn Docker (JDK Client)</name>

<dependencies>
<dependency>
Expand Down Expand Up @@ -76,23 +76,6 @@
<version>${codemodel.version}</version>
</dependency>

<dependency>
<groupId>com.kohlschutter.junixsocket</groupId>
<artifactId>junixsocket-common</artifactId>
<version>${junixsocket.version}</version>
</dependency>

<dependency>
<groupId>com.kohlschutter.junixsocket</groupId>
<artifactId>junixsocket-native-common</artifactId>
<version>${junixsocket.version}</version>
</dependency>

<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>${okhttp.version}</version>
</dependency>

<dependency>
<groupId>jakarta.inject</groupId>
Expand Down Expand Up @@ -154,18 +137,24 @@
<artifactId>maven-surefire-plugin</artifactId>
<version>${maven-surefire-plugin.version}</version>
<configuration>
<argLine>--enable-native-access=ALL-UNNAMED</argLine>
<argLine>--add-modules jdk.httpserver --add-reads build.spawn.docker.jdk=jdk.httpserver</argLine>
<useSystemClassLoader>true</useSystemClassLoader>
<useManifestOnlyJar>false</useManifestOnlyJar>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<compilerArgs>
<arg>--add-reads=build.spawn.docker.jdk=jdk.httpserver</arg>
</compilerArgs>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<configuration>
<ignoredUnusedDeclaredDependencies>
<unusedDeclaredDependency>com.kohlschutter.junixsocket:junixsocket-native-common</unusedDeclaredDependency>
</ignoredUnusedDeclaredDependencies>
<ignoredNonTestScopedDependencies>
<ignoredNonTestScopedDependency>build.codemodel:codemodel-foundation</ignoredNonTestScopedDependency>
<ignoredNonTestScopedDependency>build.codemodel:jdk-codemodel</ignoredNonTestScopedDependency>
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
* %%
Expand Down Expand Up @@ -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.
Expand All @@ -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.
Expand Down Expand Up @@ -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}.
* <p>
* 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.
* <p>
* 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<OkHttpClient> clientSupplier,
final Supplier<HttpUrl.Builder> httpUrlBuilderSupplier,
final HttpTransport transport,
final Configuration configuration) {

Objects.requireNonNull(injectionFramework, "The InjectionFramework must not be null");
Objects.requireNonNull(clientSupplier, "The Supplier<OkHttpClient> must not be null");
Objects.requireNonNull(httpUrlBuilderSupplier, "The Supplier<HttpUrl.Builder> 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()
Expand All @@ -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);
Expand Down Expand Up @@ -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));

Expand Down Expand Up @@ -316,7 +293,7 @@ public Optional<Image> 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
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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<Request.Builder, Request.Builder> {
extends UnaryOperator<HttpTransport.Request> {

/**
* An {@link Authenticator} that performs no authentication.
*/
Authenticator NONE = builder -> builder;
Authenticator NONE = request -> request;
}
Loading
Loading