docker-swarm 0.7.2 → 0.8.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4173c79f1bd808f22729c09eda7b4b70a494c4f080625fff33285a410f1224dc
4
- data.tar.gz: a8d0aaa08dc6ed272e16ef3577d59f7571791d793a7a2192afb4b5be762c2578
3
+ metadata.gz: 61bc38a1b8f94704857812a859abbe425f57a63130655bcbaf7938f5621ad09a
4
+ data.tar.gz: 1b824044722bc3a3c840889b5fef843aa6eafd25576ea25811ac8ee8e688d4e3
5
5
  SHA512:
6
- metadata.gz: 88d788fdd06036b76b6c666f46acfc6880bbb74b8f80feb706a8cfe71aa6e7a62164717de75b5711dc34086218a3feaf5e90323461ab27f76685417d199a07cd
7
- data.tar.gz: 44bd8aa057c5b1d9e9e160deed73f13e08d0c441ba4f63a2f0d4142a89c2dc5c3939f732c80adca3c298e7e61b3192506dcf30064c93c9caaf6fa01ee4d4efc9
6
+ metadata.gz: bfbb5850f24f31b4e00e7da3a90e3ed73996304c757688736cc0dcbe52bba19738e0c4a9615a79f88912f984992d603546a4a46e40f4102f356b7e718b37e983
7
+ data.tar.gz: c3d7d6bf0e27dd190bd4dfe78a4b09436f3dada89edbf8628552a7a17d0a63a66ffb49c6c62b2e923c5f58d6cd12a584ba819e12329e13ae90ad50079e8bdc1a
data/CHANGELOG.md CHANGED
@@ -2,6 +2,18 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## [0.8.0] — 2026-07-22
6
+
7
+ ### Nuevas funcionalidades
8
+ - `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
9
+ - `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
10
+
11
+ ### Breaking changes
12
+ - `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
13
+
14
+ ### Seguridad
15
+ - `LogHelper` sanitiza recursivamente los headers de autenticación (`X-Registry-Auth`, `Authorization`) para no filtrar credenciales en logs de wire-debug — @Pslp
16
+
5
17
  ## [0.7.2] — 2026-06-29
6
18
 
7
19
  ### 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) | F1 completo |
46
- | Comportamiento | [`docs/behavior/behavior.md`](docs/behavior/behavior.md) | F1 backfill on-demand (8 flujos) |
47
- | Configuración | [`docs/config/configuracion.md`](docs/config/configuracion.md) | F6 inventario base (7 opciones, sin env vars) |
48
- | API (operaciones) | | F2 pendiente; contrato resumido inline en `skill/SKILL.md` |
49
- | Interfaz | | F2 pendiente; contrato resumido inline en `skill/SKILL.md` |
50
- | Topología | | F2 pendiente |
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 (8 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. F2 pendiente = capa declarada en la RFC pero todavía no implementada en `arch-structure`; el contenido relevante vive transitoriamente en `skill/SKILL.md` (RFC-008 §2 coexistencia transitoria).
60
+ `n/a` = no aplica al tipo de repo.
54
61
 
55
62
  ## Desarrollo
56
63
 
@@ -1,6 +1,6 @@
1
1
  # Comportamiento — docker-swarm
2
2
 
3
- > meta: artefacto · RFC-007 · generado dev-enrich · anclado a `a4e3129` · cobertura: backfill on-demand inicial (8 flujos load-bearing)
3
+ > meta: artefacto · RFC-007 · generado dev-enrich · anclado a `29856f1` · cobertura: 10 flujos load-bearing (8 backfill inicial + 2 nuevos: auth de registry privado, `Image.pull` síncrono)
4
4
 
5
5
  ## 1. Resumen
6
6
 
@@ -18,12 +18,13 @@ 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)
21
23
 
22
24
  ### No documentados (ausencia ≠ inexistencia, RFC-007)
23
25
 
24
26
  - Reconexión / reapertura de socket Unix (Excon nativo, fuera de nuestra superficie).
25
27
  - 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
28
  - `Container.create` — **no implementado** en F1 (intencional, ver glossary).
28
29
 
29
30
  ## 3. Flujos
@@ -232,9 +233,63 @@ sequenceDiagram
232
233
  - El body se devuelve sin parseo (no es JSON; `ResponseJSONParser` lo respeta porque Content-Type no es `application/json`).
233
234
  - `follow: 1` mantiene la conexión abierta — el caller debe manejar el stream/timeout.
234
235
 
236
+ ### 3.9 Auth de registry privado (`X-Registry-Auth` / `registryAuthFrom`)
237
+
238
+ 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`.
239
+
240
+ ```mermaid
241
+ flowchart TD
242
+ Start[Caller pasa registry_auth y/o registry_auth_from] --> Resolve[RegistryAuth.resolve valida y traduce]
243
+ Resolve --> Both{ambos presentes?}
244
+ Both -->|si| Err1[ArgumentError mutuamente excluyentes]
245
+ Both -->|no| Enum{registry_auth_from en spec o previous-spec?}
246
+ Enum -->|invalido| Err2[ArgumentError valor invalido]
247
+ Enum -->|valido o ausente| Split[arma headers y query_params]
248
+ Split --> Header[registry_auth va al header X-Registry-Auth]
249
+ Split --> Query[registry_auth_from va a la query registryAuthFrom]
250
+ Header --> Req[Api.request en create update o pull]
251
+ Query --> Req
252
+ ```
253
+
254
+ **Notas load-bearing:**
255
+ - 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).
256
+ - 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.
257
+
258
+ ### 3.10 `Image.pull` síncrono (stream NDJSON → error tipado → resultado)
259
+
260
+ `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.
261
+
262
+ ```mermaid
263
+ sequenceDiagram
264
+ actor Caller
265
+ participant Image as DockerSwarm::Image
266
+ participant RegAuth as RegistryAuth
267
+ participant Api as DockerSwarm::Api
268
+ participant Docker as Docker Engine
269
+
270
+ Caller->>Image: pull(image_reference, registry_auth)
271
+ Image->>RegAuth: resolve(registry_auth)
272
+ RegAuth-->>Image: headers con X-Registry-Auth
273
+ Image->>Api: request pull con fromImage y headers
274
+ Api->>Docker: POST /images/create?fromImage=ref
275
+ Docker-->>Image: stream NDJSON de progreso
276
+ Image->>Image: parse_progress_stream + raise_on_stream_error!
277
+ alt frame error o errorDetail
278
+ Image-->>Caller: raise Error tipado
279
+ else terminacion limpia
280
+ Image->>Image: extract_digest desde frame Digest
281
+ Image-->>Caller: status pulled + image_ref + digest
282
+ end
283
+ ```
284
+
285
+ **Notas load-bearing:**
286
+ - El middleware entrega el stream como `String` (multi-frame NDJSON) o `Hash` (frame único); `parse_progress_stream` normaliza ambos a lista de frames.
287
+ - 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.
288
+ - Es un `POST` → **no** entra en la política de retries (ver flujo 3.5).
289
+
235
290
  ## 4. Cobertura y fronteras
236
291
 
237
- - **Cobertura inicial (RFC-007 backfill on-demand):** 8 flujos load-bearing documentados. Esta gema es chica; el backfill completo es factible y se hace ahora.
292
+ - **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) = 10. Esta gema es chica; el backfill completo era factible y se hizo, y a partir de ahí se acreta por PR.
238
293
  - **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
294
  - **Frontera con configuración:** `DockerSwarm.configure` es boot, no flujo de negocio. No se diagrama.
240
295
  - **No localizable / fuera de alcance:**
@@ -0,0 +1,116 @@
1
+ # Dependencias consumidas — docker-swarm
2
+
3
+ > meta: artefacto · RFC-018 · generado arch-structure + enriquecido arch-enrich · anclado a `29856f1` · 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) |
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 raw |
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 raw |
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` | 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 raw |
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
+ #### d. Errores del proveedor → excepción nuestra
65
+
66
+ 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).
67
+
68
+ | condición del proveedor | excepción nuestra |
69
+ |---|---|
70
+ | status 4xx/5xx mapeado | la `DockerSwarm::Error::*` correspondiente (ver `docs/errors` §b) |
71
+ | status no-2xx no mapeado | `DockerSwarm::Error` (`HTTP <status>`) |
72
+ | socket caído / timeout de conexión (`Excon::Error::Socket`) | `DockerSwarm::Error::Communication` (preserva `cause`) |
73
+
74
+ #### c. Retry / idempotencia (semántica)
75
+
76
+ **Estructural** (anclado a `connection.rb:24-33`):
77
+
78
+ | aspecto | valor |
79
+ |---|---|
80
+ | métodos con retry | `get/head/put/delete/options` (`Connection::IDEMPOTENT_METHODS`) |
81
+ | reintentos | `max_retries` (default 3), solo en métodos idempotentes; POST/PATCH = 0 |
82
+ | errores reintentados | `Excon::Error::Socket`, `Excon::Error::Timeout` |
83
+ | backoff | ninguno — reintento inmediato (no se setea `retry_interval`) |
84
+
85
+ **Semántica:** la frontera idempotente/no-idempotente refleja la del Docker Engine API:
86
+
87
+ - `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.
88
+ - `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`.
89
+ - `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.
90
+ - **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`.
91
+
92
+ #### e. Degradación (si la dependencia cae)
93
+
94
+ | escenario | comportamiento de la gema |
95
+ |---|---|
96
+ | socket caído / daemon no responde | tras `max_retries` (idempotentes) o inmediato (POST), levanta `DockerSwarm::Error::Communication` con el `Excon::Error::Socket` en `cause` |
97
+ | daemon devuelve 5xx | levanta la `Error::*` correspondiente (502/503/504) sin reintento HTTP |
98
+ | fallback / cola / circuit-breaker | **ninguno** — la gema es un cliente fino, fail-fast; no encola ni degrada |
99
+ | responsabilidad del consumidor | decidir reintento con backoff, fallback o propagación; la gema solo provee el error tipado |
100
+
101
+ - **SLA del proveedor:** n/a — el daemon Docker suele ser local (socket Unix) o de infraestructura propia; no hay SLA externo que documentar.
102
+ - **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.
103
+
104
+ ## 3. Inferencias
105
+
106
+ | afirmación | confidence | a verificar |
107
+ |---|---|---|
108
+ | 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 |
109
+ | `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 |
110
+
111
+ ## 4. Cobertura y fronteras
112
+
113
+ - **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.
114
+ - **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.
115
+ - **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).
116
+ - **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.
@@ -1,6 +1,6 @@
1
1
  # Glosario — docker-swarm
