simple_docker

Architecture

Architecture Overview

simple_docker is designed as a facade layer over the Docker Engine API. It communicates with Docker via Windows named pipes, sending HTTP/1.1 requests and parsing JSON responses.

Class Hierarchy

DOCKER_CLIENT (facade)
    |
    +-- CONTAINER_SPEC (builder)
    |
    +-- DOCKER_CONTAINER (model)
    |
    +-- DOCKER_IMAGE (model)
    |
    +-- DOCKER_NETWORK (model)
    |
    +-- DOCKER_VOLUME (model)
    |
    +-- DOCKERFILE_BUILDER (builder)
    |
    +-- DOCKER_ERROR (error handling)
    |
    +-- CONTAINER_STATE (constants)

Design Patterns

Facade Pattern

DOCKER_CLIENT provides a simple interface to complex Docker API operations.

Builder Pattern

CONTAINER_SPEC uses fluent interface for container configuration.

Value Objects

DOCKER_CONTAINER and DOCKER_IMAGE are immutable representations.

Docker Communication

Transport Layer

On Windows, Docker Engine exposes its API via a named pipe at \\.\pipe\docker_engine. We use simple_ipc to communicate with this pipe.

-- Connection via simple_ipc
create connection.make_client ("docker_engine")

-- All API calls are HTTP/1.1 over the pipe
connection.write_string ("GET /v1.45/version HTTP/1.1%R%NHost: localhost%R%N%R%N")
response := connection.read_string (buffer_size)

API Version

We target Docker Engine API v1.45 (Docker 24.0+). The version is included in all request paths: /v1.45/containers/json.

Request/Response Flow

1. Client calls DOCKER_CLIENT.list_containers (True)

2. DOCKER_CLIENT builds HTTP request:
   GET /v1.45/containers/json?all=true HTTP/1.1
   Host: localhost

3. Request sent via named pipe (SIMPLE_IPC)

4. Docker responds with HTTP/1.1 + chunked body:
   HTTP/1.1 200 OK
   Content-Type: application/json
   Transfer-Encoding: chunked

   [{"Id":"abc123...","Names":["/my-container"],...}]

5. Response parsed, DOCKER_CONTAINER objects created

6. Result returned to caller

Chunked Transfer Encoding

Docker uses HTTP/1.1 chunked transfer encoding for responses. This requires special handling since the response body may arrive in multiple reads.

Challenge

Named pipes are stream-based. A single read_string call may return:

Solution

-- Keep reading until we get the terminating chunk
from
    l_read_more := not l_response.has_substring ("%R%N0%R%N")
until
    not l_read_more or l_max_reads <= 0
loop
    l_chunk := connection.read_string (buffer_size)
    if l_chunk.count > 0 then
        l_response.append (l_chunk)
        l_read_more := not l_response.has_substring ("%R%N0%R%N")
    else
        l_read_more := False
    end
    l_max_reads := l_max_reads - 1
end

Chunk Decoding

-- Chunked body format:
-- size_in_hex\r\n
-- data\r\n
-- size_in_hex\r\n
-- data\r\n
-- 0\r\n
-- \r\n

decode_chunked_body (a_chunked: STRING): STRING
    local
        l_pos, l_chunk_size: INTEGER
        l_size_str: STRING
    do
        create Result.make (a_chunked.count)
        from l_pos := 1 until l_pos > a_chunked.count loop
            -- Read chunk size (hex)
            l_size_str := read_until_crlf (a_chunked, l_pos)
            l_chunk_size := hex_to_integer (l_size_str)

            if l_chunk_size = 0 then
                -- End of chunks
                l_pos := a_chunked.count + 1
            else
                -- Append chunk data
                Result.append (a_chunked.substring (l_pos, l_pos + l_chunk_size - 1))
                l_pos := l_pos + l_chunk_size + 2  -- Skip \r\n
            end
        end
    end

Design by Contract

All classes use full Design by Contract with preconditions, postconditions, and invariants.

CONTAINER_SPEC Contracts

set_name (a_name: STRING): like Current
    require
        name_not_void: a_name /= Void
        name_not_empty: not a_name.is_empty
    do
        name := a_name
        Result := Current
    ensure
        name_set: name.same_string (a_name)
        result_is_current: Result = Current
    end

DOCKER_CONTAINER Invariants

invariant
    id_exists: id /= Void
    short_id_exists: short_id /= Void
    short_id_length_valid: short_id.count <= 12
    short_id_consistency: (not id.is_empty and id.count >= short_id.count)
        implies id.starts_with (short_id)
    names_exists: names /= Void
    labels_exists: labels /= Void
    ports_exists: ports /= Void

CONTAINER_STATE Postconditions

can_start (a_state: STRING): BOOLEAN
    require
        state_not_void: a_state /= Void
        state_not_empty: not a_state.is_empty
    do
        Result := a_state.same_string (created) or else a_state.same_string (exited)
    ensure
        created_can_start: a_state.same_string (created) implies Result
        running_cannot_start: a_state.same_string (running) implies not Result
        paused_cannot_start: a_state.same_string (paused) implies not Result
    end

Error Handling

Errors are captured in DOCKER_ERROR objects rather than exceptions. This allows callers to handle errors gracefully.

Error Flow

1. Operation fails (connection error, 404, 409, etc.)

2. DOCKER_CLIENT sets:
   - has_error := True
   - last_error := create {DOCKER_ERROR}.make_from_status (...)

3. Caller checks:
   if client.has_error then
       if attached client.last_error as err then
           -- Handle based on error type
       end
   end

4. Next successful operation clears error state

Error Classification

TypeHTTP StatusRetryable?
Connection ErrorN/AYes
Timeout ErrorN/AYes
Not Found404No
Conflict409No
Client Error4xxNo
Server Error5xxMaybe

Logging

simple_docker uses simple_logger for structured logging. Log levels:

Enabling Debug Logging

local
    logger: SIMPLE_LOGGER
do
    create logger.make ("docker")
    logger.set_level_debug

    -- Now all requests/responses are logged
end

Dependencies

Dependency Graph

simple_docker
    |
    +-- simple_ipc      (named pipe communication)
    |
    +-- simple_json     (JSON parsing/building)
    |
    +-- simple_file     (file operations)
    |
    +-- simple_logger   (logging)
    |
    +-- EiffelBase      (core Eiffel library)

Why These Dependencies?

LibraryPurpose
simple_ipcCross-platform IPC. Named pipes on Windows, Unix sockets on Linux.
simple_jsonParse Docker API responses, build request bodies.
simple_fileFile operations for volume mounts, config files.
simple_loggerStructured logging for debugging and monitoring.

Future Architecture

Planned Additions

Implemented P2 Features