Skip to content

TestContainers labels#7

Merged
dragosv merged 1 commit intomainfrom
testcontainers-labels
Mar 7, 2026
Merged

TestContainers labels#7
dragosv merged 1 commit intomainfrom
testcontainers-labels

Conversation

@dragosv
Copy link
Owner

@dragosv dragosv commented Mar 7, 2026

Add standard testcontainers labels to all containers and networks

Summary

This PR implements the testcontainers label specification, automatically injecting four standard org.testcontainers.* labels on every container and network created by the library. Previously only user-supplied labels were applied — no standard metadata was set.

Motivation

  • Specification compliance — all official testcontainers libraries (Go, Java, .NET, etc.) tag managed resources with a standard set of org.testcontainers.* labels. This PR brings testcontainers-zig in line with that convention.
  • Resource reaping — the org.testcontainers.sessionId label enables future Ryuk-style reaper support to clean up orphaned containers.
  • Observabilitydocker ps --filter label=org.testcontainers now reliably identifies all testcontainers-managed resources.

Standard labels injected

Key Value Description
org.testcontainers "true" Marks the resource as testcontainers-managed
org.testcontainers.lang "zig" Language that created the container
org.testcontainers.version "0.1.0" Library version
org.testcontainers.sessionId UUID v4 Per-process session identifier for reaping

What changed

Area Change
Label constants Added label_base, label_lang, label_version, label_session_id, and tc_version in docker_client.zig
UUID v4 session ID Added formatUuidV4 and lazy getSessionId() — generates a per-process UUID on first call using std.crypto.random
Container labels buildCreateBody now always injects the 4 standard labels before user labels (user labels take precedence)
Network labels buildNetworkCreateBody now always injects the same 4 standard labels
Public API Re-exported label constants, tc_version, and getSessionId from root.zig
Unit tests Added 7 new tests: UUID formatting, session ID stability, label injection on containers/networks, label merging, user override
Integration test Extended CustomLabelsImage to verify all 4 standard labels on inspected containers

Key new internal components

All located in src/docker_client.zig:

  • formatUuidV4 — formats 16 random bytes as a RFC 4122 UUID v4 string (36 characters)
  • getSessionId — lazily generates and caches a per-process UUID v4 session ID
  • putDefaultLabels — writes the 4 standard labels into a std.json.ObjectMap

Design decisions

  • Labels always present — even when the user provides no labels, the Labels JSON object is emitted. This matches testcontainers-go behavior.
  • User labels override — user-supplied labels are written after defaults, so a user can override any standard label if needed.
  • Lazy session ID — the UUID is generated once on first access and cached in a module-level variable, avoiding repeated allocation.
  • No std.fmt.fmtSliceHexLower — that API doesn't exist in Zig 0.15.2, so a manual formatUuidV4 function handles hex encoding.

Testing

  • zig build test --summary all — all 59 unit tests pass (no Docker required)
  • zig build integration-test --summary all — integration suite validates labels on real containers

Summary by Sourcery

Add standard Testcontainers metadata labels and a per-process session ID to all Docker containers and networks created by the library.

New Features:

  • Automatically tag all managed containers and networks with standard org.testcontainers.* labels, including language, library version, and session ID.

Enhancements:

  • Expose standard label keys, library version, and session ID helper via the public API for external consumers.
  • Introduce a lazily generated UUID v4 session identifier to support consistent labeling and future resource reaping.

Tests:

  • Add unit tests covering UUID formatting, session ID stability, default label injection, user label merging, and override behavior.
  • Extend integration tests to assert the presence and correctness of standard Testcontainers labels on real containers.

Summary by CodeRabbit

Release Notes

  • New Features

    • Exposed Testcontainers standard labels and version metadata as public API.
    • Containers and networks now automatically include standard metadata labels (base, language, version, and session ID).
    • Added session ID accessor for process identification and tracking.
  • Tests

    • Expanded test coverage for label injection, session stability, and metadata behavior.

@sourcery-ai
Copy link

sourcery-ai bot commented Mar 7, 2026

Reviewer's Guide

Implements standard Testcontainers labels and session ID support, ensuring all containers and networks created by the library are tagged with a consistent set of metadata, and exposes related constants and helpers via the public API with accompanying tests.