2
2
 
3
- > meta: artefacto · RFC-009 · generado dev-enrich · anclado a `a4e3129` · cobertura: completo inicial (primitivas Docker + arquitectura interna); no se acrecienta sin tocar el flujo/concepto
3
+ > meta: artefacto · RFC-009 · generado dev-enrich · anclado a `8f2e1f7` · cobertura: completo inicial (primitivas Docker + arquitectura interna); no se acrecienta sin tocar el flujo/concepto
4
4
 
5
5
  ## 1. Resumen
6
6
 
@@ -8,81 +8,81 @@ Términos de negocio que la gema `docker-swarm` materializa. Dos grupos: **primi
8
8
 
9
9
  ## 2. Términos
10
10
 
11
- ### Service
11
+ ## Service
12
12
 
13
13
  Servicio de Docker Swarm: definición declarativa de un conjunto de tasks que corren en el cluster. La gema lo expone como CRUD completo + `restart` + `logs`. Update atómico vía `Version.Index`.
14
14
  **Binding:** [`DockerSwarm::Service`](../../lib/docker_swarm/models/service.rb)
15
15
 
16
- ### Node
16
+ ## Node
17
17
 
18
18
  Miembro físico del cluster Swarm (manager o worker). Read-only desde el punto de vista de creación: los nodos se unen al swarm fuera de la gema; la gema sólo permite update (rol/disponibilidad) y destroy.
