docker-swarm 0.5.4 → 0.7.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: 4f9bd0d4eefbad47fc20b9782dbbd580f1c95c53eb04bb7a21a65c118d773025
4
- data.tar.gz: bb5591f35bf9bc5a39e321429bee25c371e051bea0005abe16a1c32829cd364c
3
+ metadata.gz: 2cfb44c7a62e5acff4c50b2ef3a0e6bf1099a373168dd5ab5b84acaf64b992be
4
+ data.tar.gz: cfce12e7a89c7445e529abb318d96b4537ea48f57feeb9a8a868811cf36e5761
5
5
  SHA512:
6
- metadata.gz: ee250666f9059abbfa04cba500e009e1988acfebd6a21d28d773fa9a28989908dc65b211127e0248b925fc964766a8476298448e864c6ebe45b0c34982a03729
7
- data.tar.gz: ba04b6783f3ae9e686ee25638c3effe3e525881106a91cb066a07928016d5b71cca83bb91cbee601cc00c9959c208df3914a6ad74410d9ce76a288e5d1d1bcbc
6
+ metadata.gz: 21537e14be775b36a683347b95b20c7bdf470390196aef69ec33f2107beab3b3b05ad293961f17fe9dde25537893323abb54b9cadeae0787aeff2f903cc4133d
7
+ data.tar.gz: 65be2402e1745493295b3f399343d659bb3b63bc15af92ad13f788172658229a28af3a6ce22d58b92af103757c4ab384a81c2e6d6ba7045e6b11585e0d3800c8
data/CHANGELOG.md ADDED
@@ -0,0 +1,121 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ ## [0.7.0] — 2026-05-21
6
+
7
+ ### Breaking changes
8
+ - `Connection#request`: retries automáticos sólo en métodos seguros (GET/HEAD/PUT/DELETE/OPTIONS). POST/PATCH ya **no** reintentan para evitar recursos duplicados ante caída de socket — @Gabriel
9
+ - `Base#assign_attributes`: rama muerta que aceptaba no-Hash ahora levanta `ArgumentError` con mensaje claro — @Gabriel
10
+ - Ruby floor: `>= 3.2.0` (antes `>= 2.7.0`), alineado con `activesupport` 8.1 — @Gabriel
11
+
12
+ ### Correcciones
13
+ - `Connection#request`: clasificación de errores por `is_a?(DockerSwarm::Error)` en vez de `class.name.include?`, evita falsos positivos con clases externas — @Gabriel
14
+ - `LogHelper::SENSITIVE_KEYS`: `data` ahora se matchea con `\b` para no filtrar `metadata`, `database` u otras claves legítimas — @Gabriel
15
+
16
+ ### Documentación
17
+ - Re-estructura completa bajo skills `dev-*` (RFC-001/007/008/009/010): nuevos `docs/glossary/glossary.md` (20 términos) y `docs/behavior/behavior.md` (8 flujos load-bearing con Mermaid). `README.md` minimal-control y `skill/SKILL.md` con frontmatter, contrato resumido, version-lock — @Gabriel
18
+ - Eliminados docs ad-hoc pre-estándar (`docs/{api,configuration,errors,models,testing}.md` y `skill/references/`) — @Gabriel
19
+
20
+ ### Mejoras internas
21
+ - CI: tests unitarios en `bundle exec rspec --tag ~type:integration` para que el build verde sea significativo sin Docker disponible — @Gabriel
22
+ - Gemspec: `metadata` con `changelog_uri`/`bug_tracker_uri`/`documentation_uri`/`rubygems_mfa_required` version-locked; `LICENSE`, `CHANGELOG.md` y `docs/**/*` incluidos en `spec.files` para que viajen con el release — @Gabriel
23
+ - `LICENSE` (MIT) agregado al repo — @Gabriel
24
+ - Removido `debug_docker.rb` del repo (artefacto de desarrollo) — @Gabriel
25
+
26
+ ## [0.6.0] - 2026-04-08
27
+
28
+ ### Nuevas funcionalidades
29
+ - `Service#restart`: reinicia un servicio incrementando `ForceUpdate` en el `TaskTemplate`, equivalente a `docker service update --force` — @Gabriel
30
+
31
+ ## [0.5.4] - 2026-04-07
32
+
33
+ ### Correcciones
34
+ - `Inspectable`: todos los atributos deben usar `send(:attr)` para invocar métodos dinámicos — @Gabriel
35
+
36
+ ## [0.5.3] - 2026-04-07
37
+
38
+ ### Mejoras internas
39
+ - `Inspectable` ahora muestra todos los atributos (ID, Name, CreatedAt, UpdatedAt, Version, Spec) — @Gabriel
40
+
41
+ ## [0.5.2] - 2026-04-07
42
+
43
+ ### Correcciones
44
+ - Fix en `Inspectable`: usar `send(:ID)` en vez de `ID` directamente para evitar `NameError` por interpretación como constante — @Gabriel
45
+
46
+ ## [0.5.1] - 2026-04-06
47
+
48
+ ### Mejoras internas
49
+ - Refactor de logs en `Connection`: cambio de `duration_ms` a `duration_s` y de `status` a `http_status` para alinearse con estándares de observabilidad — @Gabriel
50
+ - Precisión de duración mejorada usando segundos con 4 decimales — @Gabriel
51
+ - Reestructuración de `skills.yml` para soportar variables de entorno en agentes y nuevas skills — @Gabriel
52
+
53
+ ## [0.5.0] - 2026-04-04
54
+
55
+ ### Nuevas funcionalidades
56
+ - Skill de conocimiento empaquetada (`skill/`) para consumo via `skill-manager sync` — @Gabriel
57
+ - Helper de testing documentado (`DockerSwarmHelpers`) con `stub_docker_find` y `stub_docker_list` — @Claude
58
+
59
+ ### Mejoras internas
60
+ - README reescrito con instalación, más ejemplos de uso, y sección Documentación linkeando a `docs/` — @Claude
61
+ - `docs/testing.md` reescrito: mockeo de CRUD, errores, helper reutilizable — @Claude
62
+ - `docs/errors.md` completado con errores faltantes (NotAcceptable 406, RequestTimeout 408, BadGateway 502) — @Claude
63
+ - Cross-references bidireccionales entre todos los docs — @Claude
64
+ - Centralización de logging en `LogHelper` con masking de campo `Data` para Secret/Config — @Gabriel
65
+ - Gemspec actualizado para empaquetar `skill/**/*` en el `.gem` — @Claude
66
+
67
+ ## [0.4.0] - 2026-03-31
68
+
69
+ ### Nuevas funcionalidades
70
+ - Dependabot configurado para actualizaciones semanales de gemas y mensuales de GitHub Actions — @Gabriel
71
+ - Integración de `rubocop-rails-omakase` y verificación RuboCop en CI — @Gabriel
72
+ - Tests de infraestructura robustos para `Connection`, `Configuration` y Middlewares — @Gabriel
73
+ - Documentación YARD en todos los modelos y componentes core — @Gabriel
74
+ - Concern `Inspectable` para inspección legible en consola (ID, Name, Image) — @Gabriel
75
+
76
+ ### Mejoras internas
77
+ - Memoización de accessors en `Base` para optimizar procesamiento de recursos — @Gabriel
78
+ - Refactor de logs a `Concerns::Loggable` para DRY en Service, Task y Container — @Gabriel
79
+ - Branch default renombrado de `master` a `main` — @Gabriel
80
+
81
+ ### Correcciones
82
+ - Casting automático de timeouts y retries para prevenir `TypeError` con ENV variables — @Gabriel
83
+ - Orden de carga interno reorganizado para resolver issues de inicialización — @Gabriel
84
+
85
+ ## [0.3.0] - 2026-03-31
86
+
87
+ ### Nuevas funcionalidades
88
+ - Timeouts y retries configurables: `read_timeout`, `write_timeout`, `connect_timeout`, `max_retries` — @Gabriel
89
+ - `RequestEncoder` soporta `application/x-www-form-urlencoded` y `multipart/form-data` — @Gabriel
90
+ - `ResponseJSONParser` aplica `with_indifferent_access` recursivo en Arrays — @Gabriel
91
+
92
+ ## [0.2.0] - 2026-03-31
93
+
94
+ ### Nuevas funcionalidades
95
+ - Logging estructurado KV (`component`, `event`, `source`, `duration_ms`) estándar Wispro — @Gabriel
96
+ - Reloj monotónico para medición precisa de duración de requests — @Gabriel
97
+ - Masking automático de claves sensibles en logs — @Gabriel
98
+ - Soporte para Unix sockets y HTTP/TCP via `socket_path` — @Gabriel
99
+ - `Network.update` implementado — @Gabriel
100
+ - `log_level` configurable en runtime — @Gabriel
101
+ - Error `TooManyRequests` (429) — @Gabriel
102
+ - Tests de integración para Service, Node, Task, Swarm, System, Image y Container — @Gabriel
103
+
104
+ ### Mejoras internas
105
+ - Modelos movidos a `lib/docker_swarm/models/` — @Gabriel
106
+ - `Base.all` normalizado para respuestas envueltas (`root_key`) — @Gabriel
107
+ - Método `logs` estandarizado a nivel de instancia — @Gabriel
108
+ - `Swarm` y `System` heredan de `Base` con atributos dinámicos — @Gabriel
109
+ - Jerarquía de errores reestructurada bajo `DockerSwarm::Error` con aliases — @Gabriel
110
+
111
+ ### Correcciones
112
+ - Excon ya no envuelve excepciones de negocio en `Excon::Error::Socket` — @Gabriel
113
+ - `Volume.all` corregido para la respuesta envuelta de Docker — @Gabriel
114
+ - `Gateway_Timeout` renombrado a `GatewayTimeout` — @Gabriel
115
+ - Entry point `lib/docker-swarm.rb` agregado para Bundler/Rails — @Gabriel
116
+ - Validaciones de `Service` relajadas para flexibilidad en tests — @Gabriel
117
+
118
+ ---
119
+
120
+ ## [0.1.0] - Early 2026
121
+ - Release inicial con ORM básico para Services, Networks y Volumes — @Gabriel
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Gabriel
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md CHANGED
@@ -1,139 +1,66 @@
1
- # Docker Swarm Gem
1
+ # docker-swarm
2
2
 
