Skip to content

Commit 73210ab

Browse files
committed
fixup: Container egress interception image has to be optional until we exit experimental phase
1 parent dcd8eaa commit 73210ab

4 files changed

Lines changed: 32 additions & 26 deletions

File tree

src/workerd/server/container-client.c++

Lines changed: 11 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,7 @@ ContainerClient::ContainerClient(capnp::ByteStreamFactory& byteStreamFactory,
177177
kj::String dockerPath,
178178
kj::String containerName,
179179
kj::String imageName,
180-
kj::String containerEgressInterceptorImage,
180+
kj::Maybe<kj::String> containerEgressInterceptorImage,
181181
kj::TaskSet& waitUntilTasks,
182182
kj::Function<void()> cleanupCallback,
183183
ChannelTokenHandler& channelTokenHandler)
@@ -314,8 +314,7 @@ class EgressHttpService final: public kj::HttpService {
314314
auto innerServer =
315315
kj::heap<kj::HttpServer>(containerClient.timer, headerTable, *innerService);
316316

317-
co_await innerServer->listenHttpCleanDrain(connection)
318-
.attach(kj::mv(innerServer), kj::mv(innerService));
317+
co_await innerServer->listenHttpCleanDrain(connection);
319318

320319
co_return;
321320
}
@@ -351,7 +350,6 @@ constexpr kj::StringPtr WORKERD_NETWORK_NAME = "workerd-network"_kj;
351350

352351
kj::Promise<ContainerClient::IPAMConfigResult> ContainerClient::getDockerBridgeIPAMConfig() {
353352
// First, try to find or create the workerd-network
354-
// Docker API: GET /networks/workerd-network
355353
auto response = co_await dockerApiRequest(network, kj::str(dockerPath), kj::HttpMethod::GET,
356354
kj::str("/networks/", WORKERD_NETWORK_NAME));
357355

@@ -383,7 +381,6 @@ kj::Promise<ContainerClient::IPAMConfigResult> ContainerClient::getDockerBridgeI
383381
}
384382