19
19
  **Binding:** [`DockerSwarm::Node`](../../lib/docker_swarm/models/node.rb)
20
20
 
21
- ### Task
21
+ ## Task
22
22
 
23
23
  Unidad de ejecución de un Service en un Node específico. Read-only: las tasks se generan automáticamente por el orquestador a partir del Spec del Service. La gema sólo permite listar/inspeccionar/obtener logs.
24
24
  **Binding:** [`DockerSwarm::Task`](../../lib/docker_swarm/models/task.rb)
25
25
 
26
- ### Container
26
+ ## Container
27
27
 
28
28
  Container Docker standalone (no Swarm). La gema expone start/stop/destroy/logs sobre containers existentes. Creación intencionalmente fuera de scope F1 — el caso de uso primario de la gema es Swarm.
29
29
  **Binding:** [`DockerSwarm::Container`](../../lib/docker_swarm/models/container.rb)
30
30
 
31
- ### Image
31
+ ## Image
32
32
 
33
- Imagen Docker (registry o local). La gema cubre listar, pull (`create`) y destroy. Build local no cubierto (no caso de uso de orquestación).
33
+ Imagen Docker (registry o local). La gema cubre listar, pull (`Image.pull`, explícito y síncrono) y destroy. Build local no cubierto (no caso de uso de orquestación). El `create` genérico se retiró: `Image` ya no es Creatable.
34
34
  **Binding:** [`DockerSwarm::Image`](../../lib/docker_swarm/models/image.rb)