3
3
  [![Gem Version](https://badge.fury.io/rb/docker-swarm.svg)](https://badge.fury.io/rb/docker-swarm)
4
- [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
5
5
 
6
- `docker-swarm` es un ORM ligero y cliente API robusto para interactuar con Docker Swarm desde Ruby. Diseñado para sentirse familiar a los desarrolladores de Rails, utiliza `ActiveModel` para ofrecer una interfaz limpia y potente con estándares de observabilidad de Wispro.
6
+ ## Propósito
7
7
 
8
- ## Instalacion
8
+ ORM ligero y cliente API para Docker Swarm en Ruby. Expone las primitivas del Docker Engine (Service, Node, Task, Container, Network, Volume, Config, Secret, Swarm, System, Image) como modelos `ActiveModel` con CRUD, validaciones y logs estructurados KV. Excon directo sobre el socket Unix (default) o TCP.
9
9
 
10
- Agrega la gema a tu `Gemfile`:
10
+ ## Setup
11
11
 
12
12
  ```ruby
13
- gem 'docker-swarm', '~> 0.5'
13
+ # Gemfile
14
+ gem 'docker-swarm', '~> 0.6'
14
15
  ```
15
16
 
16
- Y ejecuta:
17
-
18
17
  ```bash
19
18
  bundle install
20
19
  ```
21
20
 
22
- ## Quick Start
21
+ Quick start:
23
22
 
24
23
  ```ruby
25
24
  require 'docker_swarm'
26
25
 
27
- # Configurar (opcional, usa defaults razonables)
28
26
  DockerSwarm.configure do |config|
29
- config.socket_path = "unix:///var/run/docker.sock"
27
+ config.socket_path = "unix:///var/run/docker.sock" # default
30
28
  config.log_level = Logger::INFO
31
- config.max_retries = 3
32
29
  end
33
30
 
34
- # Verificar conectividad
35
- DockerSwarm::System.up # => "OK"
36
-
37
- # Listar servicios
38
- services = DockerSwarm::Service.all
39
- services.each { |s| puts "#{s.ID}: #{s.Spec[:Name]}" }
40
- ```
41
-
42
- ## Uso
43
-
44
- ### Crear un servicio
45
-
46
- ```ruby
47
- service = DockerSwarm::Service.create(
48
- Name: "my-webapp",
49
- TaskTemplate: {
50
- ContainerSpec: { Image: "nginx:latest" }
51
- }
52
- )
53
- puts service.ID
54
- ```
55
-
56
- ### Actualizar (maneja Version.Index automaticamente)
57
-
58
- ```ruby
59
- service = DockerSwarm::Service.find("svc-id")
60
- service.update(Mode: { Replicated: { Replicas: 3 } })
61
- ```
62
-
63
- ### Eliminar
64
-
65
- ```ruby
66
- service.destroy # graceful: retorna nil si ya no existe
67
- ```
68
-
69
- ### Logs
70
-
71
- ```ruby
72
- puts service.logs(stdout: 1, stderr: 1)
73
- ```
74
-
75
- ### Filtros
76
-
77
- ```ruby
78
- DockerSwarm::Service.all(label: ["env=production"])
79
- DockerSwarm::Node.all(role: ["manager"])
80
- DockerSwarm::Container.all(status: ["running"])
31
+ DockerSwarm::System.up # => "OK"
32
+ DockerSwarm::Service.all # => [#<Service ...>, ...]
33
+ DockerSwarm::Service.find("svc-id") # => nil si no existe
81
34
  ```
82
35
 
83
- ### Sistema y Cluster
36
+ Para el contrato completo de la gema (símbolos públicos, gotchas, integración) ver [`skill/SKILL.md`](skill/SKILL.md).
84
37
 
85
- ```ruby
86
- DockerSwarm::System.info # informacion del daemon
87
- DockerSwarm::System.version # version de Docker
88
- DockerSwarm::System.df # uso de disco
89
- DockerSwarm::Swarm.show # informacion del cluster
90
- ```
38
+ ## Índice de artefactos
91
39
 
92
- ### Manejo de errores
40
+ Documentación normada (RFC-001) por capa:
93
41
 
94
- ```ruby
95
- begin
96
- DockerSwarm::Service.create(Name: "web", TaskTemplate: { ... })
97
- rescue DockerSwarm::Conflict => e
98
- puts "Nombre duplicado"
99
- rescue DockerSwarm::Communication => e
100
- puts "Docker inalcanzable: #{e.message}"
101
- end
102
- ```
42
+ | Capa | Artefacto | Estado |
43
+ |---|---|---|
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
+ | API (operaciones) | — | F2 pendiente; contrato resumido inline en `skill/SKILL.md` |
48
+ | Interfaz | — | F2 pendiente; contrato resumido inline en `skill/SKILL.md` |
49
+ | Topología | — | F2 pendiente |
50
+ | Eventos | — | `n/a` (la gema no emite eventos) |
103
51
 
104
- ## Configuracion
52
+ `n/a` = no aplica al tipo de repo. F2 pendiente = capa declarada en la RFC pero todavía no implementada en `dev-structure`; el contenido relevante vive transitoriamente en `skill/SKILL.md` (RFC-008 §2 coexistencia transitoria).
105
53
 
106
- ```ruby
107
- DockerSwarm.configure do |config|
108
- config.socket_path = "unix:///var/run/docker.sock" # o http://host:port
109
- config.logger = Logger.new($stdout)
110
- config.log_level = Logger::INFO
111
- config.read_timeout = 60 # segundos
112
- config.write_timeout = 60
113
- config.connect_timeout = 10
114
- config.max_retries = 3
115
- end
116
- ```
117
-
118
- Ver [Guia de Configuracion](docs/configuration.md) para opciones avanzadas, Rails integration y observabilidad.
119
-
120
- ## Documentacion
121
-
122
- - [Modelos (ORM)](docs/models.md) — Todos los modelos, concerns y ciclo de vida
123
- - [Configuracion](docs/configuration.md) — Opciones, observabilidad y seguridad
124
- - [Manejo de Errores](docs/errors.md) — Jerarquia de excepciones y uso
125
- - [Cliente API](docs/api.md) — Acceso de bajo nivel y middlewares
126
- - [Testing](docs/testing.md) — Estrategias de mocking para tus tests
127
-
128
- ## Contribuir
129
-
130
- Las contribuciones son bienvenidas. Por favor, lee `CLAUDE.md` para las guias de desarrollo.
54
+ ## Desarrollo
131
55
 
132
56
  ```bash
133
- bundle exec rspec # tests
134
- bundle exec rubocop -a # linting
57
+ bundle exec rspec --tag ~type:integration # unit suite
58
+ bundle exec rspec # incluye integration (requiere Docker socket)
59
+ bundle exec rubocop -a # lint
135
60
  ```
136
61
 
62
+ Release: `/gem-release` (publica a RubyGems via GitHub Action al tag `v*`).
63
+
137
64
  ## Licencia
138
65
 
139
- Este proyecto esta bajo la licencia MIT.
66
+ MIT ver [LICENSE](LICENSE).
@@ -0,0 +1,243 @@
1
+ # Comportamiento — docker-swarm
2
+
3
+ > meta: artefacto · RFC-007 · generado dev-enrich · anclado a `a4e3129` · cobertura: backfill on-demand inicial (8 flujos load-bearing)
4
+
5
+ ## 1. Resumen
6
+
7
+ Flujos de ejecución load-bearing de `docker-swarm`: cómo se materializan en runtime las operaciones CRUD, el control de concurrencia optimista de Docker, la política de retries post-fix correctness, y el mapeo de errores. Render Mermaid sequenceDiagram/flowchart por flujo.
8
+
9
+ ## 2. Cobertura declarada
10
+
11
+ ### Documentados (8)
12
+
13
+ 1. `Service.create` + reload
14
+ 2. `Service.update` con `Version.Index`
15
+ 3. `Service.restart` (force update)
16
+ 4. `Model.destroy` (graceful 404)
17
+ 5. Política de retries por método HTTP
18
+ 6. Error mapping (HTTP status → excepción tipada)
19
+ 7. `Container.start` / `Container.stop`
20
+ 8. `Loggable#logs` streaming
21
+
22
+ ### No documentados (ausencia ≠ inexistencia, RFC-007)
23
+
24
+ - Reconexión / reapertura de socket Unix (Excon nativo, fuera de nuestra superficie).
25
+ - 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
+ ## 3. Flujos
30
+
31
+ ### 3.1 `Service.create` + reload
32
+
33
+ Crear un Service implica un POST seguido de un reload automático para hidratar la instancia con la respuesta completa de Docker (el endpoint `create` sólo devuelve `ID`).
34
+
35
+ ```mermaid
36
+ sequenceDiagram
37
+ actor Caller
38
+ participant Service as DockerSwarm::Service
39
+ participant Api as DockerSwarm::Api
40
+ participant Conn as Connection
41
+ participant Docker as Docker Engine
42
+
43
+ Caller->>Service: Service.create(Name:, TaskTemplate:)
44
+ Service->>Service: new(attrs) → valid?
45
+ Service->>Api: request(:create, payload: payload_for_docker)
46
+ Api->>Conn: request(method: :post, path: services/create, body:)
47
+ Conn->>Docker: POST /services/create
48
+ Docker-->>Conn: 201 { ID: "abc" }
49
+ Conn-->>Service: { ID: "abc" }
50
+ Service->>Service: self.ID = "abc"
51
+ Service->>Api: request(:show, arguments: { id: "abc" })
52
+ Api->>Conn: request(method: :get, path: services/abc)
53
+ Conn->>Docker: GET /services/abc
54
+ Docker-->>Conn: 200 { ID, Spec, Version, ... }
55
+ Conn-->>Service: full payload
56
+ Service-->>Caller: hydrated Service instance
57
+ ```
58
+
59
+ **Notas load-bearing:**
60
+ - `valid?` corre antes del POST. Si falla, `save` retorna `false` sin tocar el API.
61
+ - El POST no se reintenta automáticamente: ver §3.5 (retry policy).
62
+ - El reload usa `find` interno; si Docker devuelve 404 entre create+show (caso extremo), el reload no se realiza y la instancia queda sólo con `ID`.
63
+
64
+ ### 3.2 `Service.update` con `Version.Index`
65
+
66
+ Updates atómicos sobre Service/Node/Network requieren enviar el `Version.Index` actual como query param. Sin él, Docker rechaza con 500. La gema lo extrae automáticamente de `self.Version["Index"]`.
67
+
68
+ ```mermaid
69
+ sequenceDiagram
70
+ actor Caller
71
+ participant Model as Service/Node/Network
72
+ participant Api
73
+ participant Docker as Docker Engine
74
+
75
+ Caller->>Model: model.update(Mode: { Replicated: { Replicas: 3 } })
76
+ Model->>Model: assign_attributes(new_attrs) -- Spec deep_merge si aplica
77
+ Model->>Model: valid?
78
+ Model->>Api: request(:update, id:, query: { version: Version.Index }, payload:)
79
+ Api->>Docker: POST /services/abc/update?version=42
80
+ alt version coincide
81
+ Docker-->>Api: 200 OK
82
+ Api-->>Model: success
83
+ Model-->>Caller: true
84
+ else version stale
85
+ Docker-->>Api: 500 update out of sequence
86
+ Api->>Api: ErrorHandler → raise InternalServerError
87
+ Api-->>Caller: DockerSwarm::Error::InternalServerError
88
+ end
89
+ ```
90
+
91
+ **Notas load-bearing:**
92
+ - `assign_attributes` muta el estado local **antes** de validar/enviar. Si el update falla, la instancia queda mutada (gotcha conocido, ver SKILL.md).
93
+ - `Spec` se mergea con `deep_merge`; otros atributos top-level se asignan directo.
94
+ - El update **no** hace reload automático (a diferencia de `create`). El caller llama `reload` si necesita el `Version.Index` nuevo.
95
+
96
+ ### 3.3 `Service.restart` (force update)
97
+
98
+ Reinicio sin downtime equivalente a `docker service update --force`: incrementa `ForceUpdate` del `TaskTemplate` para que Docker recree todas las tasks.
99
+
100
+ ```mermaid
101
+ flowchart LR
102
+ A["service.restart"] --> B["lee Spec.TaskTemplate.ForceUpdate"]
103
+ B --> C["current = ForceUpdate o 0 si nil"]
104
+ C --> D["update Spec: TaskTemplate: ForceUpdate: current+1"]
105
+ D --> E["POST /services/id/update?version=X"]
106
+ E --> F["Docker recrea tasks"]
107
+ ```
108
+
109
+ **Notas load-bearing:**
110
+ - Usa el flujo §3.2 internamente — hereda Version.Index, retry-policy y error-mapping.
111
+ - Si `Spec` o `TaskTemplate` no existen aún (instancia nueva sin reload), `current = 0`.
112
+
113
+ ### 3.4 `Model.destroy` (graceful 404)
114
+
115
+ DELETE con tolerancia a 404: si el recurso ya fue eliminado, `destroy` retorna `nil` en vez de raise.
116
+
117
+ ```mermaid
118
+ sequenceDiagram
119
+ actor Caller
120
+ participant Model
121
+ participant Api
122
+ participant Docker
123
+
124
+ Caller->>Model: model.destroy
125
+ Model->>Api: request(:destroy, id:)
126
+ Api->>Docker: DELETE /services/abc
127
+ alt recurso existe
128
+ Docker-->>Api: 200 OK
129
+ Api-->>Model: true
130
+ Model-->>Caller: true
131
+ else 404 (ya eliminado)
132
+ Docker-->>Api: 404 Not Found
133
+ Api->>Api: ErrorHandler → raise NotFound
134
+ Model->>Model: rescue Errors::NotFound
135
+ Model-->>Caller: nil
136
+ end
137
+ ```
138
+
139
+ **Notas load-bearing:**
140
+ - Tanto `Model.destroy(id)` (class method) como `model.destroy` (instance) capturan `NotFound`.
141
+ - 409 (`Conflict` — recurso en uso) **no** se captura; el caller debe manejarlo si aplica.
142
+
143
+ ### 3.5 Política de retries por método HTTP
144
+
145
+ Post-fix correctness (PR #3): los retries automáticos sólo aplican a métodos seguros. POST/PATCH no reintentan para evitar duplicados.
146
+
147
+ ```mermaid
148
+ flowchart TD
149
+ Req[Connection#request<br/>method, path, body] --> Class{IDEMPOTENT_METHODS<br/>incluye method?}
150
+ Class -- get/head/put/delete/options --> Safe[idempotent: true<br/>retries: max_retries]
151
+ Class -- post/patch/otro --> Write[idempotent: false<br/>retries: 0]
152
+ Safe --> Excon[Excon.request<br/>retry on Socket/Timeout]
153
+ Write --> ExconNoRetry[Excon.request<br/>sin retry]
154
+ ```
155
+
156
+ **Notas load-bearing:**
157
+ - Si el socket se cierra durante la lectura de la respuesta de un `services/create`, **no** se replica — la instancia queda en error y el caller decide.
158
+ - `max_retries` (default 3) sólo afecta lecturas/idempotentes.
159
+
160
+ ### 3.6 Error mapping (status → excepción)
161
+
162
+ `ErrorHandler` interpreta status HTTP 4xx/5xx y emite excepción tipada de la jerarquía `DockerSwarm::Error`.
163
+
164
+ ```mermaid
165
+ flowchart TD
166
+ Resp[Response status, body] --> Range{status range}
167
+ Range -- 200-299 --> Pass[pass to next middleware]
168
+ Range -- 4xx/5xx --> Log[log business_error<br/>KV format]
169
+ Log --> Map{status}
170
+ Map -- 400 --> BadRequest
171
+ Map -- 401 --> Unauthorized
172
+ Map -- 403 --> Forbidden
173
+ Map -- 404 --> NotFound
174
+ Map -- 406 --> NotAcceptable
175
+ Map -- 408 --> RequestTimeout
176
+ Map -- 409 --> Conflict
177
+ Map -- 422 --> UnprocessableEntity
178
+ Map -- 429 --> TooManyRequests
179
+ Map -- 500 --> InternalServerError
180
+ Map -- 502 --> BadGateway
181
+ Map -- 503 --> ServiceUnavailable
182
+ Map -- 504 --> GatewayTimeout
183
+ Map -- otro --> GenericError[Error HTTP N]
184
+ ```
185
+
186
+ **Notas load-bearing:**
187
+ - Antes de raise, `ErrorHandler` emite un evento `business_error` en formato KV con `status`, `message`, `method`, `path`. Permite diagnosticar sin abrir backtrace.
188
+ - Errores de red (no HTTP) se mapean a `DockerSwarm::Error::Communication` en `Connection#request`.
189
+ - El mensaje de la excepción viene del body parseado: prefiere `body["message"]`, luego `body["error"]`, luego el body completo.
190
+
191
+ ### 3.7 `Container.start` / `Container.stop`
192
+
193
+ Operaciones específicas sin payload (POST a endpoint dedicado).
194
+
195
+ ```mermaid
196
+ sequenceDiagram
197
+ actor Caller
198
+ participant Container as DockerSwarm::Container
199
+ participant Api
200
+ participant Docker
201
+
202
+ Caller->>Container: container.start
203
+ Container->>Api: request(:start, id:)
204
+ Api->>Docker: POST /containers/abc/start
205
+ Docker-->>Api: 204 No Content
206
+ Api-->>Container: ""
207
+ Container-->>Caller: true
208
+ ```
209
+
210
+ Mismo patrón para `stop`. POST sin body → no se reintenta automáticamente (§3.5).
211
+
212
+ ### 3.8 `Loggable#logs` streaming
213
+
214
+ Obtención de logs raw para Service/Task/Container.
215
+
216
+ ```mermaid
217
+ sequenceDiagram
218
+ actor Caller
219
+ participant Model as Service/Task/Container
220
+ participant Api
221
+ participant Docker
222
+
223
+ Caller->>Model: model.logs(stdout: 1, stderr: 1, follow: 0)
224
+ Model->>Api: request(:logs, id:, query: { stdout:, stderr:, follow: })
225
+ Api->>Docker: GET /services/abc/logs?stdout=1&stderr=1
226
+ Docker-->>Api: 200 raw stream (text/plain)
227
+ Api-->>Model: raw body
228
+ Model-->>Caller: String
229
+ ```
230
+
231
+ **Notas load-bearing:**
232
+ - El body se devuelve sin parseo (no es JSON; `ResponseJSONParser` lo respeta porque Content-Type no es `application/json`).
233
+ - `follow: 1` mantiene la conexión abierta — el caller debe manejar el stream/timeout.
234
+
235
+ ## 4. Cobertura y fronteras
236
+
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.
238
+ - **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
+ - **Frontera con configuración:** `DockerSwarm.configure` es boot, no flujo de negocio. No se diagrama.
240
+ - **No localizable / fuera de alcance:**
241
+ - Lógica interna de Excon (retry timing, socket pool) — vive en Excon, no se inventa diagrama.
242
+ - Flujo de auth registry para `Image.create` — la gema **no implementa** `X-Registry-Auth` (gap, no flujo a documentar).
243
+ - **Cadencia incremental a partir de acá:** sólo se diagrama un flujo cuando un PR lo toca o agrega. No barrido retroactivo de legacy.