Sequence diagram for container creation label injection

sequenceDiagram
    actor TestCode
    participant DockerClient
    participant JsonBuilder
    participant SessionIdCache
    participant StdCryptoRandom

    TestCode->>DockerClient: buildCreateBody(allocator, containerRequest)
    DockerClient->>JsonBuilder: init root JSON object

    DockerClient->>JsonBuilder: init lbl_obj ObjectMap
    DockerClient->>DockerClient: putDefaultLabels(lbl_obj)

    activate DockerClient
    DockerClient->>SessionIdCache: getSessionId()
    alt first_call
        SessionIdCache->>StdCryptoRandom: bytes(random_bytes[16])
        StdCryptoRandom-->>SessionIdCache: random_bytes
        SessionIdCache->>SessionIdCache: set UUID version and variant bits
        SessionIdCache->>SessionIdCache: formatUuidV4(session_id_buf, random_bytes)
        SessionIdCache->>SessionIdCache: session_id_initialized = true
        SessionIdCache-->>DockerClient: session_id_buf
    else cached_value
        SessionIdCache-->>DockerClient: session_id_buf
    end
    DockerClient->>JsonBuilder: put(label_base, "true")
    DockerClient->>JsonBuilder: put(label_lang, "zig")
    DockerClient->>JsonBuilder: put(label_version, tc_version)
    DockerClient->>JsonBuilder: put(label_session_id, session_id_buf)
    deactivate DockerClient

    loop for each user_label in containerRequest.labels
        DockerClient->>JsonBuilder: put(user_label.key, user_label.value)
    end

    DockerClient->>JsonBuilder: root.object.put(Labels, lbl_obj)
    DockerClient-->>TestCode: JSON body with merged labels
Loading

Sequence diagram for network creation label injection

sequenceDiagram
    actor TestCode
    participant DockerClient
    participant JsonBuilder
    participant SessionIdCache

    TestCode->>DockerClient: buildNetworkCreateBody(allocator, name, driver, labels)
    DockerClient->>JsonBuilder: init root JSON object
    DockerClient->>JsonBuilder: put(Driver, driver)
    DockerClient->>JsonBuilder: put(CheckDuplicate, true)

    DockerClient->>JsonBuilder: init lbl_obj ObjectMap
    DockerClient->>DockerClient: putDefaultLabels(lbl_obj)
    DockerClient->>SessionIdCache: getSessionId()
    SessionIdCache-->>DockerClient: cached session_id_buf

    loop for each user_label in labels
        DockerClient->>JsonBuilder: put(user_label.key, user_label.value)
    end

    DockerClient->>JsonBuilder: root.object.put(Labels, lbl_obj)
    DockerClient-->>TestCode: JSON body with merged labels
Loading

Class diagram for docker_client label and session ID helpers

classDiagram
    class docker_client_module {
        <<module>>
        +const docker_socket : [*]const u8
        +const api_version : [*]const u8
        +const label_base : [*]const u8
        +const label_lang : [*]const u8
        +const label_version : [*]const u8
        +const label_session_id : [*]const u8
        +const tc_version : [*]const u8
        -var session_id_buf : [36]u8
        -var session_id_initialized : bool
        -fn formatUuidV4(buf : *[36]u8, bytes : [16]u8) void
        +fn getSessionId() []const u8
        -fn putDefaultLabels(obj : *std_json_ObjectMap) !void
        +fn buildCreateBody(allocator : std_mem_Allocator, req : *const container_mod_ContainerRequest) ![]u8
        +fn buildNetworkCreateBody(allocator : std_mem_Allocator, name : []const u8, driver : []const u8, labels : []const container_mod_KV) ![]u8
    }

    class root_module {
        <<module>>
        +const DockerClient : type
        +const docker_socket : [*]const u8
        +const label_base : [*]const u8
        +const label_lang : [*]const u8
        +const label_version : [*]const u8
        +const label_session_id : [*]const u8
        +const tc_version : [*]const u8
        +fn getSessionId() []const u8
    }

    docker_client_module <.. root_module : reexports

    class std_json_ObjectMap {
        <<struct>>
        +fn put(key : []const u8, value : std_json_Value) !void
    }

    class container_mod_ContainerRequest {
        <<struct>>
        +image : []const u8
        +labels : []const container_mod_KV
    }

    class container_mod_KV {
        <<struct>>
        +key : []const u8
        +value : []const u8
    }

    class std_mem_Allocator {
        <<struct>>
    }

    docker_client_module --> std_json_ObjectMap : uses
    docker_client_module --> container_mod_ContainerRequest : uses
    docker_client_module --> container_mod_KV : uses
    docker_client_module --> std_mem_Allocator : uses
