docker-swarm 0.4.0 → 0.5.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: 78ad878518bedd7c1ac7355efbb6372b49f2721fd7db5a3b0536b0872ed9f3ea
4
- data.tar.gz: 6a5b9b06a1c43e8735d111cda5e9eb65820101fe515832fdf64647834310ad12
3
+ metadata.gz: a5c66a1153f6aa54787f22d1281e4b8d1932a951f70000ff0a3af72e79c9be7b
4
+ data.tar.gz: 6469d6833f97509511d0bfffda44bbbdc650fa1c928d624efef79b5ba71221ef
5
5
  SHA512:
6
- metadata.gz: 762b554eb9e1321d1cb2cb82867809b816ba7e7a84ee55e1e34b571b0942aba9e13020d8b7019dc40c70d4457b3307f9d72ca5cc98cd5de26207c9bf9ce70c1b
7
- data.tar.gz: e092c1f28b3de38986f3f5e207dd1aafeac8449448e861675aa0be88ee4763f71aa663fc9ee8c1b3bafcfe1997b3917a0924ca9dea777b484b5163bd4b197d45
6
+ metadata.gz: bc5f0fa14f81cf2d9da225c586458a155e8ac1042d5bc3d190b17a2f4c151d1239f2dd6046e1aa078d15ddea59a322476cdce5827945f8ad74a9fdd9030d79a5
7
+ data.tar.gz: 3bd4011e41f199280010a1cb716735ef72ba54926c33c36425fb483b75611821a0ce12b07b0c731b88e0bfc63f7cfe66e84fe99c42b60c8f1ecf2745ed86feac
data/README.md CHANGED
@@ -5,54 +5,135 @@
5
5
 
6
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.
7
7
 
8
- ## 🚀 Inicio Rápido
8
+ ## Instalacion
9
+
10
+ Agrega la gema a tu `Gemfile`:
11
+
12
+ ```ruby
13
+ gem 'docker-swarm', '~> 0.5'
14
+ ```
15
+
16
+ Y ejecuta:
17
+
18
+ ```bash
19
+ bundle install
20
+ ```
21
+
22
+ ## Quick Start
9
23
 
10
24
  ```ruby
11
25
  require 'docker_swarm'
12
26
 
13
- # Configurar (opcional, usa defaults)
27
+ # Configurar (opcional, usa defaults razonables)
14
28
  DockerSwarm.configure do |config|
15
29
  config.socket_path = "unix:///var/run/docker.sock"
16
- config.log_level = Logger::INFO
17
- config.read_timeout = 30
30
+ config.log_level = Logger::INFO
18
31
  config.max_retries = 3
19
32
  end
20
33
 
21
- # Listar servicios (con soporte para Indifferent Access en arrays)
34
+ # Verificar conectividad
35
+ DockerSwarm::System.up # => "OK"
36
+
37
+ # Listar servicios
22
38
  services = DockerSwarm::Service.all
23
39
  services.each { |s| puts "#{s.ID}: #{s.Spec[:Name]}" }
40
+ ```
41
+
42
+ ## Uso
24
43
 
25
- # Crear un nuevo servicio
44
+ ### Crear un servicio
45
+
46
+ ```ruby
26
47
  service = DockerSwarm::Service.create(
27
48
  Name: "my-webapp",
28
- Spec: {
29
- TaskTemplate: {
30
- ContainerSpec: { Image: "nginx:latest" }
31
- }
49
+ TaskTemplate: {
50
+ ContainerSpec: { Image: "nginx:latest" }
32
51
  }
33
52
  )
53
+ puts service.ID
54
+ ```
34
55
 
35
- # Obtener logs (stdout y stderr habilitados por defecto)
36
- puts service.logs
56
+ ### Actualizar (maneja Version.Index automaticamente)
57
+
58
+ ```ruby
59
+ service = DockerSwarm::Service.find("svc-id")
60
+ service.update(Mode: { Replicated: { Replicas: 3 } })
37
61
  ```
38
62
 
