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
18 changes: 18 additions & 0 deletions src/workerd/api/container.c++
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,24 @@ void Container::start(jsg::Lock& js, jsg::Optional<StartupOptions> maybeOptions)
}
}

KJ_IF_SOME(labels, options.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<kj::byte>(c) >= 0x20, Error,
"Label names cannot contain control characters (index ", i, ")");
}
for (auto c: field.value) {
JSG_REQUIRE(static_cast<kj::byte>(c) >= 0x20, Error,
"Label values cannot contain control characters (index ", i, ")");
}
list[i].setName(field.name);
Comment thread
martinezjandrew marked this conversation as resolved.
list[i].setValue(field.value);
}
}

req.setCompatibilityFlags(flags);

IoContext::current().addTask(req.sendIgnoringResult());
Expand Down
5 changes: 4 additions & 1 deletion src/workerd/api/container.h
Original file line number Diff line number Diff line change
Expand Up @@ -29,23 +29,26 @@ class Container: public jsg::Object {
bool enableInternet = false;
jsg::Optional<jsg::Dict<kj::String>> env;
jsg::Optional<int64_t> hardTimeout;
jsg::Optional<jsg::Dict<kj::String>> labels;

// TODO(containers): Allow intercepting stdin/stdout/stderr by specifying streams here.

JSG_STRUCT(entrypoint, enableInternet, env, hardTimeout);
JSG_STRUCT(entrypoint, enableInternet, env, hardTimeout, labels);
JSG_STRUCT_TS_OVERRIDE_DYNAMIC(CompatibilityFlags::Reader flags) {
if (flags.getWorkerdExperimental()) {
JSG_TS_OVERRIDE(ContainerStartupOptions {
entrypoint?: string[];
enableInternet: boolean;
env?: Record<string, string>;
hardTimeout?: number | bigint;
labels?: Record<string, string>;
});
} else {
JSG_TS_OVERRIDE(ContainerStartupOptions {
entrypoint?: string[];
enableInternet: boolean;
env?: Record<string, string>;
labels?: Record<string, string>;
});
}
}
Expand Down
8 changes: 8 additions & 0 deletions src/workerd/io/container.capnp
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,14 @@ interface Container @0x9aaceefc06523bca {

compatibilityFlags @4 :CompatibilityFlags;
# Compatibility flags for this worker

labels @5 :List(Label);
# Optional key-value metadata labels for metrics/observability.
}

struct Label {
name @0 :Text;
value @1 :Text;
}

monitor @2 () -> (exitCode: Int32);
Expand Down
10 changes: 10 additions & 0 deletions src/workerd/server/container-client.c++
Original file line number Diff line number Diff line change
Expand Up @@ -790,6 +790,16 @@ kj::Promise<void> ContainerClient::createContainer(
jsonEnv.set(envSize + i, defaultEnv[i]);
}

// Pass user-supplied labels as Docker object labels, visible via `docker inspect`.
if (params.hasLabels()) {
auto lbls = params.getLabels();
auto labelsObj = jsonRoot.initLabels().initObject(lbls.size());
for (auto i: kj::zeroTo(lbls.size())) {
labelsObj[i].setName(lbls[i].getName());
labelsObj[i].initValue().setString(lbls[i].getValue());
}
}

auto hostConfig = jsonRoot.initHostConfig();
// We need to set a restart policy to avoid having ambiguous states
// where the container we're managing is stuck at "exited" state.
Expand Down
74 changes: 74 additions & 0 deletions src/workerd/server/tests/container-client/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,58 @@ export class DurableObjectExample extends DurableObject {
await monitor;
}

async testLabels() {
const container = this.ctx.container;
if (container.running) {
let monitor = container.monitor().catch((_err) => {});
await container.destroy();
await monitor;
}

assert.strictEqual(container.running, false);

container.start({
enableInternet: true,
labels: { team: 'workers', environment: 'testing' },
Comment thread
martinezjandrew marked this conversation as resolved.
});

const monitor = container.monitor().catch((_err) => {});
await this.waitUntilContainerIsHealthy();

assert.strictEqual(container.running, true);

await container.destroy();
await monitor;
assert.strictEqual(container.running, false);
}

async testLabelValidation() {
const container = this.ctx.container;
if (container.running) {
let monitor = container.monitor().catch((_err) => {});
await container.destroy();
await monitor;
}

assert.strictEqual(container.running, false);

// Empty label name
assert.throws(() => container.start({ labels: { '': 'value' } }), {
message: /Label names cannot be empty/,
});

// Label name with control character
assert.throws(
() => container.start({ labels: { 'bad\x01name': 'value' } }),
{ message: /Label names cannot contain control characters \(index 0\)/ }
);

// Label value with control character
assert.throws(() => container.start({ labels: { name: 'bad\x01value' } }), {
message: /Label values cannot contain control characters \(index 0\)/,
});
}

async testPidNamespace() {
const container = this.ctx.container;
if (container.running) {
Expand Down Expand Up @@ -899,6 +951,28 @@ export const testSetInactivityTimeout = {
},
};

// Test that custom labels are passed through to the container
export const testLabels = {
async test(_ctrl, env) {
const id = env.MY_CONTAINER.idFromName(
getRandomDurableObjectName('testLabels')
);
const stub = env.MY_CONTAINER.get(id);
await stub.testLabels();
},
};

// Test that invalid labels are rejected with clear error messages
export const testLabelValidation = {
async test(_ctrl, env) {
const id = env.MY_CONTAINER.idFromName(
getRandomDurableObjectName('testLabelValidation')
);
const stub = env.MY_CONTAINER.get(id);
await stub.testLabelValidation();
},
};

// Test PID namespace isolation behavior
// When containers_pid_namespace is ENABLED, the container has its own isolated PID namespace.
// We verify this by checking that PID 1 in the container's namespace is the container's
Expand Down
1 change: 1 addition & 0 deletions types/generated-snapshot/experimental/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3894,6 +3894,7 @@ interface ContainerStartupOptions {
enableInternet: boolean;
env?: Record<string, string>;
hardTimeout?: number | bigint;
labels?: Record<string, string>;
}
/**
* The **`FileSystemHandle`** interface of the File System API is an object which represents a file or directory entry.
Expand Down
1 change: 1 addition & 0 deletions types/generated-snapshot/experimental/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3900,6 +3900,7 @@ export interface ContainerStartupOptions {
enableInternet: boolean;
env?: Record<string, string>;
hardTimeout?: number | bigint;
labels?: Record<string, string>;
}
/**
* The **`FileSystemHandle`** interface of the File System API is an object which represents a file or directory entry.
Expand Down
1 change: 1 addition & 0 deletions types/generated-snapshot/latest/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3772,6 +3772,7 @@ interface ContainerStartupOptions {
enableInternet: boolean;
env?: Record<string, string>;
hardTimeout?: number | bigint;
labels?: Record<string, string>;
}
/**
* The **`MessagePort`** interface of the Channel Messaging API represents one of the two ports of a MessageChannel, allowing messages to be sent from one port and listening out for them arriving at the other.
Expand Down
1 change: 1 addition & 0 deletions types/generated-snapshot/latest/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3778,6 +3778,7 @@ export interface ContainerStartupOptions {
enableInternet: boolean;
env?: Record<string, string>;
hardTimeout?: number | bigint;
labels?: Record<string, string>;
}
/**
* The **`MessagePort`** interface of the Channel Messaging API represents one of the two ports of a MessageChannel, allowing messages to be sent from one port and listening out for them arriving at the other.
Expand Down
Loading