35
35
 
36
- ### Network
36
+ ## Network
37
37
 
38
38
  Red Docker (overlay para Swarm, bridge para Container). CRUD completo. Update soporta conectar/desconectar containers.
39
39
  **Binding:** [`DockerSwarm::Network`](../../lib/docker_swarm/models/network.rb)
40
40
 
41
- ### Volume
41
+ ## Volume
42
42
 
43
43
  Volumen Docker (named volume). CRUD sin update (Docker no soporta update de Volume). La respuesta del index viene envuelta en `{"Volumes": [...]}` — manejado vía `root_key`.
44
44
  **Binding:** [`DockerSwarm::Volume`](../../lib/docker_swarm/models/volume.rb)
45
45
 
46
- ### Config
46
+ ## Config
47
47
 
48
48
  Configuración inmutable distribuida en el cluster (archivos de configuración, manifests). CRUD sin update — Docker requiere recrear. Sólo Swarm.
49
49
  **Binding:** [`DockerSwarm::Config`](../../lib/docker_swarm/models/config.rb)
50
50
 
51
- ### Secret
51
+ ## Secret
52
52
 
53
53
  Dato sensible distribuido en el cluster (passwords, tokens, certs). Misma semántica que Config pero el `Data` se filtra automáticamente en logs vía `LogHelper`. Sólo Swarm.
54
54
  **Binding:** [`DockerSwarm::Secret`](../../lib/docker_swarm/models/secret.rb)
55
55
 
56
- ### Swarm
56
+ ## Swarm
57
57
 
58
58
  Cluster Docker Swarm como entidad singleton. Sólo `show` (info del cluster: ID, Version, Spec, JoinTokens). No CRUD — el cluster se inicializa/disuelve fuera de la gema.
59
59
  **Binding:** [`DockerSwarm::Swarm`](../../lib/docker_swarm/models/swarm.rb)
60
60
 
61
- ### System
61
+ ## System
62
62
 
63
63
  Daemon Docker como entidad singleton. Métodos estáticos: `info`, `version`, `up` (ping), `df` (disk usage). Útil para health checks y observabilidad.
64
64
  **Binding:** [`DockerSwarm::System`](../../lib/docker_swarm/models/system.rb)
65
65
 
66
66
  ---
67
67
 
68
- ### Base (ORM base)
68
+ ## Base (ORM base)
69
69
 
70
70
  Clase base de todos los modelos. Hereda de `ActiveModel::Model`. Provee accessors dinámicos PascalCase, `find`, `all`, `where`, `reload`, `payload_for_docker`. Centraliza el patrón ORM contra Docker Engine API.
71
71
  **Binding:** [`DockerSwarm::Base`](../../lib/docker_swarm/base.rb)
72
72
 
73
- ### Concern
73
+ ## Concern
74
74
 
75
75
  Mixin (`ActiveSupport::Concern`) que agrega capacidad CRUD/auxiliar a un modelo. La gema define cinco concerns ortogonales: cada modelo incluye los que aplican a su semántica Docker.
76
76
 
77
77
  | Concern | Símbolo | Aplica a |
78
78
  |---|---|---|
79
- | Creatable | [`DockerSwarm::Concerns::Creatable`](../../lib/docker_swarm/concerns/creatable.rb) | Service, Network, Volume, Config, Secret, Image |
79
+ | Creatable | [`DockerSwarm::Concerns::Creatable`](../../lib/docker_swarm/concerns/creatable.rb) | Service, Network, Volume, Config, Secret |
80
80
  | Updatable | [`DockerSwarm::Concerns::Updatable`](../../lib/docker_swarm/concerns/updatable.rb) | Service, Node, Network |
81
81
  | Deletable | [`DockerSwarm::Concerns::Deletable`](../../lib/docker_swarm/concerns/deletable.rb) | Service, Node, Container, Network, Volume, Config, Secret, Image |