39
- ## 🛠 Características Clave
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"])
81
+ ```
82
+
83
+ ### Sistema y Cluster
84
+
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
+ ```
91
+
92
+ ### Manejo de errores
93
+
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
+ ```
103
+
104
+ ## Configuracion
105
+
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
40
121
 
41
- - **Observabilidad Wispro**: Logging estructurado (KV) con `component`, `event`, `source` y `duration_ms` usando reloj monotónico.
42
- - **Seguridad**: Enmascaramiento automático de secretos (`password`, `token`, etc.) en los logs.
43
- - **Deep Indifferent Access**: Acceso a atributos mediante símbolos o strings, incluso en resultados de listados (`.all`).
44
- - **Resiliencia**: Gestión inteligente de timeouts (`read`, `write`, `connect`) y reintentos automáticos para errores de red.
45
- - **Mapeo PascalCase**: Mantiene la fidelidad con los atributos de Docker (e.g., `s.ID`, `s.Spec`) evitando transformaciones costosas.
46
- - **ActiveModel Ready**: Soporta validaciones, serialización JSON y comportamientos estándar de modelos Ruby.
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
47
127
 
48
- ## 🤝 Contribuir
128
+ ## Contribuir
49
129
 
50
- Las contribuciones son bienvenidas. Por favor, lee `CLAUDE.md` para las guías de desarrollo y asegúrate de que todos los tests pasen antes de enviar un PR.
130
+ Las contribuciones son bienvenidas. Por favor, lee `CLAUDE.md` para las guias de desarrollo.
51
131
 
52
132
  ```bash