Loading

File-Level Changes

Change Details Files
Introduce standard Testcontainers label constants, UUID v4 session ID generation, and default label injection helper
  • Add public constants for Testcontainers label keys and library version
  • Implement formatUuidV4 to render 16 random bytes as a RFC 4122 UUID v4 string
  • Implement lazy, cached getSessionId using std.crypto.random with proper UUID v4 version/variant bits
  • Add putDefaultLabels helper that writes the four standard labels into a std.json.ObjectMap using getSessionId
src/docker_client.zig
Always inject standard labels into container and network create requests, merging and allowing user label overrides
  • Update buildCreateBody to always create a Labels JSON object, populate default labels via putDefaultLabels, then apply user labels so they override defaults when keys clash
  • Update buildNetworkCreateBody to follow the same pattern for networks, ensuring Labels is always present and merged
src/docker_client.zig
Re-export label/session metadata in the public API and validate behavior with unit and integration tests
  • Re-export label constants, tc_version, and getSessionId from root.zig for external consumers
  • Add unit tests covering UUID formatting, session ID stability, container/network label injection, label merging, and user overrides
  • Extend CustomLabelsImage integration test to assert presence and values of the four standard labels on inspected containers
src/root.zig
src/docker_client.zig
src/integration_test.zig

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 7, 2026

📝 Walkthrough

Walkthrough

Introduced standardized Testcontainers label support for containers and networks. Added public label constants (base, language, version, session ID) and a session ID accessor that generates and caches UUID v4 strings. Updated container/network creation logic to inject default labels and merge with user-supplied labels. Extended public API exports and test coverage.

Changes

Cohort / File(s) Summary
Label Constants & Session ID
src/docker_client.zig
Added public constants label_base, label_lang, label_version, label_session_id, and tc_version. Introduced formatUuidV4() helper for UUID string rendering, getSessionId() public function for lazy session ID generation and caching, and putDefaultLabels() helper to inject standard labels into container/network objects. Updated buildCreateBody() and buildNetworkCreateBody() to apply default labels before user-supplied labels.
Public API Exports
src/root.zig
Exported label constants and getSessionId() function from docker_client.zig to extend public module interface.
Test Coverage
src/integration_test.zig
Expanded test assertions to verify standard Testcontainers labels are correctly injected into containers, validating base, language, version, and session ID label presence and values.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 Behold! We label all the boxes with care,
Base, language, version dancing through the air,
Session IDs in UUID glory bloom—
Each container now knows its rightful room!

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title 'TestContainers labels' directly and accurately summarizes the main change—adding standard testcontainers labels to containers and networks, which is the core objective of the PR.
Description check ✅ Passed The PR description is comprehensive and well-structured, covering all template sections with substantial detail: motivation, implementation details, testing results, and design decisions are thoroughly documented.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch testcontainers-labels

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 1 issue, and left some high level feedback:

  • The lazy session ID caching uses module-level mutable state (session_id_initialized and session_id_buf) without synchronization, which could lead to data races if the library is used from multiple threads; consider making initialization thread-safe (e.g. via std.Thread.Mutex or std.once-style logic).
  • The hardcoded tc_version = "0.1.0" risks drifting from the actual library version; it might be more robust to centralize this in a single version definition or derive it from build metadata so updates don’t require touching this constant.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The lazy session ID caching uses module-level mutable state (`session_id_initialized` and `session_id_buf`) without synchronization, which could lead to data races if the library is used from multiple threads; consider making initialization thread-safe (e.g. via `std.Thread.Mutex` or `std.once`-style logic).
- The hardcoded `tc_version = "0.1.0"` risks drifting from the actual library version; it might be more robust to centralize this in a single version definition or derive it from build metadata so updates don’t require touching this constant.

