docker-swarm 0.7.2 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +28 -0
- data/README.md +14 -7
- data/docs/behavior/behavior.md +106 -11
- data/docs/consumed/docker-engine-api.md +126 -0
- data/docs/errors/errors.md +103 -0
- data/docs/glossary/glossary.md +25 -25
- data/docs/interface/interface.md +121 -0
- data/docs/release/release.md +54 -0
- data/docs/test/testing.md +100 -0
- data/docs/topology/topology.md +55 -0
- data/lib/docker_swarm/api.rb +8 -4
- data/lib/docker_swarm/concerns/creatable.rb +32 -6
- data/lib/docker_swarm/concerns/updatable.rb +15 -4
- data/lib/docker_swarm/connection.rb +15 -6
- data/lib/docker_swarm/log_helper.rb +60 -3
- data/lib/docker_swarm/middleware/log_stream_demuxer.rb +89 -0
- data/lib/docker_swarm/models/container.rb +10 -0
- data/lib/docker_swarm/models/image.rb +85 -1
- data/lib/docker_swarm/registry_auth.rb +48 -0
- data/lib/docker_swarm/version.rb +1 -1
- data/lib/docker_swarm.rb +2 -0
- data/skill/SKILL.md +24 -12
- metadata +15 -7
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8e27b0beec0476baca5222a41e38aa7b11998641c39cc15eeb16390343e8364b
|
|
4
|
+
data.tar.gz: 61faa29cc088ea4286fb864b0b2237bfd104059236e4b748168344148931ab9e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 01b0508429e58f4878e8aa5bba874e67d57e87bf691c31ebc144da22cbeb86d89dc438f268ab85374009949e28296546747be6e0b03b718ddaa4a2df6f1e521b
|
|
7
|
+
data.tar.gz: bc02c9d23be1bacc5a8f7bee2e105cf0fdc90a75fa96d4257c41467bff9b7369a031c6c21644ac3cfc84159910bd10657371b2b9407d4c9bb270cf081d52af23
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,34 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [0.9.0] — 2026-08-03
|
|
6
|
+
|
|
7
|
+
### Nuevas funcionalidades
|
|
8
|
+
- `Container` pasa a incluir `Concerns::Creatable`: la gema ya puede **crear** containers, no solo operar los existentes. El nombre viaja por **query string** (`POST /containers/create?name=`), que es donde el Engine lo espera — en el body lo descarta en silencio y responde `201`, dejando el container con nombre aleatorio. `Concerns::Creatable` gana `create_query_params` (default `[]`, override por modelo; `Container` declara `%w[name]`) y `query_params_for_docker`; `save` los manda en la URL y los excluye del payload. Implementa ADR-025 cláusula 1 — @gedera
|
|
9
|
+
|
|
10
|
+
### Breaking changes
|
|
11
|
+
- **`logs` devuelve texto demultiplexado.** Sin TTY el Engine enmarca cada fragmento con 8 bytes de cabecera (tipo de stream · relleno · tamaño big-endian); el nuevo `Middleware::LogStreamDemuxer` los saca, así que `Loggable#logs` entrega texto limpio en `Container`, `Service` y `Task`. **Cambia el valor de retorno** para cualquier consumidor que hoy reciba el body crudo. Un stream sin framing (TTY) pasa intacto. Implementa ADR-025 cláusula 3 — @gedera
|
|
12
|
+
- El dispatch **no** se decide solo por `Content-Type`: `application/vnd.docker.multiplexed-stream` existe desde la **API v1.42**, y antes un stream multiplexado viajaba igual como `application/vnd.docker.raw-stream`. Como la gema no fija `?version=`, un Engine que tope en v1.41 devuelve `raw-stream` **con** framing. Ante `raw-stream` el middleware valida la **forma del frame** y solo demultiplexa si la cadena cierra de punta a punta; ante cualquier inconsistencia devuelve el body intacto. Registrado en **ADR-027**, que corrige un dato de apoyo del §Alternativas de ADR-025 **sin** superseder su Decisión.
|
|
13
|
+
|
|
14
|
+
### Seguridad
|
|
15
|
+
- **`LogHelper.sanitize` redacta los secretos que viajan como `"CLAVE=VALOR"` en `Env`** (#24). Antes redactaba solo por **clave de hash**: un String caía al `else` y pasaba intacto, y el `Env` de un `ContainerSpec` es un **array de strings** `"CLAVE=VALOR"` donde el nombre del secreto vive *dentro* del elemento. Como `"Env"` tampoco matchea `SENSITIVE_KEYS`, el valor de todo secreto pasado por variable de entorno **se logueaba entero en `request_success` — camino feliz, nivel INFO** — en cada create/update de un service. Se agrega `redact_kv_string` y una rama `when String`: si la parte izquierda matchea `SENSITIVE_KEYS` se reemplaza el valor y **se conserva el nombre** (saber qué secreto apareció es diagnóstico útil; su valor no). El regex usa `[^=]+` a la izquierda para no partir en un `=` del valor (base64, URLs) y `/m` para valores multilínea. Afecta a **todas** las versiones anteriores — @Pslp
|
|
16
|
+
- **Hueco declarado, no cubierto por este fix:** `private_key` **no está** en `SENSITIVE_KEYS`, así que un PEM con ese nombre sigue saliendo en claro (hay un spec que lo fija como comportamiento conocido). Ampliar la lista va aparte: cambia la redacción para todos los consumidores.
|
|
17
|
+
|
|
18
|
+
### Otros cambios
|
|
19
|
+
- `spec.homepage` del gemspec pasa a `https://github.com/sequre/docker-swarm` (#27): el repo se transfirió de la cuenta personal `gedera` a la org `sequre`. De ahí derivan los cuatro metadata URIs (`source_code_uri`, `changelog_uri`, `bug_tracker_uri`, `documentation_uri`), así que desde esta versión apuntan a la ubicación nueva. Las versiones ya publicadas conservan la URL anterior — las salva el redirect de GitHub — @gedera
|
|
20
|
+
|
|
21
|
+
## [0.8.0] — 2026-07-22
|
|
22
|
+
|
|
23
|
+
### Nuevas funcionalidades
|
|
24
|
+
- `Service.create`/`Service#update`: soporte de autenticación de registry privado — `registry_auth` viaja en el header `X-Registry-Auth` y `registry_auth_from` (`spec`|`previous-spec`) en la query `registryAuthFrom` del update; mutuamente excluyentes y validados antes del request. La credencial nunca toca el payload ni el estado del modelo (helper `RegistryAuth`) — @Pslp
|
|
25
|
+
- `Image.pull`: pull explícito síncrono (consume el stream NDJSON hasta EOF, eleva error tipado ante `error`/`errorDetail`, devuelve `{status: :pulled, image_ref:, digest?}` sin `find`; el digest sale del frame `Digest: sha256:…`, verificado contra Docker 29.5.3). **Capacidad sin consumidor activo hoy**: el deploy de imágenes privadas se autentica vía `Service.create` (X-Registry-Auth distribuido a los nodos por Swarm), no por pull explícito. `Image.pull` queda disponible para un futuro requerimiento (pre-pull / warm-cache) — @Pslp
|
|
26
|
+
|
|
27
|
+
### Breaking changes
|
|
28
|
+
- `Image` deja de incluir `Creatable`: se retira `Image.create` (roto y sin consumidores) en favor de `Image.pull`. `Image` conserva `Deletable` y el listado — @Pslp
|
|
29
|
+
|
|
30
|
+
### Seguridad
|
|
31
|
+
- `LogHelper` sanitiza recursivamente los headers de autenticación (`X-Registry-Auth`, `Authorization`) para no filtrar credenciales en logs de wire-debug — @Pslp
|
|
32
|
+
|
|
5
33
|
## [0.7.2] — 2026-06-29
|
|
6
34
|
|
|
7
35
|
### Documentación
|
data/README.md
CHANGED
|
@@ -42,15 +42,22 @@ Documentación normada (RFC-001) por capa:
|
|
|
42
42
|
| Capa | Artefacto | Estado |
|
|
43
43
|
|---|---|---|
|
|
44
44
|
| Datos | — | `n/a` (gema sin DB) |
|
|
45
|
-
| Glosario | [`docs/glossary/glossary.md`](docs/glossary/glossary.md) |
|
|
46
|
-
| Comportamiento | [`docs/behavior/behavior.md`](docs/behavior/behavior.md) |
|
|
47
|
-
| Configuración | [`docs/config/configuracion.md`](docs/config/configuracion.md) |
|
|
48
|
-
|
|
|
49
|
-
|
|
|
50
|
-
|
|
|
45
|
+
| Glosario | [`docs/glossary/glossary.md`](docs/glossary/glossary.md) | completo (primitivas + arquitectura interna) |
|
|
46
|
+
| Comportamiento | [`docs/behavior/behavior.md`](docs/behavior/behavior.md) | backfill on-demand + incremental (11 flujos) |
|
|
47
|
+
| Configuración | [`docs/config/configuracion.md`](docs/config/configuracion.md) | inventario base (7 opciones, sin env vars) |
|
|
48
|
+
| Interfaz | [`docs/interface/interface.md`](docs/interface/interface.md) | API Ruby pública (11 modelos + Base + concerns) |
|
|
49
|
+
| Topología | [`docs/topology/topology.md`](docs/topology/topology.md) | 3 deps runtime + grafo de contexto |
|
|
50
|
+
| Errores | [`docs/errors/errors.md`](docs/errors/errors.md) | jerarquía + mapeo HTTP + política §c |
|
|
51
|
+
| Consumidas | [`docs/consumed/docker-engine-api.md`](docs/consumed/docker-engine-api.md) | Docker Engine API + retry/degradación §c/§e |
|
|
52
|
+
| Test | [`docs/test/testing.md`](docs/test/testing.md) | suite RSpec unit+integration + gaps/contract/PII §e-h (§g sin incidentes) |
|
|
53
|
+
| Release | [`docs/release/release.md`](docs/release/release.md) | completo — §a estructura (tag `v*`→RubyGems, patrón 1) + §b enrich (deploy/rollback/ambientes/dueño) |
|
|
54
|
+
| API (operaciones) | — | `n/a` (gema sin superficie HTTP/CLI/eventos; superficie pública = Interfaz) |
|
|
51
55
|
| Eventos | — | `n/a` (la gema no emite eventos) |
|
|
56
|
+
| Seguridad | — | `n/a` (sin authn/authz propios; frontera auth-hacia-Docker en `docs/consumed/docker-engine-api.md` §a) |
|
|
57
|
+
| Multi-tenancy | — | `n/a` (gema stateless sin DB ni scope de tenant) |
|
|
58
|
+
| Data-lifecycle | — | `n/a` (sin persistencia/PII/retención; fixtures sintéticas) |
|
|
52
59
|
|
|
53
|
-
`n/a` = no aplica al tipo de repo.
|
|
60
|
+
`n/a` = no aplica al tipo de repo.
|
|
54
61
|
|
|
55
62
|
## Desarrollo
|
|
56
63
|
|
data/docs/behavior/behavior.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Comportamiento — docker-swarm
|
|
2
2
|
|
|
3
|
-
> meta: artefacto · RFC-007 · generado dev-enrich · anclado a `
|
|
3
|
+
> meta: artefacto · RFC-007 · generado dev-enrich · anclado a `v0.9.0` · cobertura: 11 flujos load-bearing (8 backfill inicial + 3 nuevos: auth de registry privado, `Image.pull` síncrono, `Container.create`)
|
|
4
4
|
|
|
5
5
|
## 1. Resumen
|
|
6
6
|
|
|
@@ -8,7 +8,7 @@ Flujos de ejecución load-bearing de `docker-swarm`: cómo se materializan en ru
|
|
|
8
8
|
|
|
9
9
|
## 2. Cobertura declarada
|
|
10
10
|
|
|
11
|
-
### Documentados (
|
|
11
|
+
### Documentados (11)
|
|
12
12
|
|
|
13
13
|
1. `Service.create` + reload
|
|
14
14
|
2. `Service.update` con `Version.Index`
|
|
@@ -18,13 +18,14 @@ Flujos de ejecución load-bearing de `docker-swarm`: cómo se materializan en ru
|
|
|
18
18
|
6. Error mapping (HTTP status → excepción tipada)
|
|
19
19
|
7. `Container.start` / `Container.stop`
|
|
20
20
|
8. `Loggable#logs` streaming
|
|
21
|
+
9. Auth de registry privado (`RegistryAuth.resolve` → `X-Registry-Auth` / `registryAuthFrom`)
|
|
22
|
+
10. `Image.pull` síncrono (stream NDJSON → error tipado → resultado explícito)
|
|
23
|
+
11. `Container.create` con nombre por query string (`create_query_params`)
|
|
21
24
|
|
|
22
25
|
### No documentados (ausencia ≠ inexistencia, RFC-007)
|
|
23
26
|
|
|
24
27
|
- Reconexión / reapertura de socket Unix (Excon nativo, fuera de nuestra superficie).
|
|
25
28
|
- Flujo de configuración / boot (`DockerSwarm.configure`) — trivial, sin secuencia de interés.
|
|
26
|
-
- Pull de imágenes con autenticación de registry (`X-Registry-Auth`) — **no implementado** en la gema (gap conocido).
|
|
27
|
-
- `Container.create` — **no implementado** en F1 (intencional, ver glossary).
|
|
28
29
|
|
|
29
30
|
## 3. Flujos
|
|
30
31
|
|
|
@@ -211,33 +212,127 @@ Mismo patrón para `stop`. POST sin body → no se reintenta automáticamente (
|
|
|
211
212
|
|
|
212
213
|
### 3.8 `Loggable#logs` streaming
|
|
213
214
|
|
|
214
|
-
Obtención de logs
|
|
215
|
+
Obtención de logs para Service/Task/Container, ya demultiplexados.
|
|
215
216
|
|
|
216
217
|
```mermaid
|
|
217
218
|
sequenceDiagram
|
|
218
219
|
actor Caller
|
|
219
220
|
participant Model as Service/Task/Container
|
|
220
221
|
participant Api
|
|
222
|
+
participant Demux as LogStreamDemuxer
|
|
221
223
|
participant Docker
|
|
222
224
|
|
|
223
225
|
Caller->>Model: model.logs(stdout: 1, stderr: 1, follow: 0)
|
|
224
226
|
Model->>Api: request(:logs, id:, query: { stdout:, stderr:, follow: })
|
|
225
227
|
Api->>Docker: GET /services/abc/logs?stdout=1&stderr=1
|
|
226
|
-
Docker-->>
|
|
227
|
-
|
|
228
|
+
Docker-->>Demux: 200 stream + Content-Type
|
|
229
|
+
alt cadena de frames cierra de punta a punta
|
|
230
|
+
Demux->>Demux: saca 8 bytes de cabecera por frame, concatena en orden
|
|
231
|
+
else body sin framing (TTY) o inconsistente
|
|
232
|
+
Demux->>Demux: deja el body intacto
|
|
233
|
+
end
|
|
234
|
+
Demux-->>Api: texto limpio
|
|
235
|
+
Api-->>Model: body
|
|
228
236
|
Model-->>Caller: String
|
|
229
237
|
```
|
|
230
238
|
|
|
231
239
|
**Notas load-bearing:**
|
|
232
|
-
- El body se
|
|
233
|
-
- `
|
|
240
|
+
- El body no se parsea como JSON (`ResponseJSONParser` lo respeta porque el Content-Type no es `application/json`).
|
|
241
|
+
- **El demux vive en un middleware, no en `Loggable`:** `Connection#request` devuelve `response.body` y descarta los headers, así que aguas abajo ya no hay `Content-Type` con el que decidir (ADR-025 cláusula 3).
|
|
242
|
+
- **`raw-stream` no implica TTY.** Ese `Content-Type` era el único que existía antes de la API v1.42, y la gema no fija `?version=` → un Engine 20.10 devuelve `raw-stream` con framing. El middleware decide por la forma del frame, no por el header (detalle y cita del changelog en [`docs/consumed/docker-engine-api.md`](../consumed/docker-engine-api.md) §b).
|
|
243
|
+
- **Desviación de ADR-025, acotada.** La **Decisión** normativa (`ADR-025:130-131` — *"un middleware que decide por `Content-Type`"*) **se cumple**: el middleware corta si el header falta o no es uno de los dos. Lo que la implementación contradice es el **rationale de §Alternativas** (`ADR-025:106-107`), que da por sentado que `raw-stream` implica TTY. Ese dato de apoyo es falso para Engines que topan en la API v1.41. Asentar la corrección en el reino queda **pendiente**.
|
|
244
|
+
- **El demux limpia los frames, no separa señal de ruido.** `stdout` y `stderr` siguen intercalados en un solo String: quien necesite un dato puntual tiene que delimitarlo en origen.
|
|
245
|
+
- **Un frame partido entre chunks no se reensambla:** el demux es todo-o-nada, así que devuelve el body **intacto** en vez de texto a medias. El comportamiento está definido y cubierto por spec — los cuatro casos que pide `ADR-025:196-198` (cadena de frames, varios en un chunk, tamaño/cola truncados, TTY sin framing) están en `spec/docker/swarm/middleware/log_stream_demuxer_spec.rb`.
|
|
246
|
+
- `follow: 1` mantiene la conexión abierta — el caller debe manejar el stream/timeout. `[inferred]` Con `follow` el body no llega completo, así que el demux no aplica por diseño; no está ejercitado por spec ni contemplado en ADR-025 — es extrapolación de esta capa, no una limitación declarada por la ADR.
|
|
247
|
+
|
|
248
|
+
### 3.9 Auth de registry privado (`X-Registry-Auth` / `registryAuthFrom`)
|
|
249
|
+
|
|
250
|
+
El caller pasa una credencial **opaca base64url** (`registry_auth`) y/o la fuente a reusar (`registry_auth_from`). `RegistryAuth.resolve` valida **antes** de la request (exclusión mutua + enum del `from`) y traduce a canales de transporte, sin tocar el payload ni el estado del modelo. La credencial se sanitiza en logs vía `LogHelper`. Aplica a `Service.create`/`#update` e `Image.pull`.
|
|
251
|
+
|
|
252
|
+
```mermaid
|
|
253
|
+
flowchart TD
|
|
254
|
+
Start[Caller pasa registry_auth y/o registry_auth_from] --> Resolve[RegistryAuth.resolve valida y traduce]
|
|
255
|
+
Resolve --> Both{ambos presentes?}
|
|
256
|
+
Both -->|si| Err1[ArgumentError mutuamente excluyentes]
|
|
257
|
+
Both -->|no| Enum{registry_auth_from en spec o previous-spec?}
|
|
258
|
+
Enum -->|invalido| Err2[ArgumentError valor invalido]
|
|
259
|
+
Enum -->|valido o ausente| Split[arma headers y query_params]
|
|
260
|
+
Split --> Header[registry_auth va al header X-Registry-Auth]
|
|
261
|
+
Split --> Query[registry_auth_from va a la query registryAuthFrom]
|
|
262
|
+
Header --> Req[Api.request en create update o pull]
|
|
263
|
+
Query --> Req
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
**Notas load-bearing:**
|
|
267
|
+
- La validación es **fail-fast local**: un caller que pasa ambos, o un `from` fuera de `spec`/`previous-spec`, corta con `ArgumentError` antes de tocar el Engine (no se deriva la ambigüedad a Docker).
|
|
268
|
+
- La credencial viaja **solo** por header/query — nunca en el payload ni en el estado del modelo. `LogHelper::SENSITIVE_KEYS` la enmascara en el wire-debug.
|
|
269
|
+
|
|
270
|
+
### 3.10 `Image.pull` síncrono (stream NDJSON → error tipado → resultado)
|
|
271
|
+
|
|
272
|
+
`Image.pull` es síncrono: consume el stream de progreso NDJSON hasta EOF, eleva error tipado ante un frame `error`/`errorDetail` (que Docker manda **con HTTP 200**), y solo tras terminación limpia devuelve un resultado explícito construido desde el stream — sin un `find` posterior.
|
|
273
|
+
|
|
274
|
+
```mermaid
|
|
275
|
+
sequenceDiagram
|
|
276
|
+
actor Caller
|
|
277
|
+
participant Image as DockerSwarm::Image
|
|
278
|
+
participant RegAuth as RegistryAuth
|
|
279
|
+
participant Api as DockerSwarm::Api
|
|
280
|
+
participant Docker as Docker Engine
|
|
281
|
+
|
|
282
|
+
Caller->>Image: pull(image_reference, registry_auth)
|
|
283
|
+
Image->>RegAuth: resolve(registry_auth)
|
|
284
|
+
RegAuth-->>Image: headers con X-Registry-Auth
|
|
285
|
+
Image->>Api: request pull con fromImage y headers
|
|
286
|
+
Api->>Docker: POST /images/create?fromImage=ref
|
|
287
|
+
Docker-->>Image: stream NDJSON de progreso
|
|
288
|
+
Image->>Image: parse_progress_stream + raise_on_stream_error!
|
|
289
|
+
alt frame error o errorDetail
|
|
290
|
+
Image-->>Caller: raise Error tipado
|
|
291
|
+
else terminacion limpia
|
|
292
|
+
Image->>Image: extract_digest desde frame Digest
|
|
293
|
+
Image-->>Caller: status pulled + image_ref + digest
|
|
294
|
+
end
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
**Notas load-bearing:**
|
|
298
|
+
- El middleware entrega el stream como `String` (multi-frame NDJSON) o `Hash` (frame único); `parse_progress_stream` normaliza ambos a lista de frames.
|
|
299
|
+
- El digest sale del frame `Digest: sha256:...` (el stream de pull **no** trae campo `aux` — verificado contra Docker 29.5.3), escaneando desde el final.
|
|
300
|
+
- Es un `POST` → **no** entra en la política de retries (ver flujo 3.5).
|
|
301
|
+
|
|
302
|
+
### 3.11 `Container.create` con nombre por query string
|
|
303
|
+
|
|
304
|
+
El Engine toma el nombre del container por query string. En el body lo **descarta en silencio** y responde `201` igual: el container nace con nombre aleatorio, y una lógica de adopción por nombre determinista no lo encuentra → el reintento duplica en vez de adoptar (ADR-025 cláusula 1).
|
|
305
|
+
|
|
306
|
+
```mermaid
|
|
307
|
+
sequenceDiagram
|
|
308
|
+
actor Caller
|
|
309
|
+
participant Container as DockerSwarm::Container
|
|
310
|
+
participant Api
|
|
311
|
+
participant Docker
|
|
312
|
+
|
|
313
|
+
Caller->>Container: Container.create(name: "acs-seed-helper", Image:, Cmd:)
|
|
314
|
+
Container->>Container: valid?
|
|
315
|
+
Container->>Container: query_params_for_docker → { name: "acs-seed-helper" }
|
|
316
|
+
Container->>Container: payload_for_docker.except("name")
|
|
317
|
+
Container->>Api: request(:create, query_params:, payload:)
|
|
318
|
+
Api->>Docker: POST /containers/create?name=acs-seed-helper
|
|
319
|
+
Docker-->>Api: 201 { Id: "abc" }
|
|
320
|
+
Api-->>Container: { Id: "abc" }
|
|
321
|
+
Container->>Container: self.ID = "abc" → reload
|
|
322
|
+
Container-->>Caller: instancia hidratada
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
**Notas load-bearing:**
|
|
326
|
+
- El split lo declara el modelo con `.create_query_params` (`%w[name]` en `Container`); el default del concern es `[]`, así que los demás modelos Creatable no cambian de comportamiento.
|
|
327
|
+
- **El atributo se excluye del payload**, no se duplica: mandarlo en los dos lados no da error pero deja el body con una clave que Docker ignora.
|
|
328
|
+
- Hereda el flujo §3.1: `valid?` antes del POST, `reload` después, y **sin retry** por ser `POST` (§3.5). Si el `create` falla por `Communication`, el caller decide — con nombre determinista puede adoptar el existente en el reintento.
|
|
234
329
|
|
|
235
330
|
## 4. Cobertura y fronteras
|
|
236
331
|
|
|
237
|
-
- **Cobertura
|
|
332
|
+
- **Cobertura (RFC-007 backfill on-demand + incremental):** 8 flujos load-bearing en el backfill inicial + 2 agregados con el soporte de auth de registry privado (auth de registry privado, `Image.pull` síncrono) + 1 con el `create` de containers = 11. Esta gema es chica; el backfill completo era factible y se hizo, y a partir de ahí se acreta por PR.
|
|
238
333
|
- **Frontera con glosario:** términos (Service, Spec, Version.Index, etc.) viven en [`docs/glossary/glossary.md`](../glossary/glossary.md). Esta capa documenta secuencias, no significado.
|
|
239
334
|
- **Frontera con configuración:** `DockerSwarm.configure` es boot, no flujo de negocio. No se diagrama.
|
|
240
335
|
- **No localizable / fuera de alcance:**
|
|
241
336
|
- Lógica interna de Excon (retry timing, socket pool) — vive en Excon, no se inventa diagrama.
|
|
242
|
-
-
|
|
337
|
+
- Nada pendiente por este motivo. (Hasta el 2026-08-03 esta línea decía que la gema no implementaba `X-Registry-Auth` y citaba `Image.create`; las dos cosas quedaron obsoletas — la auth de registry está documentada en §3.9 e `Image.create` fue retirado.)
|
|
243
338
|
- **Cadencia incremental a partir de acá:** sólo se diagrama un flujo cuando un PR lo toca o agrega. No barrido retroactivo de legacy.
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# Dependencias consumidas — docker-swarm
|
|
2
|
+
|
|
3
|
+
> meta: artefacto · RFC-018 · generado arch-structure + enriquecido arch-enrich · anclado a `v0.9.0` · cobertura: superficie del Docker Engine API consumida por la gema (`api.rb` ENDPOINTS + `connection.rb`); §c/§e enriquecidas 1/1
|
|
4
|
+
|
|
5
|
+
## 1. Resumen
|
|
6
|
+
|
|
7
|
+
La gema consume **una** dependencia externa: el **Docker Engine API** (HTTP REST sobre Unix socket o TCP). Es el único servicio que invoca; toda la gema es un cliente tipado de esa API. Cliente propio = `DockerSwarm::Connection` (Excon) + `DockerSwarm::Api` (mapa de endpoints).
|
|
8
|
+
|
|
9
|
+
## 2. Cuerpo
|
|
10
|
+
|
|
11
|
+
### Docker Engine API
|
|
12
|
+
|
|
13
|
+
#### a. Identidad
|
|
14
|
+
|
|
15
|
+
| campo | valor |
|
|
16
|
+
|---|---|
|
|
17
|
+
| proveedor / servicio | Docker Engine API (daemon `dockerd`) |
|
|
18
|
+
| sub-tipo | **externo** (no es repo del fleet) |
|
|
19
|
+
| transporte | HTTP/REST sobre Unix socket (`unix:///var/run/docker.sock`, default) o TCP (`http://host:2375`) |
|
|
20
|
+
| cliente nuestro | `DockerSwarm::Connection` (Excon) + `DockerSwarm::Api` (`api.rb`) |
|
|
21
|
+
| auth | ninguna por default (socket local). TCP/TLS → fuera de alcance del cliente (no inyecta credenciales; 401 si el daemon las exige) |
|
|
22
|
+
| versión de API | v1.41 (referencia en los `@see` de los modelos; no se negocia explícitamente). **La gema no fija `?version=`** → el daemon sirve su versión **máxima**, así que la versión efectiva la decide cada host. Piso relevante: un Engine 20.10 topa en **v1.41**. Techo del parque: `unknown` (no derivable de este repo). Consecuencia en los logs: ver §b |
|
|
23
|
+
| ancla | doc oficial: <https://docs.docker.com/engine/api/v1.41/> |
|
|
24
|
+
|
|
25
|
+
#### b. Operaciones consumidas
|
|
26
|
+
|
|
27
|
+
Subset que la gema invoca, derivado de `Api::ENDPOINTS` (`api.rb:5-72`). `destino` = `método HTTP + path` (los `%<id>s` se interpolan en runtime).
|
|
28
|
+
|
|
29
|
+
| recurso | operación | destino | qué mandamos / esperamos |
|
|
30
|
+
|---|---|---|---|
|
|
31
|
+
| swarm | show | `GET swarm` | — / info del cluster (Hash) |
|
|
32
|
+
| system | info | `GET info` | — / Hash |
|
|
33
|
+
| system | version | `GET version` | — / Hash |
|
|
34
|
+
| system | up | `GET _ping` | — / `"OK"` |
|
|
35
|
+
| system | df | `GET system/df` | — / Hash de uso de disco |
|
|
36
|
+
| nodes | index | `GET nodes` | `?filters=` opcional / array |
|
|
37
|
+
| nodes | show | `GET nodes/%<id>s` | — / Hash |
|
|
38
|
+
| nodes | update | `POST nodes/%<id>s/update` | `?version=` + payload / — |
|
|
39
|
+
| nodes | destroy | `DELETE nodes/%<id>s` | — / — |
|
|
40
|
+
| tasks | index / show | `GET tasks`, `GET tasks/%<id>s` | `?filters=` / array \| Hash |
|
|
41
|
+
| tasks | logs | `GET tasks/%<id>s/logs` | `?stdout/stderr/...` / stream multiplexado (demux en el cliente) |
|
|
42
|
+
| services | index / show | `GET services`, `GET services/%<id>s` | `?filters=` / array \| Hash |
|
|
43
|
+
| services | create | `POST services/create` | payload (Spec aplanado) + header `X-Registry-Auth` (opcional, registry privado) / `{ID}` |
|
|
44
|
+
| services | update | `POST services/%<id>s/update` | `?version=` (+ `?registryAuthFrom=` opcional: `spec`\|`previous-spec`) + payload + header `X-Registry-Auth` (opcional; excluyente con `registryAuthFrom`) / — |
|
|
45
|
+
| services | destroy | `DELETE services/%<id>s` | — / — |
|
|
46
|
+
| services | logs | `GET services/%<id>s/logs` | `?stdout/stderr/...` / stream multiplexado (demux en el cliente) |
|
|
47
|
+
| configs | index / show / create / destroy | `GET configs`, `GET configs/%<id>s`, `POST configs/create`, `DELETE configs/%<id>s` | payload en create / `{ID}` |
|
|
48
|
+
| secrets | index / show / create / destroy | `GET secrets`, `GET secrets/%<id>s`, `POST secrets/create`, `DELETE secrets/%<id>s` | payload en create (`Data` filtrado en logs) / `{ID}` |
|
|
49
|
+
| networks | index / show / create / update / destroy | `GET/POST networks...`, `POST networks/%<id>s/update`, `DELETE networks/%<id>s` | payload / `{ID}` |
|
|
50
|
+
| volumes | index / show / create / destroy | `GET volumes`, `GET volumes/%<id>s`, `POST volumes/create`, `DELETE volumes/%<id>s` | payload / respuesta wrapped en `Volumes` |
|
|
51
|
+
| containers | index | `GET containers/json` | `?filters=` / array |
|
|
52
|
+
| containers | show | `GET containers/%<id>s/json` | — / Hash |
|
|
53
|
+
| containers | create | `POST containers/create` | `?name=` (query, NO en el body) + payload / `{Id}` |
|
|
54
|
+
| containers | start / stop | `POST containers/%<id>s/start`, `POST containers/%<id>s/stop` | — / — |
|
|
55
|
+
| containers | destroy | `DELETE containers/%<id>s` | — / — |
|
|
56
|
+
| containers | logs | `GET containers/%<id>s/logs` | `?stdout/stderr/...` / stream multiplexado (demux en el cliente) |
|
|
57
|
+
| images | index | `GET images/json` | — / array |
|
|
58
|
+
| images | show | `GET images/%<id>s/json` | — / Hash |
|
|
59
|
+
| images | pull | `POST images/create?fromImage=<ref>` | header `X-Registry-Auth` (opcional, registry privado) / stream NDJSON de progreso |
|
|
60
|
+
| images | destroy | `DELETE images/%<id>s` | — / — |
|
|
61
|
+
|
|
62
|
+
Serialización: request body no-String → JSON (`Content-Type: application/json`) vía `Middleware::RequestEncoder`; response `application/json` → `HashWithIndifferentAccess` recursivo vía `Middleware::ResponseJSONParser`.
|
|
63
|
+
|
|
64
|
+
**Streams de logs (`containers`/`services`/`tasks` → `logs`).** Sin TTY el Engine multiplexa: 8 bytes de cabecera por frame (1 tipo de stream · 3 de relleno en cero · 4 de tamaño big-endian). `Middleware::LogStreamDemuxer` los saca, así que `Loggable#logs` entrega texto limpio.
|
|
65
|
+
|
|
66
|
+
El `Content-Type` **no alcanza** para decidir. `application/vnd.docker.multiplexed-stream` existe **desde la API v1.42**; su entrada de changelog dice, textual:
|
|
67
|
+
|
|
68
|
+
> `GET /containers/{id}/attach`, `GET /exec/{id}/start`, `GET /containers/{id}/logs` `GET /services/{id}/logs` and `GET /tasks/{id}/logs` now set Content-Type header to `application/vnd.docker.multiplexed-stream` when a multiplexed stdout/stderr stream is sent to client, `application/vnd.docker.raw-stream` otherwise.
|
|
69
|
+
|
|
70
|
+
Antes de v1.42 ese valor no existía y un stream multiplexado viajaba igual como `application/vnd.docker.raw-stream`. Como la gema no fija `?version=`, un Engine 20.10 (tope v1.41) devuelve `raw-stream` **con** framing. Por eso el middleware, ante `raw-stream`, decide por la **forma del frame** —recorre el body entero y solo demultiplexa si la cadena de frames cierra de punta a punta— en vez de confiar en el header. Ante cualquier inconsistencia deja el body intacto.
|
|
71
|
+
|
|
72
|
+
> Anclaje: <https://docs.docker.com/reference/api/engine/version-history/> (entrada de v1.42).
|
|
73
|
+
|
|
74
|
+
#### d. Errores del proveedor → excepción nuestra
|
|
75
|
+
|
|
76
|
+
El daemon responde con status HTTP; `Middleware::ErrorHandler` los mapea a la jerarquía `DockerSwarm::Error`. La tabla completa status→excepción está en [`docs/errors/errors.md`](../errors/errors.md) §b (este artefacto **referencia**, no la redefine).
|
|
77
|
+
|
|
78
|
+
| condición del proveedor | excepción nuestra |
|
|
79
|
+
|---|---|
|
|
80
|
+
| status 4xx/5xx mapeado | la `DockerSwarm::Error::*` correspondiente (ver `docs/errors` §b) |
|
|
81
|
+
| status no-2xx no mapeado | `DockerSwarm::Error` (`HTTP <status>`) |
|
|
82
|
+
| socket caído / timeout de conexión (`Excon::Error::Socket`) | `DockerSwarm::Error::Communication` (preserva `cause`) |
|
|
83
|
+
|
|
84
|
+
#### c. Retry / idempotencia (semántica)
|
|
85
|
+
|
|
86
|
+
**Estructural** (anclado a `connection.rb:24-33`):
|
|
87
|
+
|
|
88
|
+
| aspecto | valor |
|
|
89
|
+
|---|---|
|
|
90
|
+
| métodos con retry | `get/head/put/delete/options` (`Connection::IDEMPOTENT_METHODS`) |
|
|
91
|
+
| reintentos | `max_retries` (default 3), solo en métodos idempotentes; POST/PATCH = 0 |
|
|
92
|
+
| errores reintentados | `Excon::Error::Socket`, `Excon::Error::Timeout` |
|
|
93
|
+
| backoff | ninguno — reintento inmediato (no se setea `retry_interval`) |
|
|
94
|
+
|
|
95
|
+
**Semántica:** la frontera idempotente/no-idempotente refleja la del Docker Engine API:
|
|
96
|
+
|
|
97
|
+
- `GET`/`DELETE`/`PUT` son seguros de reintentar: re-listar, re-borrar (404 → `nil` graceful) o re-actualizar produce el mismo estado final → la gema los reintenta automáticamente ante caída de socket.
|
|
98
|
+
- `POST create` (services/networks/volumes/configs/secrets/containers) **no** se reintenta: un replay tras fallo parcial podría crear un recurso duplicado (el daemon no deduplica por nombre en todos los recursos). El caller decide qué hacer si un `create` falla por `Communication`.
|
|
99
|
+
- `POST update`/`start`/`stop`/`restart` tampoco se reintentan (son POST); `update` además acarrea `?version=` → un replay con versión vieja daría 409 `Conflict`, no un duplicado.
|
|
100
|
+
- **Sin backoff** es aceptable acá: el socket Unix local rara vez está transitoriamente saturado; ante un daemon caído, 3 reintentos inmediatos fallan rápido y se propaga `Communication`.
|
|
101
|
+
|
|
102
|
+
#### e. Degradación (si la dependencia cae)
|
|
103
|
+
|
|
104
|
+
| escenario | comportamiento de la gema |
|
|
105
|
+
|---|---|
|
|
106
|
+
| socket caído / daemon no responde | tras `max_retries` (idempotentes) o inmediato (POST), levanta `DockerSwarm::Error::Communication` con el `Excon::Error::Socket` en `cause` |
|
|
107
|
+
| daemon devuelve 5xx | levanta la `Error::*` correspondiente (502/503/504) sin reintento HTTP |
|
|
108
|
+
| fallback / cola / circuit-breaker | **ninguno** — la gema es un cliente fino, fail-fast; no encola ni degrada |
|
|
109
|
+
| responsabilidad del consumidor | decidir reintento con backoff, fallback o propagación; la gema solo provee el error tipado |
|
|
110
|
+
|
|
111
|
+
- **SLA del proveedor:** n/a — el daemon Docker suele ser local (socket Unix) o de infraestructura propia; no hay SLA externo que documentar.
|
|
112
|
+
- **Sin estado degradado:** la gema no cachea ni mantiene estado entre llamadas; si el daemon cae, cada operación falla independientemente. No hay "modo degradado" que activar/desactivar.
|
|
113
|
+
|
|
114
|
+
## 3. Inferencias
|
|
115
|
+
|
|
116
|
+
| afirmación | confidence | a verificar |
|
|
117
|
+
|---|---|---|
|
|
118
|
+
| Versión de API = v1.41 | inferred | tomada de los `@see` de los modelos; la gema no fija `?version=` en la URL base ni negocia versión |
|
|
119
|
+
| `qué mandamos/esperamos` por operación | inferred | derivado del flujo de los concerns (`payload_for_docker`, `reload` tras create); el shape exacto del Spec lo fija Docker |
|
|
120
|
+
|
|
121
|
+
## 4. Cobertura y fronteras
|
|
122
|
+
|
|
123
|
+
- **Regla de dependencia directa:** solo se documenta lo que la gema invoca directo contra el daemon. Lo que Docker orqueste por debajo (scheduling de tasks en nodos, overlay networks) es concern del daemon, no de la gema.
|
|
124
|
+
- **Subset, no la API completa:** `Api::ENDPOINTS` cubre el subset que la gema expone; el Docker Engine API tiene endpoints (build, exec, plugins, `POST /auth`) que la gema **no** consume → fuera de alcance. Nota: la gema **sí** manda el header `X-Registry-Auth` (credencial opaca por-request); eso es distinto del endpoint `POST /auth` (login contra un registry), que sigue sin consumirse.
|
|
125
|
+
- **Auth de registry privado:** soportado como credencial opaca por-request — `Service.create`/`#update` (header `X-Registry-Auth`; `#update` además `registryAuthFrom` para reusar la del spec) e `Image.pull` (header `X-Registry-Auth`). La gema no mintea ni valida la credencial: la recibe base64url del caller y la pasa tal cual (`RegistryAuth` traduce a header/query). TLS/TCP para alcanzar el daemon sigue fuera de alcance (ver abajo).
|
|
126
|
+
- **TLS/TCP:** el cliente no gestiona certificados; un endpoint TCP con TLS mutuo queda fuera de alcance (resultará en `Unauthorized`/`Communication`).
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# Errores — docker-swarm
|
|
2
|
+
|
|
3
|
+
> meta: artefacto · RFC-020 · generado arch-structure + enriquecido arch-enrich · anclado a `8f2e1f7` · cobertura: excepciones públicas que la gema emite (`lib/docker_swarm/errors.rb` + `middleware/error_handler.rb` + `Image#raise_on_stream_error!`); política §c 16/16 enriquecida
|
|
4
|
+
|
|
5
|
+
## 1. Resumen
|
|
6
|
+
|
|
7
|
+
La gema traduce los status HTTP no-2xx del Docker Engine API a una jerarquía de excepciones Ruby tipadas, todas bajo `DockerSwarm::Error < StandardError`. El mapeo status→excepción vive en `Middleware::ErrorHandler`; los fallos de socket se envuelven en `Communication`. No hay payload propio (no es un servicio HTTP): el "shape" es la excepción Ruby + su `message`.
|
|
8
|
+
|
|
9
|
+
## 2. Cuerpo
|
|
10
|
+
|
|
11
|
+
### a. Inventario de excepciones públicas
|
|
12
|
+
|
|
13
|
+
Todas heredan de `DockerSwarm::Error` (que hereda de `StandardError`). Tres formas de acceso equivalentes: `DockerSwarm::NotFound`, `DockerSwarm::Error::NotFound`, `DockerSwarm::Errors::NotFound` (alias + `Errors.const_missing`).
|
|
14
|
+
|
|
15
|
+
| excepción | jerarquía base | qué la levanta |
|
|
16
|
+
|---|---|---|
|
|
17
|
+
| `Error` | `StandardError` | base de todas; el fallback `HTTP <status>` para status no mapeado; **y** el fallo de `Image.pull` cuando el stream de progreso reporta un frame `error`/`errorDetail` (Docker lo emite con **HTTP 200** → no lo agarra `ErrorHandler`; lo eleva `Image#raise_on_stream_error!`, `models/image.rb`) |
|
|
18
|
+
| `Error::BadRequest` | `Error` | status 400 (payload malformado) |
|
|
19
|
+
| `Error::Unauthorized` | `Error` | status 401 (TLS sin credenciales) |
|
|
20
|
+
| `Error::Forbidden` | `Error` | status 403 (permisos insuficientes, ej. swarm op en worker) |
|
|
21
|
+
| `Error::NotFound` | `Error` | status 404; capturada por `find`/`destroy` → `nil` |
|
|
22
|
+
| `Error::NotAcceptable` | `Error` | status 406 |
|
|
23
|
+
| `Error::RequestTimeout` | `Error` | status 408 |
|
|
24
|
+
| `Error::Conflict` | `Error` | status 409 (nombre duplicado o `Version.Index` stale) |
|
|
25
|
+
| `Error::UnprocessableEntity` | `Error` | status 422 (raised con el `body` crudo, no el `message`) |
|
|
26
|
+
| `Error::TooManyRequests` | `Error` | status 429 |
|
|
27
|
+
| `Error::InternalServerError` | `Error` | status 500 |
|
|
28
|
+
| `Error::BadGateway` | `Error` | status 502 |
|
|
29
|
+
| `Error::ServiceUnavailable` | `Error` | status 503 |
|
|
30
|
+
| `Error::GatewayTimeout` | `Error` | status 504 |
|
|
31
|
+
| `Error::Communication` | `Error` | socket caído/inalcanzable (`Excon::Error::Socket`); `Connection` la envuelve preservando el mensaje original |
|
|
32
|
+
|
|
33
|
+
### b. Códigos HTTP → excepción (por `Middleware::ErrorHandler`)
|
|
34
|
+
|
|
35
|
+
La gema **consume** HTTP, no lo expone. Esta tabla es el mapeo de respuesta-del-daemon → excepción-nuestra (`middleware/error_handler.rb:17-33`). El detalle de qué operación produce cada status está en el productor (Docker Engine API, ver [`docs/consumed/`](../consumed/docker-engine-api.md)).
|
|
36
|
+
|
|
37
|
+
| status | excepción | mensaje |
|
|
38
|
+
|---|---|---|
|
|
39
|
+
| 200–299 | — | pasa (no levanta) |
|
|
40
|
+
| 400 | `BadRequest` | `error_message(body)` |
|
|
41
|
+
| 401 | `Unauthorized` | `error_message(body)` |
|
|
42
|
+
| 403 | `Forbidden` | `error_message(body)` |
|
|
43
|
+
| 404 | `NotFound` | `error_message(body)` |
|
|
44
|
+
| 406 | `NotAcceptable` | `error_message(body)` |
|
|
45
|
+
| 408 | `RequestTimeout` | `error_message(body)` |
|
|
46
|
+
| 409 | `Conflict` | `error_message(body)` |
|
|
47
|
+
| 422 | `UnprocessableEntity` | `body` (crudo) |
|
|
48
|
+
| 429 | `TooManyRequests` | `error_message(body)` |
|
|
49
|
+
| 500 | `InternalServerError` | `error_message(body)` |
|
|
50
|
+
| 502 | `BadGateway` | `error_message(body)` |
|
|
51
|
+
| 503 | `ServiceUnavailable` | `error_message(body)` |
|
|
52
|
+
| 504 | `GatewayTimeout` | `error_message(body)` |
|
|
53
|
+
| otro no-2xx | `Error` | `"HTTP #{status}: #{error_msg}"` |
|
|
54
|
+
|
|
55
|
+
`error_message(body)`: si `body` es Hash → `body["message"] \|\| body["error"] \|\| body.to_json`; si no → `body.to_s` (`error_handler.rb:59-65`).
|
|
56
|
+
|
|
57
|
+
### d. Shape del payload de error
|
|
58
|
+
|
|
59
|
+
No aplica un shape propio tipo RFC 7807: la gema es un cliente, no un servidor. El "payload" que recibe el consumidor es la **excepción Ruby** con:
|
|
60
|
+
|
|
61
|
+
- `exception.class` — el tipo tipado (tabla §a).
|
|
62
|
+
- `exception.message` — el `message`/`error` del body de Docker (o el body crudo en 422), o `"Docker socket error: ..."` en `Communication`.
|
|
63
|
+
- `exception.cause` — en `Communication`, el `Excon::Error::Socket` original queda accesible (`connection.rb:47,61`).
|
|
64
|
+
|
|
65
|
+
### c. Política por error (retriable · backoff · idempotencia · acción)
|
|
66
|
+
|
|
67
|
+
> **Mecanismo (anclado a `connection.rb:24-33`):** el auto-retry de la gema opera **solo a nivel transporte** — reintenta `Excon::Error::Socket` y `Excon::Error::Timeout` en métodos idempotentes (`get/head/put/delete/options`), `max_retries=3`, **sin backoff** (reintento inmediato, no hay `retry_interval`). Los errores **HTTP 4xx/5xx NO se auto-reintentan**: una vez que llega una respuesta con status, `ErrorHandler` levanta y no hay retry. La columna "retriable" abajo es por tanto **recomendación al consumidor**, no comportamiento automático (salvo `Communication`).
|
|
68
|
+
|
|
69
|
+
> **Acción:** la gema **siempre loguea** el fallo (`request_failure`, nivel ERROR, `connection.rb:49`) y **propaga** (raise). No hay integración de observabilidad (Sentry/exis_ray) en la gema → `escalate`/`report` quedan al consumidor. Por eso la acción base de todas es **log + propagate**; la columna marca el matiz por error.
|
|
70
|
+
|
|
71
|
+
| excepción | retriable? (consumidor) | backoff | idempotencia requerida | acción / nota |
|
|
72
|
+
|---|---|---|---|---|
|
|
73
|
+
| `BadRequest` (400) | no | — | — | propagate — corregir payload |
|
|
74
|
+
| `Unauthorized` (401) | no | — | — | propagate — corregir credenciales/TLS |
|
|
75
|
+
| `Forbidden` (403) | no | — | — | propagate — op no permitida en este rol |
|
|
76
|
+
| `NotFound` (404) | no | — | — | `find`/`destroy` la absorben → `nil`; resto propagate |
|
|
77
|
+
| `NotAcceptable` (406) | no | — | — | propagate |
|
|
78
|
+
| `RequestTimeout` (408) | condicional | sí | sí (si POST) | retry solo en op idempotente; subir `read_timeout` |
|
|
79
|
+
| `Conflict` (409) | condicional | — | sí | si `Version.Index` stale → `reload` + reintentar `update`; si nombre duplicado → no retriable, propagate |
|
|
80
|
+
| `UnprocessableEntity` (422) | no | — | — | propagate — payload semánticamente inválido |
|
|
81
|
+
| `TooManyRequests` (429) | sí | sí | no | backoff + retry (rate limit del daemon) |
|
|
82
|
+
| `InternalServerError` (500) | condicional | sí | sí | retry en op idempotente; revisar payload (update sin `version` → 500) |
|
|
83
|
+
| `BadGateway` (502) | sí | sí | sí (si POST) | transitorio (proxy); retry seguro en op idempotente |
|
|
84
|
+
| `ServiceUnavailable` (503) | sí | sí | sí (si POST) | daemon reiniciando; retry con backoff |
|
|
85
|
+
| `GatewayTimeout` (504) | sí | sí | sí (si POST) | transitorio; retry en op idempotente |
|
|
86
|
+
| `Communication` (socket) | sí (auto) | no | sí (si POST) | la gema YA reintteta Socket/Timeout en métodos idempotentes (`max_retries`, sin backoff); en POST no reintenta → el caller decide |
|
|
87
|
+
| `Error` (pull stream, HTTP 200) | sí (consumidor) | sí | sí | `Image.pull` la eleva ante un frame `error`/`errorDetail` del stream; propagate. El pull es idempotente → el caller puede reintentar (ej. fallo transitorio de red/registry). Distinguir de un 404 **pre-stream** (imagen/registry inexistente o acceso denegado → no retriable) |
|
|
88
|
+
|
|
89
|
+
## 3. Inferencias
|
|
90
|
+
|
|
91
|
+
| afirmación | confidence | a verificar |
|
|
92
|
+
|---|---|---|
|
|
93
|
+
| El "cuándo" de cada status (ej. 403 = swarm op en worker) refleja el comportamiento de Docker, no lógica de la gema | inferred | el mapeo es status→excepción genérico; la causa la fija el daemon. Notas tomadas de `skill/SKILL.md` |
|
|
94
|
+
| `UnprocessableEntity` recibe el `body` crudo (no `error_message`) a propósito (422 suele traer detalle estructurado) | declared | `error_handler.rb:25` — divergencia explícita del resto |
|
|
95
|
+
| §c columna "retriable" = recomendación al consumidor basada en semántica HTTP/Docker; la gema **no** auto-reintenta status HTTP (solo Socket/Timeout) | inferred | mecanismo anclado a `connection.rb`; la política por-status la confirma el humano contra el comportamiento real del daemon |
|
|
96
|
+
| 409 `Conflict` con `Version.Index` stale → patrón reload+retry | inferred | el `update` extrae `Version.Index` (`updatable.rb:9`); el reload-on-conflict es decisión del consumidor, no automática |
|
|
97
|
+
|
|
98
|
+
## 4. Cobertura y fronteras
|
|
99
|
+
|
|
100
|
+
- **Solo errores públicos:** estas excepciones cruzan la frontera de la gema hacia el consumidor. No hay excepciones internas rescatadas-y-tragadas que documentar (salvo `JSON::ParserError` en `ResponseJSONParser`, que se traga y retorna el body crudo — interno, no contrato).
|
|
101
|
+
- **Frontera con consumed (RFC-018):** este catálogo = lo que la gema **emite**. El mapeo "error del proveedor Docker → excepción nuestra" lo referencia [`docs/consumed/docker-engine-api.md`](../consumed/docker-engine-api.md) §d, que apunta acá.
|
|
102
|
+
- **Política §c:** enriquecida (recomendación al consumidor); el matiz por-status lo confirma el humano contra el comportamiento real del daemon (ver §3).
|
|
103
|
+
- **Validación de input del caller (`ArgumentError`, stdlib):** `RegistryAuth.resolve`/`validate!` (exclusión mutua `registry_auth`/`registry_auth_from` + enum del `from`) y `Base#assign_attributes` (no-Hash) elevan `ArgumentError` ante input inválido del caller — fail-fast, antes de tocar el daemon. Es contrato público de esas firmas (documentado en [`docs/interface/interface.md`](../interface/interface.md)), **no** parte de la jerarquía `DockerSwarm::Error` → por eso no está en §a/§c.
|