53
- bundle exec rspec
133
+ bundle exec rspec # tests
134
+ bundle exec rubocop -a # linting
54
135
  ```
55
136
 
56
- ## 📄 Licencia
137
+ ## Licencia
57
138
 
58
- Este proyecto está bajo la licencia MIT.
139
+ Este proyecto esta bajo la licencia MIT.
@@ -3,7 +3,7 @@
3
3
  module DockerSwarm
4
4
  # Helper module to centralize logging logic and formatting
5
5
  module LogHelper
6
- SENSITIVE_KEYS = /password|token|api_key|auth|secret/i.freeze
6
+ SENSITIVE_KEYS = /password|token|api_key|auth|secret|data/i.freeze
7
7
 
8
8
  # Formats a hash into a KV structured string with sensitive data masking
9
9
  # @param payload [Hash] The data to format
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DockerSwarm
4
- VERSION = "0.4.0"
4
+ VERSION = "0.5.0"
5
5
  end
data/skill/SKILL.md ADDED
@@ -0,0 +1,227 @@
1
+ # DockerSwarm Expert
2
+
3
+ Skill de conocimiento completo sobre DockerSwarm. Consultame para cualquier pregunta sobre integración, arquitectura, API, errores y antipatrones.
4
+
5
+ ## Glosario
6
+
7
+ **Base** — Clase ORM base que hereda de ActiveModel::Model. Provee accessors dinámicos PascalCase, `find`, `all`, `where`, `reload`, `payload_for_docker`. Todos los modelos heredan de ella.
8
+
9
+ **Concern** — Mixin ActiveSupport::Concern que agrega comportamiento CRUD a un modelo: Creatable (POST), Updatable (POST con Version.Index), Deletable (DELETE), Loggable (logs streaming), Inspectable (#inspect legible).
10
+
11
+ **Middleware** — Capa Excon que procesa request/response: RequestEncoder (serialización body), ResponseJSONParser (parsing + indifferent access), ErrorHandler (status HTTP → excepción).
12
+
13
+ **Deep Indifferent Access** — Toda respuesta JSON se convierte recursivamente a `HashWithIndifferentAccess`, permitiendo acceso por symbol o string en cualquier nivel de anidamiento.
14
+
15
+ **Dynamic Accessor** — Cuando Docker devuelve un atributo nuevo (ej: `Spec`, `Status`), Base crea `attr_accessor` dinámicamente via `method_missing`. Se cachea en `defined_attributes` (Set) para no redefinir.
16
+
17
+ **Version.Index** — Mecanismo de Docker para updates atómicos. Updatable extrae `Version["Index"]` y lo envía como query param para evitar race conditions.
18
+
19
+ **LogHelper** — Módulo que formatea logs en KV (`key=value`) y enmascara campos sensibles matching `/password|token|api_key|auth|secret|data/i` → `[FILTERED]`.
20
+
21
+ ## Arquitectura
22
+
23
+ ### Responsabilidad core
24
+
25
+ ORM ligero compatible con ActiveModel para Docker Engine API. Comunica via Excon sobre Unix socket (default) o TCP. Todos los requests pasan por una cadena de middlewares.
26
+
27
+ ### Mapa de componentes
28
+
29
+ ```
30
+ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐
31
+ │ Modelo │────>│ Api │────>│ Connection │
32
+ │ (Service, │ │ (ENDPOINTS │ │ (Excon + │
33
+ │ Node...) │ │ + request) │ │ Timeouts) │
34
+ └──────────────┘ └──────────────┘ └──────────────┘
35
+
36
+
37
+ ┌───────────────────────┐
38
+ │ Middleware Stack │
39
+ │ RequestEncoder │
40
+ │ ResponseJSONParser │
41
+ │ ErrorHandler │
42
+ └───────────────────────┘
43
+ ```
44
+
45
+ ### Flujo en runtime
46
+
47
+ 1. Modelo invoca `Api.request(action:, arguments:, query_params:, payload:)`.
48
+ 2. Api formatea el path con `format()` y delega a `DockerSwarm.request`.
49
+ 3. Connection crea/reutiliza cliente Excon singleton con middlewares.
50
+ 4. RequestEncoder serializa body (JSON default, form-urlencoded, multipart).
51
+ 5. Excon envía al Docker daemon (socket o TCP).
52
+ 6. ResponseJSONParser parsea JSON y aplica `with_indifferent_access` recursivo.
53
+ 7. ErrorHandler mapea status 4xx/5xx a excepciones tipadas.
54
+ 8. Connection mide duración con `CLOCK_MONOTONIC` y loguea en KV.
55
+
56
+ ### Decisiones de diseño
57
+
58
+ - **PascalCase fiel**: Los atributos mantienen el naming de Docker (`Spec`, `TaskTemplate`, `ContainerSpec`). No se transforman a snake_case.
59
+ - **Spec merging**: `assign_attributes` hace `deep_merge` cuando el atributo es `Spec` para no perder campos anidados en updates.
60
+ - **Singleton connection**: `@client ||= Excon.new(...)` — se reutiliza, se resetea al cambiar configuración.
61
+ - **Retries en Excon**: `idempotent: true` + `retry_errors: [Socket, Timeout]` con `max_retries` configurable.
62
+
63
+ ## API Pública (resumen)
64
+
65
+ ### Configuración
66
+
67
+ ```ruby
68
+ DockerSwarm.configure do |config|
69
+ config.socket_path = "unix:///var/run/docker.sock" # o http://host:port
70
+ config.logger = Logger.new($stdout)
71
+ config.log_level = Logger::INFO
72
+ config.read_timeout = 60.0 # segundos
73
+ config.write_timeout = 60.0
74
+ config.connect_timeout = 10.0
75
+ config.max_retries = 3
76
+ end
77
+ ```
78
+
79
+ ### Operaciones comunes
80
+
81
+ ```ruby
82
+ # Listar
83
+ DockerSwarm::Service.all
84
+ DockerSwarm::Service.all(label: ["app=web"])
85
+
86
+ # Buscar
87
+ service = DockerSwarm::Service.find("service_id") # nil si no existe
88
+
89
+ # Crear
90
+ svc = DockerSwarm::Service.create(Name: "web", TaskTemplate: { ... })
91
+
92
+ # Actualizar (maneja Version.Index automáticamente)
93
+ service.update(Spec: { Replicas: 3 })
94
+
95
+ # Eliminar (graceful con 404)
96
+ service.destroy
97
+
98
+ # Logs
99
+ service.logs(stdout: 1, stderr: 1)
100
+
101
+ # Sistema
102
+ DockerSwarm::System.up # ping
103
+ DockerSwarm::System.info # daemon info
104
+ DockerSwarm::System.version # versión Docker
105
+ DockerSwarm::System.df # disk usage
106
+ DockerSwarm::Swarm.show # info del cluster
107
+ ```
108
+
109
+ Ver [API Detallada](references/api-detallada.md) para la referencia completa de todos los modelos.
110
+
111
+ ## FAQ
112
+
113
+ ### Como conecto a un Docker remoto por TCP?
114
+
115
+ ```ruby
116
+ DockerSwarm.configure do |config|
117
+ config.socket_path = "http://192.168.1.100:2375"
118
+ end
119
+ ```
120
+ Connection detecta si empieza con `unix://` (socket) o no (TCP) y configura Excon.
121
+
122
+ ### Por qué los atributos son PascalCase y no snake_case?
123
+
124
+ Docker Engine API usa PascalCase en todos sus JSON. La gema mantiene fidelidad 1:1 para evitar confusión al leer la documentación de Docker. Se accede igual: `service.Spec`, `node.Status`.
125
+
126
+ ### Como filtro recursos con labels?
127
+
128
+ ```ruby
129
+ DockerSwarm::Service.all(label: ["env=production", "app=web"])
130
+ DockerSwarm::Node.all(role: ["manager"])
131
+ ```
132
+ Los filtros se serializan como JSON en el query param `filters` del Docker API.
133
+
134
+ ### Como manejo errores de conexión?
135
+
136
+ ```ruby
137
+ begin
138
+ DockerSwarm::System.up
139
+ rescue DockerSwarm::Communication => e
140
+ # Socket caído o inalcanzable
141
+ end
142
+ ```
143
+ `Communication` envuelve errores de `Excon::Error::Socket`. Los retries se aplican automáticamente (`max_retries`).
144
+
145
+ ### Puedo usar validaciones de ActiveModel?
146
+
147
+ Sí. Todos los modelos heredan de `ActiveModel::Model`. Podés agregar validaciones custom:
148
+
149
+ ```ruby
150
+ service = DockerSwarm::Service.new(Name: "")
151
+ service.valid? # usa validaciones ActiveModel
152
+ service.save # retorna false si invalid?
153
+ ```
154
+
155
+ ## Antipatrones
156
+
157
+ ### Transformar atributos a snake_case
158
+
159
+ ```ruby
160
+ # MAL
161
+ service.task_template["container_spec"]
162
+
163
+ # BIEN
164
+ service.TaskTemplate["ContainerSpec"]
165
+ ```
166
+ **Razón:** Docker API usa PascalCase. La gema mantiene fidelidad. Con indifferent access, podés usar strings o symbols, pero siempre PascalCase.
167
+
168
+ ### Reemplazar Spec en vez de mergear
169
+
170
+ ```ruby
171
+ # MAL — pierde campos existentes del Spec
172
+ service.update(Spec: { Mode: { Replicated: { Replicas: 5 } } })
173
+
174
+ # BIEN — mergear solo lo que cambia
175
+ service.update(Mode: { Replicated: { Replicas: 5 } })
176
+ ```
177
+ **Razón:** `payload_for_docker` extrae contenido de Spec al root. Si pasás Spec completo, `assign_attributes` hace deep_merge, pero es más limpio pasar los campos directamente.
178
+
179
+ ### Ignorar Version.Index en updates
180
+
181
+ ```ruby
182
+ # MAL — update manual sin version
183
+ DockerSwarm.request(method: :post, path: "services/#{id}/update", body: payload)
184
+
185
+ # BIEN — usar el modelo, maneja version automáticamente
186
+ service.update(new_attrs)
187
+ ```
188
+ **Razón:** Docker requiere `version` query param para updates atómicos. Updatable lo extrae de `self.Version["Index"]` automáticamente.
189
+
190
+ ### No capturar errores específicos
191
+
192
+ ```ruby
193
+ # MAL
194
+ begin
195
+ service.destroy
196
+ rescue StandardError
197
+ # tragarse todo
198
+ end
199
+
200
+ # BIEN
201
+ begin
202
+ service.destroy
203
+ rescue DockerSwarm::NotFound
204
+ # ya fue eliminado, ok
205
+ rescue DockerSwarm::Conflict => e
206
+ # servicio en uso
207
+ end
208
+ ```
209
+ **Razón:** La jerarquía de errores mapea cada status HTTP. Capturar errores específicos permite manejar cada caso.
210
+
211
+ ## Errores
212
+
213
+ Los errores más comunes. Ver [Catálogo de Errores](references/errores.md) para la referencia completa.
214
+
215
+ | Excepción | Status | Causa típica |
216
+ |-----------|--------|--------------|
217
+ | `NotFound` | 404 | Recurso eliminado o ID incorrecto |
218
+ | `Conflict` | 409 | Nombre duplicado o recurso en uso |
219
+ | `Communication` | — | Socket caído o inalcanzable |
220
+ | `ServiceUnavailable` | 503 | Docker daemon reiniciando |
221
+
222
+ Todas heredan de `DockerSwarm::Error`. Se acceden como `DockerSwarm::NotFound` (alias) o `DockerSwarm::Error::NotFound`.
223
+
224
+ ## Referencias
225
+
226
+ - [API Detallada](references/api-detallada.md) — Referencia completa de modelos, concerns y métodos
227
+ - [Catálogo de Errores](references/errores.md) — Todas las excepciones con causa y resolución
@@ -0,0 +1,246 @@
1
+ # API Detallada
2
+
3
+ Referencia completa de modelos, concerns, middleware y métodos públicos de DockerSwarm.
4
+
5
+ ## Modelos — Tabla de capacidades
6
+
7
+ | Modelo | Creatable | Updatable | Deletable | Loggable | Extra |
8
+ |--------|-----------|-----------|-----------|----------|-------|
9
+ | Service | x | x | x | x | Ciclo de vida completo |
10
+ | Node | | x | x | | Miembros del cluster, no se crean |
11
+ | Task | | | | x | Read-only, generados por services |
12
+ | Container | | | x | x | `#start`, `#stop` |
13
+ | Network | x | x | x | | CRUD completo |
14
+ | Volume | x | | x | | `root_key = "Volumes"` |
15
+ | Config | x | | x | | Configuración del cluster |
16
+ | Secret | x | | x | | Datos sensibles |
17
+ | Image | x | | x | | Pull/removal |
18
+ | Swarm | | | | | `.show` (estático) |
19
+ | System | | | | | `.info`, `.version`, `.up`, `.df` |
20
+
21
+ ## Base — Métodos de clase
22
+
23
+ ```ruby
24
+ # Listar todos (con filtros opcionales)
25
+ # @param filters [Hash] Filtros Docker (label:, name:, id:, role:, etc.)
26
+ # @return [Array<Model>]
27
+ Model.all(filters = {})
28
+
29
+ # Alias de .all
30
+ Model.where(filters)
31
+
32
+ # Buscar por ID (retorna nil si 404)
33
+ # @param id [String]
34
+ # @return [Model, nil]
35
+ Model.find(id)
36
+
37
+ # Nombre del recurso pluralizado (ej: "services", "nodes")
38
+ Model.resource_name
39
+
40
+ # Endpoints del modelo desde Api::ENDPOINTS
41
+ Model.routes
42
+ ```
43
+
44
+ ### Filtros soportados
45
+
46
+ ```ruby
47
+ # Filtros Docker se serializan como JSON en query param `filters`
48
+ DockerSwarm::Service.all(label: ["app=web"], name: ["my-service"])
49
+ DockerSwarm::Container.all(status: ["running"])
50
+ DockerSwarm::Node.all(role: ["manager"])
51
+
52
+ # Parámetros globales (no van en filters)
53
+ DockerSwarm::Image.all(all: true) # incluir intermedias
54
+ DockerSwarm::Container.all(limit: 10) # limitar resultados
55
+ ```
56
+
57
+ ## Base — Métodos de instancia
58
+
59
+ ```ruby
60
+ # ID del recurso
61
+ # @return [String]
62
+ model.id
63
+
64
+ # Hash de atributos (excluye internos de ActiveModel)
65
+ # @return [Hash]
66
+ model.attributes
67
+
68
+ # Recarga desde Docker API
69
+ # @return [self]
70
+ model.reload
71
+
72
+ # Prepara payload para Docker (excluye ID, Version, CreatedAt, extrae Spec)
73
+ # @return [Hash]
74
+ model.payload_for_docker
75
+
76
+ # Persistido? (tiene ID)
77
+ # @return [Boolean]
78
+ model.persisted?
79
+
80
+ # Inspección legible: #<DockerSwarm::Service ID: abc, Name: web, Image: nginx>
81
+ model.inspect
82
+
83
+ # Serialización
84
+ model.as_json
85
+ model.serializable_hash
86
+ ```
87
+
88
+ ## Concern: Creatable
89
+
90
+ Incluido en: Service, Network, Volume, Config, Secret, Image.
91
+
92
+ ```ruby
93
+ # Crear y persistir
94
+ # @param attributes [Hash] Atributos PascalCase
95
+ # @return [Model] instancia (con ID si exitoso)
96
+ Model.create(attributes)
97
+
98
+ # Persistir instancia nueva (o delegar a update si persisted?)
99
+ # @return [Boolean] false si validación falla
100
+ model.save
101
+ ```
102
+
103
+ Flujo interno de `save`: `valid?` → `Api.request(:create, payload_for_docker)` → asigna ID de response → `reload`.
104
+
105
+ ## Concern: Updatable
106
+
107
+ Incluido en: Service, Node, Network.
108
+
109
+ ```ruby
110
+ # Actualizar recurso persistido
111
+ # @param new_attributes [Hash] Atributos a mergear
112
+ # @return [Boolean] false si validación falla
113
+ model.update(new_attributes = {})
114
+ ```
115
+
116
+ Flujo interno: `assign_attributes` (deep_merge en Spec) → `valid?` → `Api.request(:update, id:, version: Version["Index"], payload:)`.
117
+
118
+ **Importante:** El query param `version` se extrae automáticamente de `self.Version["Index"]`. Sin esto, Docker rechaza el update con 500.
119
+
120
+ ## Concern: Deletable
121
+
122
+ Incluido en: Service, Node, Container, Network, Volume, Config, Secret, Image.
123
+
124
+ ```ruby
125
+ # Eliminar por instancia
126
+ # @return [true, nil] nil si ya no existía (404 graceful)
127
+ model.destroy
128
+
129
+ # Eliminar por ID (class method)
130
+ # @return [true, nil]
131
+ Model.destroy(id)
132
+ ```
133
+
134
+ ## Concern: Loggable
135
+
136
+ Incluido en: Service, Task, Container.
137
+
138
+ ```ruby
139
+ # Obtener logs del recurso
140
+ # @param query_params [Hash] stdout:, stderr:, follow:, tail:, since:, timestamps:
141
+ # @return [String] Raw log stream
142
+ model.logs(query_params = { stdout: 1, stderr: 1 })
143
+ ```
144
+
145
+ ## Container — Métodos específicos
146
+
147
+ ```ruby
148
+ container = DockerSwarm::Container.find("container_id")
149
+
150
+ # Iniciar contenedor detenido
151
+ # @return [Boolean]
152
+ container.start
153
+
154
+ # Detener contenedor en ejecución
155
+ # @return [Boolean]
156
+ container.stop
157
+ ```
158
+
159
+ ## System — Métodos estáticos
160
+
161
+ ```ruby
162
+ # Ping al daemon
163
+ # @return [String] "OK"
164
+ DockerSwarm::System.up
165
+
166
+ # Información del daemon
167
+ # @return [Hash] (Containers, Images, Driver, MemoryLimit, etc.)
168
+ DockerSwarm::System.info
169
+
170
+ # Versión de Docker
171
+ # @return [Hash] (Version, ApiVersion, Os, Arch, etc.)
172
+ DockerSwarm::System.version
173
+
174
+ # Uso de disco
175
+ # @return [Hash] (LayersSize, Images, Containers, Volumes)
176
+ DockerSwarm::System.df
177
+ ```
178
+
179
+ ## Swarm — Método estático
180
+
181
+ ```ruby
182
+ # Información del cluster Swarm
183
+ # @return [Hash] (ID, Version, Spec, JoinTokens, etc.)
184
+ DockerSwarm::Swarm.show
185
+ ```
186
+
187
+ ## Volume — Particularidad
188
+
189
+ Volume sobreescribe `root_key` porque Docker envuelve la respuesta en `{"Volumes": [...]}`:
190
+
191
+ ```ruby
192
+ class Volume < Base
193
+ def self.root_key = "Volumes"
194
+ end
195
+ ```
196
+
197
+ ## Api — Bajo nivel
198
+
199
+ ```ruby
200
+ # Request directo al Docker API
201
+ # @param action [Hash] {method:, path:} desde ENDPOINTS
202
+ # @param arguments [Hash] Interpolación en path (id:)
203
+ # @param query_params [Hash] Query string
204
+ # @param payload [Hash, nil] Body del request
205
+ DockerSwarm::Api.request(action:, arguments: {}, query_params: {}, payload: nil)
206
+ ```
207
+
208
+ ### Endpoints registrados
209
+
210
+ Todos definidos en `Api::ENDPOINTS` como Hash frozen:
211
+
212
+ | Recurso | Acciones |
213
+ |---------|----------|
214
+ | swarm | show |
215
+ | system | info, version, up, df |
216
+ | nodes | index, show, update, destroy |
217
+ | tasks | index, show, logs |
218
+ | services | index, show, create, update, destroy, logs |
219
+ | configs | index, show, create, destroy |
220
+ | secrets | index, show, create, destroy |
221
+ | networks | index, show, create, update, destroy |
222
+ | volumes | index, show, create, destroy |
223
+ | containers | index, show, create, start, stop, destroy, logs |
224
+ | images | index, show, create, destroy |
225
+
226
+ ## Middleware Stack
227
+
228
+ Orden de ejecución en Excon:
229
+
230
+ 1. **Excon defaults** (Retry, Instrumentor, etc.)
231
+ 2. **Excon::Middleware::RedirectFollower**
232
+ 3. **RequestEncoder** — Serializa body: JSON (default), form-urlencoded, multipart. Detecta por Content-Type header.
233
+ 4. **ResponseJSONParser** — Parsea JSON si Content-Type incluye `application/json`. Aplica `with_indifferent_access` recursivo (Hash y Array de Hashes).
234
+ 5. **ErrorHandler** — Status 4xx/5xx → excepción tipada. Loguea `business_error` antes de raise.
235
+
236
+ ## Configuración
237
+
238
+ | Opción | Tipo | Default | Descripción |
239
+ |--------|------|---------|-------------|
240
+ | `socket_path` | String | `unix:///var/run/docker.sock` | Socket Unix o URL TCP |
241
+ | `logger` | Logger | `Logger.new($stdout)` | Logger para KV output |
242
+ | `log_level` | Integer | `Logger::INFO` | Nivel de log (se aplica al logger) |
243
+ | `read_timeout` | Float | `60.0` | Timeout lectura (segundos) |
244
+ | `write_timeout` | Float | `60.0` | Timeout escritura (segundos) |
245
+ | `connect_timeout` | Float | `10.0` | Timeout conexión (segundos) |
246
+ | `max_retries` | Integer | `3` | Reintentos en Socket/Timeout errors |
@@ -0,0 +1,157 @@
1
+ # Catálogo de Errores
2
+
3
+ Referencia completa de excepciones de DockerSwarm. Todas heredan de `DockerSwarm::Error`.
4
+
5
+ ## Jerarquía
6
+
7
+ ```
8
+ DockerSwarm::Error (base)
9
+ ├── BadRequest (400)
10
+ ├── Unauthorized (401)
11
+ ├── Forbidden (403)
12
+ ├── NotFound (404)
13
+ ├── NotAcceptable (406)
14
+ ├── RequestTimeout (408)
15
+ ├── Conflict (409)
16
+ ├── UnprocessableEntity (422)
17
+ ├── TooManyRequests (429)
18
+ ├── InternalServerError (500)
19
+ ├── BadGateway (502)
20
+ ├── ServiceUnavailable (503)
21
+ ├── GatewayTimeout (504)
22
+ └── Communication (socket/red)
23
+ ```
24
+
25
+ ## Acceso
26
+
27
+ Cada excepción tiene 3 formas de acceso equivalentes:
28
+
29
+ ```ruby
30
+ DockerSwarm::NotFound # alias directo (recomendado)
31
+ DockerSwarm::Error::NotFound # acceso via clase Error
32
+ DockerSwarm::Errors::NotFound # módulo Errors (const_missing dinámico)
33
+ ```
34
+
35
+ ## Catálogo completo
36
+
37
+ ### BadRequest (400)
38
+
39
+ **Causa:** Payload malformado o parámetros inválidos.
40
+ **Reproducción:** Enviar JSON con campos incorrectos (ej: `Replicas: "abc"` en vez de integer).
41
+ **Resolución:** Validar payload contra la documentación de Docker API. Verificar tipos de datos.
42
+
43
+ ### Unauthorized (401)
44
+
45
+ **Causa:** Credenciales inválidas o ausentes para Docker daemon con TLS.
46
+ **Reproducción:** Conectar a daemon protegido sin certificados.
47
+ **Resolución:** Configurar TLS client certificates en Excon o en la URL de conexión.
48
+
49
+ ### Forbidden (403)
50
+
51
+ **Causa:** Permisos insuficientes para la operación.
52
+ **Reproducción:** Intentar operación de swarm en un nodo worker.
53
+ **Resolución:** Verificar que el nodo es manager y que el usuario tiene permisos sobre el socket.
54
+
55
+ ### NotFound (404)
56
+
57
+ **Causa:** Recurso no existe o fue eliminado.
58
+ **Reproducción:** `Service.find("id_inexistente")`.
59
+ **Resolución:** `Base.find` retorna `nil` automáticamente. `Deletable#destroy` también es graceful (retorna `nil` en 404). No requiere rescue manual en estos casos.
60
+
61
+ ### NotAcceptable (406)
62
+
63
+ **Causa:** El servidor no puede producir una respuesta aceptable.
64
+ **Reproducción:** Raro en Docker API.
65
+ **Resolución:** Verificar headers Accept del request.
66
+
67
+ ### RequestTimeout (408)
68
+
69
+ **Causa:** Docker daemon tardó demasiado en responder.
70
+ **Reproducción:** Operación en un cluster sobrecargado.
71
+ **Resolución:** Incrementar `read_timeout` en configuración. Verificar estado del cluster.
72
+
73
+ ### Conflict (409)
74
+
75
+ **Causa:** Nombre duplicado, recurso en uso, o versión desactualizada.
76
+ **Reproducción:** `Service.create(Name: "nombre_existente")` o update con `Version.Index` stale.
77
+ **Resolución:** Para nombres: verificar existencia antes de crear. Para versiones: hacer `reload` y reintentar el update.
78
+
79
+ ### UnprocessableEntity (422)
80
+
81
+ **Causa:** Payload semánticamente inválido.
82
+ **Reproducción:** Crear servicio con imagen inexistente y restricciones de scheduling imposibles.
83
+ **Resolución:** Verificar que la imagen existe y que las constraints del servicio son alcanzables.
84
+
85
+ ### TooManyRequests (429)
86
+
87
+ **Causa:** Rate limiting del Docker daemon o registry.
88
+ **Reproducción:** Burst de requests al API.
89
+ **Resolución:** Implementar backoff. Los retries de Excon cubren errores de socket/timeout pero no 429.
90
+
91
+ ### InternalServerError (500)
92
+
93
+ **Causa:** Error interno del Docker daemon.
94
+ **Reproducción:** Bug en Docker, operación sobre estado inconsistente, o update sin `version` query param.
95
+ **Resolución:** Verificar logs del Docker daemon (`journalctl -u docker`). Si es por version faltante, usar el modelo (Updatable lo maneja).
96
+
97
+ ### BadGateway (502)
98
+
99
+ **Causa:** Proxy o load balancer entre cliente y daemon devuelve error.
100
+ **Reproducción:** Docker daemon detrás de reverse proxy caído.
101
+ **Resolución:** Verificar infraestructura de red y proxy.
102
+
103
+ ### ServiceUnavailable (503)
104
+
105
+ **Causa:** Docker daemon reiniciando o en mantenimiento.
106
+ **Reproducción:** Request durante restart del servicio Docker.
107
+ **Resolución:** Reintentar después de esperar. `max_retries` cubre errores de socket pero no 503.
108
+
109
+ ### GatewayTimeout (504)
110
+
111
+ **Causa:** Proxy entre cliente y daemon timeout.
112
+ **Reproducción:** Operación larga detrás de proxy con timeout corto.
113
+ **Resolución:** Incrementar timeout del proxy. Verificar que `read_timeout` de la gema es menor que el del proxy.
114
+
115
+ ### Communication
116
+
117
+ **Causa:** Error de socket o red — daemon caído, socket inexistente, permisos insuficientes.
118
+ **Reproducción:** `DockerSwarm::System.up` con daemon apagado.
119
+ **Resolución:** Verificar que Docker está corriendo (`systemctl status docker`), que el socket existe y que el usuario tiene permisos de lectura.
120
+
121
+ ## Manejo recomendado
122
+
123
+ ```ruby
124
+ begin
125
+ DockerSwarm::Service.create(Name: "web", TaskTemplate: { ... })
126
+ rescue DockerSwarm::Conflict => e
127
+ # Nombre duplicado — buscar existente
128
+ existing = DockerSwarm::Service.all(name: ["web"]).first
129
+ rescue DockerSwarm::Communication => e
130
+ # Docker caído
131
+ logger.error("Docker unreachable: #{e.message}")
132
+ rescue DockerSwarm::Error => e
133
+ # Cualquier otro error de Docker
134
+ logger.error("Docker error: #{e.class} #{e.message}")
135
+ end
136
+ ```
137
+
138
+ ## Logging automático
139
+
140
+ El middleware ErrorHandler loguea automáticamente antes de raise:
141
+
142
+ ```
143
+ component=docker_swarm.middleware.error_handler event=business_error source=http status=409 message="name conflicts" method=post path=/services/create
144
+ ```
145
+
146
+ Formato KV con masking de datos sensibles via LogHelper.
147
+
148
+ ## Causa original (Excon wrapping)
149
+
150
+ Excon puede envolver excepciones del middleware en `Excon::Error::Socket`. Connection detecta esto y re-raise la excepción original:
151
+
152
+ ```ruby
153
+ # Connection#request internamente:
154
+ actual_error = e.cause&.class&.name&.include?("DockerSwarm::Error") ? e.cause : e
155
+ ```
156
+
157
+ La excepción original mantiene la causa via `Exception#cause` de Ruby para trazabilidad completa.
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: docker-swarm
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Gabriel
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-03-31 00:00:00.000000000 Z
11
+ date: 2026-04-04 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -117,6 +117,9 @@ files:
117
117
  - lib/docker_swarm/models/task.rb
118
118
  - lib/docker_swarm/models/volume.rb
119
119
  - lib/docker_swarm/version.rb
120
+ - skill/SKILL.md
121
+ - skill/references/api-detallada.md
122
+ - skill/references/errores.md
120
123
  homepage: https://github.com/wispro/docker-swarm
121
124
  licenses:
122
125
  - MIT