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 +4 -4
- data/CHANGELOG.md +12 -0
- data/README.md +14 -7
- data/docs/behavior/behavior.md +58 -3
- data/docs/consumed/docker-engine-api.md +116 -0
- data/docs/errors/errors.md +103 -0
- data/docs/glossary/glossary.md +23 -23
- data/docs/interface/interface.md +118 -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 +13 -6
- data/lib/docker_swarm/concerns/updatable.rb +15 -4
- data/lib/docker_swarm/connection.rb +6 -6
- data/lib/docker_swarm/log_helper.rb +26 -3
- 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 +1 -0
- data/skill/SKILL.md +14 -8
- metadata +10 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 61bc38a1b8f94704857812a859abbe425f57a63130655bcbaf7938f5621ad09a
|
|
4
|
+
data.tar.gz: 1b824044722bc3a3c840889b5fef843aa6eafd25576ea25811ac8ee8e688d4e3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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) |
|
|
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 (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.
|
|
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 `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
|
|
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.
|
data/docs/glossary/glossary.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Glosario — docker-swarm
|
|
2
2
|
|
|
3
|
-
> meta: artefacto · RFC-009 · generado dev-enrich · anclado a `
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
31
|
+
## Image
|
|
32
32
|
|
|
33
|
-
Imagen Docker (registry o local). La gema cubre listar, pull (`
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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)
|