## Individual Comments

### Comment 1
<location path="src/docker_client.zig" line_range="62-71" />
<code_context>
+
+/// Returns the per-process session ID as a UUID v4 string.
+/// The value is generated lazily on first call and cached.
+pub fn getSessionId() []const u8 {
+    if (!session_id_initialized) {
+        var bytes: [16]u8 = undefined;
+        std.crypto.random.bytes(&bytes);
+        // UUID v4: set version nibble to 4, variant bits to 10xx
+        bytes[6] = (bytes[6] & 0x0f) | 0x40;
+        bytes[8] = (bytes[8] & 0x3f) | 0x80;
+        formatUuidV4(&session_id_buf, bytes);
+        session_id_initialized = true;
+    }
+    return &session_id_buf;
+}
+
</code_context>
<issue_to_address>
**issue (bug_risk):** Session ID lazy init is not thread-safe and can race under concurrent access.

`session_id_initialized` and `session_id_buf` are written concurrently with no synchronization, so multiple threads can enter the initialization path and write overlapping random data, producing corrupted UUIDs. Guard this with a one-time init mechanism such as `std.Thread.once`, an atomic boolean, or another primitive that ensures only a single thread performs initialization and others only read the fully initialized buffer.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +62 to +71
pub fn getSessionId() []const u8 {
if (!session_id_initialized) {
var bytes: [16]u8 = undefined;
std.crypto.random.bytes(&bytes);
// UUID v4: set version nibble to 4, variant bits to 10xx
bytes[6] = (bytes[6] & 0x0f) | 0x40;
bytes[8] = (bytes[8] & 0x3f) | 0x80;
formatUuidV4(&session_id_buf, bytes);
session_id_initialized = true;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): Session ID lazy init is not thread-safe and can race under concurrent access.

session_id_initialized and session_id_buf are written concurrently with no synchronization, so multiple threads can enter the initialization path and write overlapping random data, producing corrupted UUIDs. Guard this with a one-time init mechanism such as std.Thread.once, an atomic boolean, or another primitive that ensures only a single thread performs initialization and others only read the fully initialized buffer.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/docker_client.zig`:
- Around line 57-73: The lazy initialization of session_id_buf in getSessionId
is not thread-safe; wrap the initialization with a synchronization primitive so
only one thread generates and writes the UUID and others wait/see the
initialized value. Replace the plain bool session_id_initialized and unprotected
write to session_id_buf with either a mutex (std.Thread.Mutex) that locks around
the generation and call to formatUuidV4 inside getSessionId, or use an atomic
flag (std.atomic.AtomicBool or `@atomicCompareExchange` on session_id_initialized)
to perform a compare-and-swap "once" and ensure writers coordinate before
writing session_id_buf; update callers to return the buffer after the protected
initialization and ensure session_id_buf remains a stable global.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f6cdbbd0-d273-4e84-bca8-ff93560ddb15

📥 Commits

Reviewing files that changed from the base of the PR and between ef08c78 and 943f443.

📒 Files selected for processing (3)
  • src/docker_client.zig
  • src/integration_test.zig
  • src/root.zig

Comment on lines +57 to +73
var session_id_buf: [36]u8 = undefined;
var session_id_initialized: bool = false;

/// Returns the per-process session ID as a UUID v4 string.
/// The value is generated lazily on first call and cached.
pub fn getSessionId() []const u8 {
if (!session_id_initialized) {
var bytes: [16]u8 = undefined;
std.crypto.random.bytes(&bytes);
// UUID v4: set version nibble to 4, variant bits to 10xx
bytes[6] = (bytes[6] & 0x0f) | 0x40;
bytes[8] = (bytes[8] & 0x3f) | 0x80;
formatUuidV4(&session_id_buf, bytes);
session_id_initialized = true;
}
return &session_id_buf;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Race condition in lazy session ID initialization.

session_id_initialized and session_id_buf are global mutable state accessed without synchronization. If multiple threads call getSessionId() concurrently (e.g., in a multi-threaded application creating containers in parallel), the check-then-set pattern can result in:

  1. Multiple threads generating different UUIDs
  2. Torn writes to session_id_buf
  3. Non-deterministic session IDs across containers

Consider using std.Thread.Mutex or @atomicStore/@atomicLoad for thread-safe initialization.

🔒 Proposed fix using atomic once-initialization pattern
-var session_id_buf: [36]u8 = undefined;
-var session_id_initialized: bool = false;
+var session_id_buf: [36]u8 = undefined;
+var session_id_state: std.atomic.Value(u8) = std.atomic.Value(u8).init(0);
+// 0 = uninitialized, 1 = initializing, 2 = initialized

 /// Returns the per-process session ID as a UUID v4 string.
 /// The value is generated lazily on first call and cached.
 pub fn getSessionId() []const u8 {
-    if (!session_id_initialized) {
+    // Double-checked locking with atomics
+    if (session_id_state.load(.acquire) == 2) {
+        return &session_id_buf;
+    }
+    // Try to claim initialization
+    if (session_id_state.cmpxchgStrong(0, 1, .acquire, .monotonic) == null) {
         var bytes: [16]u8 = undefined;
         std.crypto.random.bytes(&bytes);
         // UUID v4: set version nibble to 4, variant bits to 10xx
         bytes[6] = (bytes[6] & 0x0f) | 0x40;
         bytes[8] = (bytes[8] & 0x3f) | 0x80;
         formatUuidV4(&session_id_buf, bytes);
-        session_id_initialized = true;
+        session_id_state.store(2, .release);
+    } else {
+        // Another thread is initializing; spin until done
+        while (session_id_state.load(.acquire) != 2) {
+            std.atomic.spinLoopHint();
+        }
     }
     return &session_id_buf;
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
var session_id_buf: [36]u8 = undefined;
var session_id_initialized: bool = false;
/// Returns the per-process session ID as a UUID v4 string.
/// The value is generated lazily on first call and cached.
pub fn getSessionId() []const u8 {
if (!session_id_initialized) {
var bytes: [16]u8 = undefined;
std.crypto.random.bytes(&bytes);
// UUID v4: set version nibble to 4, variant bits to 10xx
bytes[6] = (bytes[6] & 0x0f) | 0x40;
bytes[8] = (bytes[8] & 0x3f) | 0x80;
formatUuidV4(&session_id_buf, bytes);
session_id_initialized = true;
}
return &session_id_buf;
}
var session_id_buf: [36]u8 = undefined;
var session_id_state: std.atomic.Value(u8) = std.atomic.Value(u8).init(0);
// 0 = uninitialized, 1 = initializing, 2 = initialized
/// Returns the per-process session ID as a UUID v4 string.
/// The value is generated lazily on first call and cached.
pub fn getSessionId() []const u8 {
// Double-checked locking with atomics
if (session_id_state.load(.acquire) == 2) {
return &session_id_buf;
}
// Try to claim initialization
if (session_id_state.cmpxchgStrong(0, 1, .acquire, .monotonic) == null) {
var bytes: [16]u8 = undefined;
std.crypto.random.bytes(&bytes);
// UUID v4: set version nibble to 4, variant bits to 10xx
bytes[6] = (bytes[6] & 0x0f) | 0x40;
bytes[8] = (bytes[8] & 0x3f) | 0x80;
formatUuidV4(&session_id_buf, bytes);
session_id_state.store(2, .release);
} else {
// Another thread is initializing; spin until done
while (session_id_state.load(.acquire) != 2) {
std.atomic.spinLoopHint();
}
}
return &session_id_buf;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/docker_client.zig` around lines 57 - 73, The lazy initialization of
session_id_buf in getSessionId is not thread-safe; wrap the initialization with
a synchronization primitive so only one thread generates and writes the UUID and
others wait/see the initialized value. Replace the plain bool
session_id_initialized and unprotected write to session_id_buf with either a
mutex (std.Thread.Mutex) that locks around the generation and call to
formatUuidV4 inside getSessionId, or use an atomic flag (std.atomic.AtomicBool
or `@atomicCompareExchange` on session_id_initialized) to perform a
compare-and-swap "once" and ensure writers coordinate before writing
session_id_buf; update callers to return the buffer after the protected
initialization and ensure session_id_buf remains a stable global.

@dragosv dragosv merged commit b7025d4 into main Mar 7, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant