diff --git a/src/workerd/api/container.c++ b/src/workerd/api/container.c++ index 4e1fb2eebb2..03b3bb47b10 100644 --- a/src/workerd/api/container.c++ +++ b/src/workerd/api/container.c++ @@ -54,6 +54,21 @@ kj::Array emptyByteArray() { return kj::heapArray(0); } +void requireValidLabels(jsg::Dict& labels) { + for (auto i: kj::indices(labels.fields)) { + auto& field = labels.fields[i]; + JSG_REQUIRE(field.name.size() > 0, Error, "Label names cannot be empty"); + for (auto c: field.name) { + JSG_REQUIRE(static_cast(c) >= 0x20, Error, + "Label names cannot contain control characters (index ", i, ")"); + } + for (auto c: field.value) { + JSG_REQUIRE(static_cast(c) >= 0x20, Error, + "Label values cannot contain control characters (index ", i, ")"); + } + } +} + capnp::ByteStream::Client makeExecPipe( capnp::ByteStreamFactory& factory, kj::Own output) { return factory.kjToCapnp(capnp::ExplicitEndOutputStream::wrap(kj::mv(output), []() {})); @@ -237,18 +252,10 @@ void Container::start(jsg::Lock& js, jsg::Optional maybeOptions) } KJ_IF_SOME(labels, options.labels) { + requireValidLabels(labels); auto list = req.initLabels(labels.fields.size()); for (auto i: kj::indices(labels.fields)) { auto& field = labels.fields[i]; - JSG_REQUIRE(field.name.size() > 0, Error, "Label names cannot be empty"); - for (auto c: field.name) { - JSG_REQUIRE(static_cast(c) >= 0x20, Error, - "Label names cannot contain control characters (index ", i, ")"); - } - for (auto c: field.value) { - JSG_REQUIRE(static_cast(c) >= 0x20, Error, - "Label values cannot contain control characters (index ", i, ")"); - } list[i].setName(field.name); list[i].setValue(field.value); } @@ -284,6 +291,22 @@ void Container::start(jsg::Lock& js, jsg::Optional maybeOptions) running = true; } +jsg::Promise Container::setLabels(jsg::Lock& js, jsg::Dict labels) { + JSG_REQUIRE(running, Error, "setLabels() cannot be called on a container that is not running."); + + requireValidLabels(labels); + + auto req = rpcClient->setLabelsRequest(); + auto list = req.initLabels(labels.fields.size()); + for (auto i: kj::indices(labels.fields)) { + auto& field = labels.fields[i]; + list[i].setName(field.name); + list[i].setValue(field.value); + } + + return IoContext::current().awaitIo(js, req.sendIgnoringResult()); +} + jsg::Promise> Container::inspect(jsg::Lock& js) { return IoContext::current().awaitIo(js, rpcClient->inspectRequest().send(), [](jsg::Lock& js, diff --git a/src/workerd/api/container.h b/src/workerd/api/container.h index 2bbd745a188..a86e330d133 100644 --- a/src/workerd/api/container.h +++ b/src/workerd/api/container.h @@ -258,6 +258,8 @@ class Container: public jsg::Object { jsg::Promise> inspect(jsg::Lock& js); + jsg::Promise setLabels(jsg::Lock& js, jsg::Dict labels); + // TODO(containers): listenTcp() JSG_RESOURCE_TYPE(Container, CompatibilityFlags::Reader flags) { @@ -278,6 +280,7 @@ class Container: public jsg::Object { JSG_METHOD(exec); JSG_METHOD(interceptOutboundTcp); JSG_METHOD(inspect); + JSG_METHOD(setLabels); } } diff --git a/src/workerd/io/container.capnp b/src/workerd/io/container.capnp index 7fdf41005af..441577ae957 100644 --- a/src/workerd/io/container.capnp +++ b/src/workerd/io/container.capnp @@ -253,12 +253,16 @@ interface Container @0x9aaceefc06523bca { inspect @14 () -> (info :InspectInfo); # Returns information about the container, or `none` if the container has not been started. + setLabels @15 (labels :List(Label)); + # Replaces the container's current label set with the provided list. + struct InspectInfo { union { none @0 :Void; started :group { labels @1 :List(Label); - # Echo of StartParams.labels. Empty list means start() was called with no labels. + # Current in-memory label set. Initially populated from StartParams.labels and replaced by + # setLabels(). Empty list means the current set is empty. } } } diff --git a/src/workerd/server/container-client.c++ b/src/workerd/server/container-client.c++ index 69f2f0c6201..50f9e500166 100644 --- a/src/workerd/server/container-client.c++ +++ b/src/workerd/server/container-client.c++ @@ -49,10 +49,6 @@ constexpr kj::StringPtr SNAPSHOT_CLONE_VOLUME_PREFIX = "workerd-snap-clone-"_kj; constexpr kj::StringPtr CONTAINER_SNAPSHOT_IMAGE_PREFIX = "workerd-container-snap-"_kj; constexpr kj::StringPtr SNAPSHOT_VOLUME_CREATED_AT_LABEL = "dev.workerd.snapshot-created-at"_kj; -// Prefix applied to user-supplied labels when writing them to the Docker container, and -// stripped back out when reading them via inspect(). Lets us distinguish labels the worker -// set via start() from labels that came from the image (via Dockerfile LABEL) or engine. -constexpr kj::StringPtr WORKERD_LABEL_PREFIX = "workerd-"_kj; constexpr auto SNAPSHOT_STALE_AGE = 30 * kj::DAYS; // Maximum size of a snapshot tar archive held in memory during snapshot create/restore. @@ -1545,20 +1541,11 @@ kj::Promise> ContainerClient::inspec bool running = status == "running" || status == "restarting"; kj::Vector