82
82
  | Loggable | [`DockerSwarm::Concerns::Loggable`](../../lib/docker_swarm/concerns/loggable.rb) | Service, Task, Container |
83
83
  | Inspectable | [`DockerSwarm::Concerns::Inspectable`](../../lib/docker_swarm/concerns/inspectable.rb) | Todos (vía Base) |
84
84
 
85
- ### Middleware
85
+ ## Middleware
86
86
 
87
87
  Capa Excon en el stack del cliente HTTP. Tres middlewares custom: serialización de body, parsing de respuesta con indifferent access, mapeo de status a excepción tipada.
88
88
 
@@ -92,32 +92,32 @@ Capa Excon en el stack del cliente HTTP. Tres middlewares custom: serialización
92
92
  | ResponseJSONParser | [`DockerSwarm::Middleware::ResponseJSONParser`](../../lib/docker_swarm/middleware/response_json_parser.rb) | Parsea JSON y aplica `with_indifferent_access` |
93
93
  | ErrorHandler | [`DockerSwarm::Middleware::ErrorHandler`](../../lib/docker_swarm/middleware/error_handler.rb) | Mapea 4xx/5xx → `DockerSwarm::Error::*` + log `business_error` |
94
94
 
95
- ### Connection
95
+ ## Connection
96
96
 
97
97
  Wrapper sobre el cliente Excon. Memoiza la conexión, aplica timeouts/retries de configuración, clasifica errores idempotentes vs no-idempotentes (post-fix correctness), y emite logs KV.
98
98
  **Binding:** [`DockerSwarm::Connection`](../../lib/docker_swarm/connection.rb)
99
99
 
100
- ### Dynamic Accessor
100
+ ## Dynamic Accessor
101
101
 
102
102
  Mecanismo por el cual los modelos exponen atributos no declarados. Docker Engine evoluciona y agrega campos: la gema usa `method_missing` + cache en `defined_attributes` (Set) para responder a cualquier campo PascalCase de la respuesta sin requerir update del código.
103
103
  **Binding:** [`DockerSwarm::Base#method_missing`](../../lib/docker_swarm/base.rb), [`DockerSwarm::Base.defined_attributes`](../../lib/docker_swarm/base.rb)
104
104
 
105
- ### Spec deep_merge
105
+ ## Spec deep_merge
106
106
 
107
107
  Estrategia de actualización parcial del campo `Spec` de un modelo. En vez de reemplazar Spec completo, `assign_attributes` hace `deep_merge` cuando la key es `Spec` y ambos valores son Hash. Razón: updates parciales no pierden campos anidados no tocados.
108
108
  **Binding:** [`DockerSwarm::Base#assign_attributes`](../../lib/docker_swarm/base.rb)
109
109
 
110
- ### Version.Index
110
+ ## Version.Index
111
111
 
112
112
  Mecanismo de control de concurrencia optimista de Docker para updates atómicos. Cada Service/Node tiene `Version.Index` que incrementa en cada cambio. Update requiere enviar el index actual como query param; si no coincide, Docker rechaza (500). La gema lo extrae automáticamente en `Updatable#update`.
113
113
  **Binding:** [`DockerSwarm::Concerns::Updatable#update`](../../lib/docker_swarm/concerns/updatable.rb)
114
114
 
115
- ### LogHelper
115
+ ## LogHelper
116
116
 
117
117
  Módulo de formateo de logs en KV (`key=value`) con masking automático de claves sensibles. La regex (`password|pass|passwd|secret|token|api_key|auth|\bdata\b`) reemplaza valores por `[FILTERED]`. `\bdata\b` evita falsos positivos en `metadata`/`database`.
118
118
  **Binding:** [`DockerSwarm::LogHelper`](../../lib/docker_swarm/log_helper.rb)
119
119
 
120
- ### payload_for_docker
120
+ ## payload_for_docker
121
121
 
122
122
  Transformación interna que prepara un modelo para enviarlo al API: descarta atributos internos (`ID`/`Version`/`CreatedAt`/`UpdatedAt`), extrae el contenido de `Spec` al root y mergea otros campos top-level. Resultado: el payload que Docker espera para `create`/`update`.
123
123
  **Binding:** [`DockerSwarm::Base#payload_for_docker`](../../lib/docker_swarm/base.rb)