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:
- Only the HTTP headers (body in next read)
- Headers + partial body
- Complete response
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
| Type | HTTP Status | Retryable? |
|---|---|---|
| Connection Error | N/A | Yes |
| Timeout Error | N/A | Yes |
| Not Found | 404 | No |
| Conflict | 409 | No |
| Client Error | 4xx | No |
| Server Error | 5xx | Maybe |
Logging
simple_docker uses simple_logger for structured logging. Log levels:
- DEBUG - Request/response details, parsing info
- INFO - Container operations, image pulls
- WARNING - Recoverable issues
- ERROR - Operation failures
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?
| Library | Purpose |
|---|---|
| simple_ipc | Cross-platform IPC. Named pipes on Windows, Unix sockets on Linux. |
| simple_json | Parse Docker API responses, build request bodies. |
| simple_file | File operations for volume mounts, config files. |
| simple_logger | Structured logging for debugging and monitoring. |
Future Architecture
Planned Additions
- COMPOSE_BUILDER - docker-compose.yaml generation
- DOCKER_REGISTRY_AUTH - Private registry authentication
- EIFFEL_CONTAINER_TEMPLATES - Eiffel-specific container templates
- Unix Socket Support - Via simple_ipc Unix socket support
Implemented P2 Features
- DOCKERFILE_BUILDER - Fluent Dockerfile generation with multi-stage support
- DOCKER_NETWORK - Network management (list, create, connect, disconnect)
- DOCKER_VOLUME - Volume management (list, create, remove)
- Exec Operations - Execute commands in running containers