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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ import com.alibaba.opensandbox.sandbox.domain.services.Health
import com.alibaba.opensandbox.sandbox.domain.services.Metrics
import com.alibaba.opensandbox.sandbox.domain.services.Sandboxes
import com.alibaba.opensandbox.sandbox.infrastructure.factory.AdapterFactory
import okhttp3.ConnectionPool
import org.slf4j.LoggerFactory
import java.time.Duration
import java.time.OffsetDateTime
Expand Down Expand Up @@ -145,12 +144,16 @@ class Sandbox internal constructor(
* Creates a sandbox instance with the provided configuration.
*
* @param imageSpec Container image specification
* @param entrypoint Sandbox entrypoint command
* @param env Environment variables (optional)
* @param metadata Metadata for the sandbox (optional)
* @param timeout Sandbox timeout in seconds
* @param timeout Sandbox timeout (automatic termination time)
* @param readyTimeout Timeout for waiting for sandbox readiness
* @param resource Resource limits (optional)
* @param connectionConfig Connection configuration
* @param healthCheck Custom health check function (optional)
* @param healthCheckPollingInterval Polling interval for readiness/health check
* @param extensions Optional extension parameters for server-side customized behaviors
* @return Fully configured and ready Sandbox instance
* @throws SandboxException if sandbox creation or initialization fails
*/
Expand All @@ -165,7 +168,7 @@ class Sandbox internal constructor(
connectionConfig: ConnectionConfig,
healthCheck: ((Sandbox) -> Boolean)? = null,
healthCheckPollingInterval: Duration,
connectionPool: ConnectionPool? = null,
extensions: Map<String, String>,
): Sandbox {
logger.info("Start creating sandbox with image: {} (timeout: {}s)", imageSpec.image, timeout.seconds)

Expand All @@ -184,6 +187,7 @@ class Sandbox internal constructor(
metadata,
timeout,
resource,
extensions,
)
sandboxId = response.id

Expand Down Expand Up @@ -631,6 +635,14 @@ class Sandbox internal constructor(
*/
private val metadata = mutableMapOf<String, String>()

/**
* Optional extension parameters for server-side custom behaviors.
*
* This map is treated as opaque and is sent to the server as-is.
* Prefer namespaced keys (e.g. `storage.id`) to avoid collisions.
*/
private val extensions = mutableMapOf<String, String>()

/**
* Lifecycle config
*/
Expand Down Expand Up @@ -804,6 +816,47 @@ class Sandbox internal constructor(
return this
}

/**
* Adds a single extension parameter.
*
* Extensions are opaque client-side and are passed through to the server.
* Prefer stable, namespaced keys (e.g. `storage.id`).
*
* @throws InvalidArgumentException if [key] is blank
*/
fun extension(
key: String,
value: String,
): Builder {
if (key.isBlank()) {
throw InvalidArgumentException(
message = "Extension key cannot be blank",
)
}
extensions[key] = value
return this
}

/**
* Adds multiple extension parameters.
*
* Extensions are opaque client-side and are passed through to the server.
*/
fun extensions(extensions: Map<String, String>): Builder {
this.extensions.putAll(extensions)
return this
}

/**
* Configures extension parameters using a fluent configuration block.
*
* Extensions are opaque client-side and are passed through to the server.
*/
fun extensions(configure: MutableMap<String, String>.() -> Unit): Builder {
extensions.configure()
return this
}

/**
* Sets the sandbox timeout (automatic termination time).
*
Expand Down Expand Up @@ -897,6 +950,7 @@ class Sandbox internal constructor(
timeout = timeout,
readyTimeout = readyTimeout,
resource = resource,
extensions = extensions,
connectionConfig = connectionConfig ?: ConnectionConfig.builder().build(),
healthCheckPollingInterval = healthCheckPollingInterval,
healthCheck = healthCheck,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,14 @@ interface Sandboxes {
/**
* Creates a new sandbox with the specified configuration.
*
* @param spec Image specification for the sandbox
* @return Sandbox create response
* @param spec Container image specification for provisioning the sandbox
* @param entrypoint The command to run as the sandbox's main process (e.g. `["python", "/app/main.py"]`)
* @param env Environment variables injected into the sandbox runtime
* @param metadata User-defined metadata used for management and filtering
* @param timeout Sandbox lifetime. The server may terminate the sandbox when it expires
* @param resource Runtime resource limits (e.g. cpu/memory). Exact semantics are server-defined
* @param extensions Opaque extension parameters passed through to the server as-is. Prefer namespaced keys
* @return Sandbox creation response containing the sandbox id
*/
fun createSandbox(
spec: SandboxImageSpec,
Expand All @@ -46,6 +52,7 @@ interface Sandboxes {
metadata: Map<String, String>,
timeout: Duration,
resource: Map<String, String>,
extensions: Map<String, String>,
): SandboxCreateResponse

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ internal object SandboxModelConverter {
metadata: Map<String, String>,
timeout: Duration,
resource: Map<String, String>,
extensions: Map<String, String>,
): CreateSandboxRequest {
return CreateSandboxRequest(
image = spec.toApiImageSpec(),
Expand All @@ -81,6 +82,7 @@ internal object SandboxModelConverter {
metadata = metadata,
timeout = timeout.seconds.toInt(),
resourceLimits = resource,
extensions = extensions,
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,11 +57,21 @@ internal class SandboxesAdapter(
metadata: Map<String, String>,
timeout: Duration,
resource: Map<String, String>,
extensions: Map<String, String>,
): SandboxCreateResponse {
logger.info("Creating sandbox with image: {}", spec.image)

return try {
val createRequest = SandboxModelConverter.toApiCreateSandboxRequest(spec, entrypoint, env, metadata, timeout, resource)
val createRequest =
SandboxModelConverter.toApiCreateSandboxRequest(
spec = spec,
entrypoint = entrypoint,
env = env,
metadata = metadata,
timeout = timeout,
resource = resource,
extensions = extensions,
)
val apiResponse = api.sandboxesPost(createRequest)
val response = apiResponse.toSandboxCreateResponse()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,12 @@ import okhttp3.mockwebserver.MockWebServer
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertNotNull
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import java.time.Duration
import java.util.UUID

Expand Down Expand Up @@ -76,6 +80,7 @@ class SandboxesAdapterTest {

// Execute
val spec = SandboxImageSpec.builder().image("ubuntu:latest").build()
val extensions = mapOf("storage.id" to "abc123", "debug" to "true")
val result =
sandboxesAdapter.createSandbox(
spec = spec,
Expand All @@ -84,12 +89,21 @@ class SandboxesAdapterTest {
metadata = mapOf("meta" to "data"),
timeout = Duration.ofSeconds(600),
resource = mapOf("cpu" to "1"),
extensions = extensions,
)

// Verify request
val request = mockWebServer.takeRequest()
assertEquals("POST", request.method)
assertEquals("/sandboxes", request.path)
val requestBody = request.body.readUtf8()
assertTrue(requestBody.isNotBlank(), "request body should not be blank")

val payload = Json.parseToJsonElement(requestBody).jsonObject
val gotExtensions = payload["extensions"]?.jsonObject
assertNotNull(gotExtensions, "extensions should be present in createSandbox request")
assertEquals("abc123", gotExtensions!!["storage.id"]!!.jsonPrimitive.content)
assertEquals("true", gotExtensions["debug"]!!.jsonPrimitive.content)

// Verify response
assertEquals(UUID.fromString("550e8400-e29b-41d4-a716-446655440000"), result.id)
Expand Down
5 changes: 3 additions & 2 deletions tests/java/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,9 @@ group = "com.alibaba.opensandbox"
version = "1.0.0"

java {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
toolchain {
languageVersion.set(JavaLanguageVersion.of(8))
}
}

repositories {
Expand Down
4 changes: 4 additions & 0 deletions tests/java/settings.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,7 @@
*/

rootProject.name = "opensandbox-java-e2e-tests"

plugins {
id("org.gradle.toolchains.foojay-resolver-convention") version("1.0.0")
}
Original file line number Diff line number Diff line change
Expand Up @@ -109,14 +109,14 @@ protected static void assertEndpointHasPort(String endpoint, int expectedPort) {
endpoint.endsWith("/" + expectedPort),
"endpoint route must end with /" + expectedPort + ": " + endpoint);
String prefix = endpoint.split("/", 2)[0];
assertFalse(prefix.isBlank(), "missing domain in endpoint: " + endpoint);
assertFalse(prefix.trim().isEmpty(), "missing domain in endpoint: " + endpoint);
return;
}
int idx = endpoint.lastIndexOf(':');
assertTrue(idx > 0, "missing host:port in endpoint: " + endpoint);
String host = endpoint.substring(0, idx);
String port = endpoint.substring(idx + 1);
assertFalse(host.isBlank(), "missing host in endpoint: " + endpoint);
assertFalse(host.trim().isEmpty(), "missing host in endpoint: " + endpoint);
assertTrue(port.matches("\\d+"), "non-numeric port in endpoint: " + endpoint);
assertEquals(expectedPort, Integer.parseInt(port), "endpoint port mismatch: " + endpoint);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,7 @@
import com.alibaba.opensandbox.sandbox.domain.models.sandboxes.SandboxMetrics;
import java.time.Duration;
import java.time.OffsetDateTime;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.*;
import java.util.concurrent.*;
import org.junit.jupiter.api.*;
import org.slf4j.Logger;
Expand All @@ -58,14 +56,21 @@ public class CodeInterpreterE2ETest extends BaseE2ETest {
private Sandbox sandbox;
private CodeInterpreter codeInterpreter;

private static Map<String, String> createResourceMap() {
Map<String, String> map = new HashMap<>();
map.put("cpu", "2");
map.put("memory", "4Gi");
return map;
}

private static void assertTerminalEventContract(
List<ExecutionInit> initEvents,
List<ExecutionComplete> completedEvents,
List<ExecutionError> errors,
String executionId) {
assertEquals(1, initEvents.size(), "init event must exist exactly once");
assertNotNull(initEvents.get(0).getId());
assertFalse(initEvents.get(0).getId().isBlank());
assertFalse(initEvents.get(0).getId().trim().isEmpty());
assertEquals(executionId, initEvents.get(0).getId());
assertRecentTimestampMs(initEvents.get(0).getTimestamp(), 180_000);
assertTrue(
Expand All @@ -78,7 +83,7 @@ private static void assertTerminalEventContract(
}
if (!errors.isEmpty()) {
assertNotNull(errors.get(0).getName());
assertFalse(errors.get(0).getName().isBlank());
assertFalse(errors.get(0).getName().trim().isEmpty());
assertNotNull(errors.get(0).getValue());
assertRecentTimestampMs(errors.get(0).getTimestamp(), 180_000);
}
Expand All @@ -89,12 +94,13 @@ void setup() {
sandbox =
Sandbox.builder()
.connectionConfig(sharedConnectionConfig)
.entrypoint(List.of("/opt/opensandbox/code-interpreter.sh"))
.entrypoint(
Collections.singletonList("/opt/opensandbox/code-interpreter.sh"))
.image(getSandboxImage())
.resource(java.util.Map.of("cpu", "2", "memory", "4Gi"))
.resource(createResourceMap())
.timeout(Duration.ofMinutes(15))
.readyTimeout(Duration.ofSeconds(60))
.metadata(java.util.Map.of("tag", "e2e-code-interpreter"))
.metadata(Collections.singletonMap("tag", "e2e-code-interpreter"))
.env("E2E_TEST", "true")
.env("GO_VERSION", "1.25")
.env("JAVA_VERSION", "21")
Expand Down Expand Up @@ -249,7 +255,7 @@ void testJavaCodeExecution() {

assertNotNull(simpleResult);
assertNotNull(simpleResult.getId());
assertFalse(simpleResult.getId().isBlank());
assertFalse(simpleResult.getId().trim().isEmpty());
assertEquals("4", simpleResult.getResult().get(0).getText());
assertTerminalEventContract(initEvents, completedEvents, errors, simpleResult.getId());
assertTrue(errors.isEmpty());
Expand Down
Loading
Loading