385383
kj::Promise<void> ContainerClient::createWorkerdNetwork() {
386-
// Docker API: POST /networks/create
387384
// Equivalent to: docker network create -d bridge --ipv6 workerd-network
388385
capnp::JsonCodec codec;
389386
codec.handleByAnnotation<docker_api::Docker::NetworkCreateRequest>();
@@ -466,7 +463,6 @@ kj::Promise<ContainerClient::Response> ContainerClient::dockerApiRequest(kj::Net
466463
}
467464

468465
kj::Promise<ContainerClient::InspectResponse> ContainerClient::inspectContainer() {
469-
// Docker API: GET /containers/{id}/json
470466
auto endpoint = kj::str("/containers/", containerName, "/json");
471467

472468
auto response = co_await dockerApiRequest(
@@ -526,7 +522,6 @@ kj::Promise<void> ContainerClient::createContainer(
526522
kj::Maybe<capnp::List<capnp::Text>::Reader> entrypoint,
527523
kj::Maybe<capnp::List<capnp::Text>::Reader> environment,
528524
rpc::Container::StartParams::Reader params) {
529-
// Docker API: POST /containers/create
530525
capnp::JsonCodec codec;
531526
codec.handleByAnnotation<docker_api::Docker::ContainerCreateRequest>();
532527
capnp::MallocMessageBuilder message;
@@ -601,7 +596,6 @@ kj::Promise<void> ContainerClient::createContainer(
601596
}
602597

603598
kj::Promise<void> ContainerClient::startContainer() {
604-
// Docker API: POST /containers/{id}/start
605599
auto endpoint = kj::str("/containers/", containerName, "/start");
606600
// We have to send an empty body since docker API will throw an error if we don't.
607601
auto response = co_await dockerApiRequest(
@@ -613,7 +607,6 @@ kj::Promise<void> ContainerClient::startContainer() {
613607
}
614608

615609
kj::Promise<void> ContainerClient::stopContainer() {
616-
// Docker API: POST /containers/{id}/stop
617610
auto endpoint = kj::str("/containers/", containerName, "/stop");
618611
auto response = co_await dockerApiRequest(
619612
network, kj::str(dockerPath), kj::HttpMethod::POST, kj::mv(endpoint));
@@ -625,7 +618,6 @@ kj::Promise<void> ContainerClient::stopContainer() {
625618
}
626619

627620
kj::Promise<void> ContainerClient::killContainer(uint32_t signal) {
628-
// Docker API: POST /containers/{id}/kill
629621
auto endpoint = kj::str("/containers/", containerName, "/kill?signal=", signalToString(signal));
630622
auto response = co_await dockerApiRequest(
631623
network, kj::str(dockerPath), kj::HttpMethod::POST, kj::mv(endpoint));
@@ -669,7 +661,10 @@ kj::Promise<void> ContainerClient::createSidecarContainer(
669661
codec.handleByAnnotation<docker_api::Docker::ContainerCreateRequest>();
670662
capnp::MallocMessageBuilder message;
671663
auto jsonRoot = message.initRoot<docker_api::Docker::ContainerCreateRequest>();
672-
jsonRoot.setImage(containerEgressInterceptorImage);
664+
auto& image = KJ_ASSERT_NONNULL(containerEgressInterceptorImage,
665+
"containerEgressInterceptorImage must be configured to use egress interception. "
666+
"Set it in the localDocker configuration.");
667+
jsonRoot.setImage(image);
673668

674669
auto cmd = jsonRoot.initCmd(4);
675670
cmd.set(0, "--http-egress-port");
@@ -695,8 +690,7 @@ kj::Promise<void> ContainerClient::createSidecarContainer(
695690
}
696691

697692
if (response.statusCode != 201) {
698-
JSG_REQUIRE(response.statusCode != 404, Error, "No such image available named ",
699-
containerEgressInterceptorImage,
693+
JSG_REQUIRE(response.statusCode != 404, Error, "No such image available named ", image,
700694
". Please ensure the container egress interceptor image is built and available.");
701695
JSG_FAIL_REQUIRE(Error, "Failed to create the networking sidecar [", response.statusCode, "] ",
702696
response.body);
@@ -744,14 +738,17 @@ kj::Promise<void> ContainerClient::start(StartContext context) {
744738
internetEnabled = params.getEnableInternet();
745739

746740
co_await createContainer(entrypoint, environment, params);
741+
co_await startContainer();
747742

748743
// Opt in to the proxy sidecar container only if the user has configured egressMappings
749744
// for now. In the future, it will always run when a user container is running
750745
if (!egressMappings.empty()) {
746+
// The user container will be blocked on network connectivity until this finishes.
747+
// When workerd-network is more battle-tested and goes out of experimental so it's non-optional,
748+
// we should make the sidecar start first and _then_ make the user container join the sidecar network.
751749
co_await ensureSidecarStarted();
752750
}
753751

754-
co_await startContainer();
755752
containerStarted.store(true, std::memory_order_release);
756753
}
757754

@@ -861,7 +858,6 @@ kj::Promise<void> ContainerClient::setEgressHttp(SetEgressHttpContext context) {
861858
auto params = context.getParams();
862859
auto hostPortStr = kj::str(params.getHostPort());
863860
auto tokenBytes = params.getChannelToken();
864-
JSG_REQUIRE(containerEgressInterceptorImage != "", Error, "should be set for setEgressHttp");
865861

866862
auto parsed = parseHostPort(hostPortStr);
867863
uint16_t port = parsed.port.orDefault(80);

src/workerd/server/container-client.h

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ class ContainerClient final: public rpc::Container::Server, public kj::Refcounte
3939
kj::String dockerPath,
4040
kj::String containerName,
4141
kj::String imageName,
42-
kj::String containerEgressInterceptorImage,
42+
kj::Maybe<kj::String> containerEgressInterceptorImage,
4343
kj::TaskSet& waitUntilTasks,
4444
kj::Function<void()> cleanupCallback,
4545
ChannelTokenHandler& channelTokenHandler);
@@ -70,7 +70,7 @@ class ContainerClient final: public rpc::Container::Server, public kj::Refcounte
7070
kj::String imageName;
7171

7272
// Container egress interceptor image name (sidecar for egress proxy)
73-
kj::String containerEgressInterceptorImage;
73+
kj::Maybe<kj::String> containerEgressInterceptorImage;
7474

7575
kj::TaskSet& waitUntilTasks;
7676

src/workerd/server/server.c++

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2860,12 +2860,10 @@ class Server::WorkerService final: public Service,
28602860
containerClients.erase(containerId);
28612861
};
28622862

2863-
auto& interceptorImage = KJ_ASSERT_NONNULL(containerEgressInterceptorImage,
2864-
"containerEgressInterceptorImage must be defined when docker is enabled.");
2865-
28662863
auto client = kj::refcounted<ContainerClient>(byteStreamFactory, timer, dockerNetwork,
28672864
kj::str(dockerPathRef), kj::str(containerId), kj::str(imageName),
2868-
kj::str(interceptorImage), waitUntilTasks, kj::mv(cleanupCallback), channelTokenHandler);
2865+
containerEgressInterceptorImage.map([](kj::StringPtr s) { return kj::str(s); }),
2866+
waitUntilTasks, kj::mv(cleanupCallback), channelTokenHandler);
28692867

28702868
// Store raw pointer in map (does not own)
28712869
containerClients.insert(kj::str(containerId), client.get());

src/workerd/server/tests/container-client/test.js

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -355,14 +355,16 @@ export class DurableObjectExample extends DurableObject {
355355
await container.destroy();
356356
await monitor;
357357
}
358-
assert.strictEqual(container.running, false);
359-
360-
container.start({
361-
enableInternet: true,
362-
});
363358

364359
assert.strictEqual(container.running, false);
365360

361+
// Set up egress TCP mapping to route requests to the binding
362+
// We can configure this even before the container starts.
363+
await container.interceptOutboundHttp(
364+
'1.2.3.4',
365+
this.ctx.exports.TestService({ props: { id: 1234 } })
366+
);
367+
366368
// Start container
367369
container.start();
368370

@@ -388,6 +390,16 @@ export class DurableObjectExample extends DurableObject {
388390
this.ctx.exports.TestService({ props: { id: 3 } })
389391
);
390392

393+
{
394+
const response = await container
395+
.getTcpPort(8080)
396+
.fetch('http://foo/intercept', {
397+
headers: { 'x-host': '1.2.3.4:80' },
398+
});
399+
assert.equal(response.status, 200);
400+
assert.equal(await response.text(), 'hello binding: 1234');
401+
}
402+
391403
{
392404
const response = await container
393405
.getTcpPort(8080)

0 commit comments

Comments
 (0)