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 +4 -4
- data/CHANGELOG.md +121 -0
- data/LICENSE +21 -0
- data/README.md +32 -105
- data/docs/behavior/behavior.md +243 -0
- data/docs/glossary/glossary.md +141 -0
- data/lib/docker_swarm/base.rb +11 -19
- data/lib/docker_swarm/connection.rb +14 -7
- data/lib/docker_swarm/log_helper.rb +3 -1
- data/lib/docker_swarm/models/service.rb +9 -0
- data/lib/docker_swarm/version.rb +1 -1
- data/skill/SKILL.md +130 -183
- metadata +15 -12
- data/skill/references/api-detallada.md +0 -246
- data/skill/references/errores.md +0 -157
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# Glosario — docker-swarm
|
|
2
|
+
|
|
3
|
+
> meta: artefacto · RFC-009 · generado dev-enrich · anclado a `a4e3129` · cobertura: completo inicial (primitivas Docker + arquitectura interna); no se acrecienta sin tocar el flujo/concepto
|
|
4
|
+
|
|
5
|
+
## 1. Resumen
|
|
6
|
+
|
|
7
|
+
Términos de negocio que la gema `docker-swarm` materializa. Dos grupos: **primitivas Docker** (entidades del Engine API expuestas como modelos Ruby) y **conceptos arquitectónicos internos** (mecanismos que sostienen el ORM). Sólo términos de negocio con binding estable. Definiciones técnicas de columnas/atributos no van acá (sería Data Dictionary; n/a en gema sin DB).
|
|
8
|
+
|
|
9
|
+
## 2. Términos
|
|
10
|
+
|
|
11
|
+
### Service
|
|
12
|
+
|
|
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
|
+
**Binding:** [`DockerSwarm::Service`](../../lib/docker_swarm/models/service.rb)
|
|
15
|
+
|
|
16
|
+
### Node
|
|
17
|
+
|
|
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
|
+
**Binding:** [`DockerSwarm::Node`](../../lib/docker_swarm/models/node.rb)
|
|
20
|
+
|
|
21
|
+
### Task
|
|
22
|
+
|
|
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
|
+
**Binding:** [`DockerSwarm::Task`](../../lib/docker_swarm/models/task.rb)
|
|
25
|
+
|
|
26
|
+
### Container
|
|
27
|
+
|
|
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
|
+
**Binding:** [`DockerSwarm::Container`](../../lib/docker_swarm/models/container.rb)
|
|
30
|
+
|
|
31
|
+
### Image
|
|
32
|
+
|
|
33
|
+
Imagen Docker (registry o local). La gema cubre listar, pull (`create`) y destroy. Build local no cubierto (no caso de uso de orquestación).
|
|
34
|
+
**Binding:** [`DockerSwarm::Image`](../../lib/docker_swarm/models/image.rb)
|
|
35
|
+
|
|
36
|
+
### Network
|
|
37
|
+
|
|
38
|
+
Red Docker (overlay para Swarm, bridge para Container). CRUD completo. Update soporta conectar/desconectar containers.
|
|
39
|
+
**Binding:** [`DockerSwarm::Network`](../../lib/docker_swarm/models/network.rb)
|
|
40
|
+
|
|
41
|
+
### Volume
|
|
42
|
+
|
|
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
|
+
**Binding:** [`DockerSwarm::Volume`](../../lib/docker_swarm/models/volume.rb)
|
|
45
|
+
|
|
46
|
+
### Config
|
|
47
|
+
|
|
48
|
+
Configuración inmutable distribuida en el cluster (archivos de configuración, manifests). CRUD sin update — Docker requiere recrear. Sólo Swarm.
|
|
49
|
+
**Binding:** [`DockerSwarm::Config`](../../lib/docker_swarm/models/config.rb)
|
|
50
|
+
|
|
51
|
+
### Secret
|
|
52
|
+
|
|
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
|
+
**Binding:** [`DockerSwarm::Secret`](../../lib/docker_swarm/models/secret.rb)
|
|
55
|
+
|
|
56
|
+
### Swarm
|
|
57
|
+
|
|
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
|
+
**Binding:** [`DockerSwarm::Swarm`](../../lib/docker_swarm/models/swarm.rb)
|
|
60
|
+
|
|
61
|
+
### System
|
|
62
|
+
|
|
63
|
+
Daemon Docker como entidad singleton. Métodos estáticos: `info`, `version`, `up` (ping), `df` (disk usage). Útil para health checks y observabilidad.
|
|
64
|
+
**Binding:** [`DockerSwarm::System`](../../lib/docker_swarm/models/system.rb)
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
### Base (ORM base)
|
|
69
|
+
|
|
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
|
+
**Binding:** [`DockerSwarm::Base`](../../lib/docker_swarm/base.rb)
|
|
72
|
+
|
|
73
|
+
### Concern
|
|
74
|
+
|
|
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
|
+
|
|
77
|
+
| Concern | Símbolo | Aplica a |
|
|
78
|
+
|---|---|---|
|
|
79
|
+
| Creatable | [`DockerSwarm::Concerns::Creatable`](../../lib/docker_swarm/concerns/creatable.rb) | Service, Network, Volume, Config, Secret, Image |
|
|
80
|
+
| Updatable | [`DockerSwarm::Concerns::Updatable`](../../lib/docker_swarm/concerns/updatable.rb) | Service, Node, Network |
|
|
81
|
+
| Deletable | [`DockerSwarm::Concerns::Deletable`](../../lib/docker_swarm/concerns/deletable.rb) | Service, Node, Container, Network, Volume, Config, Secret, Image |
|
|
82
|
+
| Loggable | [`DockerSwarm::Concerns::Loggable`](../../lib/docker_swarm/concerns/loggable.rb) | Service, Task, Container |
|
|
83
|
+
| Inspectable | [`DockerSwarm::Concerns::Inspectable`](../../lib/docker_swarm/concerns/inspectable.rb) | Todos (vía Base) |
|
|
84
|
+
|
|
85
|
+
### Middleware
|
|
86
|
+
|
|
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
|
+
|
|
89
|
+
| Middleware | Símbolo | Rol |
|
|
90
|
+
|---|---|---|
|
|
91
|
+
| RequestEncoder | [`DockerSwarm::Middleware::RequestEncoder`](../../lib/docker_swarm/middleware/request_encoder.rb) | Serializa body (JSON / x-www-form / multipart) según Content-Type |
|
|
92
|
+
| ResponseJSONParser | [`DockerSwarm::Middleware::ResponseJSONParser`](../../lib/docker_swarm/middleware/response_json_parser.rb) | Parsea JSON y aplica `with_indifferent_access` |
|
|
93
|
+
| ErrorHandler | [`DockerSwarm::Middleware::ErrorHandler`](../../lib/docker_swarm/middleware/error_handler.rb) | Mapea 4xx/5xx → `DockerSwarm::Error::*` + log `business_error` |
|
|
94
|
+
|
|
95
|
+
### Connection
|
|
96
|
+
|
|
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
|
+
**Binding:** [`DockerSwarm::Connection`](../../lib/docker_swarm/connection.rb)
|
|
99
|
+
|
|
100
|
+
### Dynamic Accessor
|
|
101
|
+
|
|
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
|
+
**Binding:** [`DockerSwarm::Base#method_missing`](../../lib/docker_swarm/base.rb), [`DockerSwarm::Base.defined_attributes`](../../lib/docker_swarm/base.rb)
|
|
104
|
+
|
|
105
|
+
### Spec deep_merge
|
|
106
|
+
|
|
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
|
+
**Binding:** [`DockerSwarm::Base#assign_attributes`](../../lib/docker_swarm/base.rb)
|
|
109
|
+
|
|
110
|
+
### Version.Index
|
|
111
|
+
|
|
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
|
+
**Binding:** [`DockerSwarm::Concerns::Updatable#update`](../../lib/docker_swarm/concerns/updatable.rb)
|
|
114
|
+
|
|
115
|
+
### LogHelper
|
|
116
|
+
|
|
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
|
+
**Binding:** [`DockerSwarm::LogHelper`](../../lib/docker_swarm/log_helper.rb)
|
|
119
|
+
|
|
120
|
+
### payload_for_docker
|
|
121
|
+
|
|
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
|
+
**Binding:** [`DockerSwarm::Base#payload_for_docker`](../../lib/docker_swarm/base.rb)
|
|
124
|
+
|
|
125
|
+
## 3. Inferencias
|
|
126
|
+
|
|
127
|
+
| Término | Inferencia | Confidence | Verificar |
|
|
128
|
+
|---|---|---|---|
|
|
129
|
+
| Container | "creación intencionalmente fuera de scope F1" | inferred | ¿se quiere documentar como decisión explícita o como gap a cubrir? |
|
|
130
|
+
| Spec deep_merge | "razón: updates parciales no pierden campos" | declared | confirmado en CLAUDE.md decisión arquitectura |
|
|
131
|
+
| Dynamic Accessor | "Docker evoluciona y agrega campos" | declared | confirmado en CLAUDE.md decisión arquitectura |
|
|
132
|
+
|
|
133
|
+
## 4. Cobertura y fronteras
|
|
134
|
+
|
|
135
|
+
- **Cobertura:** Completa para primitivas Docker (11) y conceptos arquitectónicos internos (9). Esta es una corrida inicial (sembrado autorizado, RFC-009).
|
|
136
|
+
- **Frontera con Data Dictionary (RFC-002 §2.c r4):** la gema **no tiene capa de datos** (no DB, no `docs/data/`). Definiciones de columnas/atributos = `n/a`. Atributos PascalCase de Docker (`Spec`, `TaskTemplate`, etc.) son contrato de la API Docker, no del dominio de esta gema — no entran al glosario.
|
|
137
|
+
- **Frontera con comportamiento:** flujos (create+reload, retry, error-mapping) viven en [`docs/behavior/behavior.md`](../behavior/behavior.md), no acá.
|
|
138
|
+
- **Fuera de alcance:**
|
|
139
|
+
- Términos técnicos puros sin significado de negocio (ej: `instance_values`, `attr_accessor`) — son detalles de implementación, no contrato.
|
|
140
|
+
- Glossary del Docker Engine API (cómo funciona internamente Swarm, raft, gossip) — vive en docs de Docker, no se duplica acá.
|
|
141
|
+
- **Cadencia:** incremental por PR a partir de acá; ausencia ≠ inexistencia (RFC-009).
|
data/lib/docker_swarm/base.rb
CHANGED
|
@@ -75,31 +75,23 @@ module DockerSwarm
|
|
|
75
75
|
|
|
76
76
|
def assign_attributes(new_attributes)
|
|
77
77
|
return if new_attributes.blank?
|
|
78
|
+
raise ArgumentError, "assign_attributes expects a Hash, got #{new_attributes.class}" unless new_attributes.is_a?(Hash)
|
|
78
79
|
|
|
79
|
-
attributes_to_assign = new_attributes.deep_dup
|
|
80
|
+
attributes_to_assign = new_attributes.deep_dup.with_indifferent_access
|
|
81
|
+
normalized_attributes = {}
|
|
80
82
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
83
|
+
attributes_to_assign.each do |key, value|
|
|
84
|
+
normalized_key = key.to_s == "Id" ? "ID" : key.to_s
|
|
85
|
+
_define_dynamic_accessor(normalized_key)
|
|
84
86
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
_define_dynamic_accessor(normalized_key)
|
|
88
|
-
|
|
89
|
-
if normalized_key == "Spec" && respond_to?(:Spec) && self.Spec.is_a?(Hash) && value.is_a?(Hash)
|
|
90
|
-
value = self.Spec.deep_merge(value)
|
|
91
|
-
end
|
|
92
|
-
|
|
93
|
-
normalized_attributes[normalized_key] = value
|
|
87
|
+
if normalized_key == "Spec" && respond_to?(:Spec) && self.Spec.is_a?(Hash) && value.is_a?(Hash)
|
|
88
|
+
value = self.Spec.deep_merge(value)
|
|
94
89
|
end
|
|
95
90
|
|
|
96
|
-
|
|
97
|
-
else
|
|
98
|
-
Array(attributes_to_assign).each do |item|
|
|
99
|
-
next unless item.is_a?(Hash)
|
|
100
|
-
item.each_key { |key| _define_dynamic_accessor(key) }
|
|
101
|
-
end
|
|
91
|
+
normalized_attributes[normalized_key] = value
|
|
102
92
|
end
|
|
93
|
+
|
|
94
|
+
super(normalized_attributes)
|
|
103
95
|
end
|
|
104
96
|
|
|
105
97
|
def attributes
|
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
module DockerSwarm
|
|
4
4
|
class Connection
|
|
5
|
+
# HTTP methods safe to retry automatically. POST/PATCH are excluded because
|
|
6
|
+
# replaying after a partial failure can create duplicate resources.
|
|
7
|
+
IDEMPOTENT_METHODS = %i[get head put delete options].freeze
|
|
8
|
+
|
|
5
9
|
attr_reader :socket_path, :logger
|
|
6
10
|
|
|
7
11
|
def initialize(socket_path, logger)
|
|
@@ -16,14 +20,16 @@ module DockerSwarm
|
|
|
16
20
|
level: :debug,
|
|
17
21
|
data: options.merge(path: options[:path]))
|
|
18
22
|
|
|
19
|
-
|
|
23
|
+
method = options[:method].to_s.downcase.to_sym
|
|
24
|
+
idempotent = IDEMPOTENT_METHODS.include?(method)
|
|
25
|
+
|
|
20
26
|
request_options = {
|
|
21
|
-
idempotent:
|
|
27
|
+
idempotent: idempotent,
|
|
22
28
|
retry_errors: [ Excon::Error::Socket, Excon::Error::Timeout ],
|
|
23
29
|
read_timeout: DockerSwarm.configuration.read_timeout.to_f,
|
|
24
30
|
write_timeout: DockerSwarm.configuration.write_timeout.to_f,
|
|
25
31
|
connect_timeout: DockerSwarm.configuration.connect_timeout.to_f,
|
|
26
|
-
retries: DockerSwarm.configuration.max_retries.to_i
|
|
32
|
+
retries: idempotent ? DockerSwarm.configuration.max_retries.to_i : 0
|
|
27
33
|
}.merge(options)
|
|
28
34
|
|
|
29
35
|
response = client.request(request_options)
|
|
@@ -37,8 +43,8 @@ module DockerSwarm
|
|
|
37
43
|
response.body
|
|
38
44
|
rescue => e
|
|
39
45
|
# Excon suele envolver excepciones de middleware en Excon::Error::Socket.
|
|
40
|
-
#
|
|
41
|
-
actual_error =
|
|
46
|
+
# Recuperamos la causa original si es una excepción de DockerSwarm.
|
|
47
|
+
actual_error = e.cause.is_a?(::DockerSwarm::Error) ? e.cause : e
|
|
42
48
|
|
|
43
49
|
log_event("request_failure",
|
|
44
50
|
level: :error,
|
|
@@ -48,9 +54,10 @@ module DockerSwarm
|
|
|
48
54
|
duration_s: calculate_duration(start_time)
|
|
49
55
|
))
|
|
50
56
|
|
|
51
|
-
|
|
57
|
+
case actual_error
|
|
58
|
+
when ::DockerSwarm::Error
|
|
52
59
|
raise actual_error
|
|
53
|
-
|
|
60
|
+
when Excon::Error::Socket
|
|
54
61
|
raise ::DockerSwarm::Error::Communication, "Docker socket error: #{actual_error.message}"
|
|
55
62
|
else
|
|
56
63
|
raise actual_error
|
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
module DockerSwarm
|
|
4
4
|
# Helper module to centralize logging logic and formatting
|
|
5
5
|
module LogHelper
|
|
6
|
-
|
|
6
|
+
# `data` se matchea con \b para que `Data` (Secret/Config) se filtre
|
|
7
|
+
# pero `metadata` u otras claves no caigan en falso positivo.
|
|
8
|
+
SENSITIVE_KEYS = /password|pass|passwd|secret|token|api_key|auth|\bdata\b/i.freeze
|
|
7
9
|
|
|
8
10
|
# Formats a hash into a KV structured string with sensitive data masking
|
|
9
11
|
# @param payload [Hash] The data to format
|
|
@@ -8,5 +8,14 @@ module DockerSwarm
|
|
|
8
8
|
include Concerns::Updatable
|
|
9
9
|
include Concerns::Deletable
|
|
10
10
|
include Concerns::Loggable
|
|
11
|
+
|
|
12
|
+
# Restarts the service by incrementing ForceUpdate, which causes
|
|
13
|
+
# Docker to recreate all tasks.
|
|
14
|
+
#
|
|
15
|
+
# @return [Boolean] true if the restart was triggered successfully
|
|
16
|
+
def restart
|
|
17
|
+
current = self.Spec&.dig("TaskTemplate", "ForceUpdate").to_i
|
|
18
|
+
update(Spec: { TaskTemplate: { ForceUpdate: current + 1 } })
|
|
19
|
+
end
|
|
11
20
|
end
|
|
12
21
|
end
|
data/lib/docker_swarm/version.rb
CHANGED
data/skill/SKILL.md
CHANGED
|
@@ -1,227 +1,174 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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)
|
|
1
|
+
---
|
|
2
|
+
name: docker-swarm
|
|
3
|
+
description: >-
|
|
4
|
+
ORM y cliente API Ruby para Docker Swarm. Expone las primitivas del Docker
|
|
5
|
+
Engine (Service, Node, Task, Container, Network, Volume, Config, Secret,
|
|
6
|
+
Swarm, System, Image) como modelos ActiveModel con CRUD, control de
|
|
7
|
+
concurrencia optimista (Version.Index), retries seguros por método HTTP,
|
|
8
|
+
logging KV con masking de secrets y jerarquía de errores tipada. ACTIVAR
|
|
9
|
+
cuando el caller necesita orquestar Docker desde Ruby — listar/crear/
|
|
10
|
+
actualizar/eliminar recursos del cluster, leer logs de services/tasks/
|
|
11
|
+
containers, hacer health-check del daemon (System.up/info/df), filtrar por
|
|
12
|
+
labels, o capturar errores tipados de Docker (Conflict/NotFound/
|
|
13
|
+
Communication). NO activar para builds de imágenes (no implementado), pull
|
|
14
|
+
con auth de registry privado (no implementado), o flujos que no son
|
|
15
|
+
Swarm (Docker Compose, raw containers).
|
|
16
|
+
triggers:
|
|
17
|
+
- "DockerSwarm::"
|
|
18
|
+
- "docker-swarm gem"
|
|
19
|
+
- "Docker Engine API desde Ruby"
|
|
20
|
+
- "Service.create / Service.update / Service.restart"
|
|
21
|
+
- "Container.start / Container.stop"
|
|
22
|
+
- "logs de un servicio Docker"
|
|
23
|
+
- "Version.Index"
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
# docker-swarm — Skill
|
|
27
|
+
|
|
28
|
+
## Qué es / cuándo usar
|
|
29
|
+
|
|
30
|
+
Gema Ruby que provee ORM `ActiveModel`-compatible sobre Docker Engine API. Cliente HTTP `Excon` directo (Unix socket o TCP). Usá esta dep para orquestación programática de Docker Swarm: lifecycle de recursos, logs, health checks y observabilidad.
|
|
31
|
+
|
|
32
|
+
No es:
|
|
33
|
+
- Build/compose tool — no construye imágenes, no parsea `docker-compose.yml`.
|
|
34
|
+
- Driver Kubernetes — sólo Swarm.
|
|
35
|
+
|
|
36
|
+
## Contrato resumido (piso mínimo)
|
|
64
37
|
|
|
65
38
|
### Configuración
|
|
66
39
|
|
|
67
40
|
```ruby
|
|
68
41
|
DockerSwarm.configure do |config|
|
|
69
|
-
config.socket_path = "unix:///var/run/docker.sock" # o http://host:
|
|
42
|
+
config.socket_path = "unix:///var/run/docker.sock" # o "http://host:2375"
|
|
70
43
|
config.logger = Logger.new($stdout)
|
|
71
44
|
config.log_level = Logger::INFO
|
|
72
|
-
config.read_timeout = 60.0
|
|
45
|
+
config.read_timeout = 60.0
|
|
73
46
|
config.write_timeout = 60.0
|
|
74
47
|
config.connect_timeout = 10.0
|
|
75
48
|
config.max_retries = 3
|
|
76
49
|
end
|
|
77
50
|
```
|
|
78
51
|
|
|
79
|
-
|
|
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.
|
|
52
|
+
Defaults son razonables: en local sin TLS, no necesitás bloque `configure`.
|
|
110
53
|
|
|
111
|
-
|
|
54
|
+
### Símbolos públicos por modelo
|
|
112
55
|
|
|
113
|
-
|
|
56
|
+
| Modelo | Class methods | Instance methods | Notas |
|
|
57
|
+
|---|---|---|---|
|
|
58
|
+
| `Service` | `all(filters)`, `find(id)`, `where(filters)`, `create(attrs)` | `update(attrs)`, `restart`, `destroy`, `logs(query)`, `reload`, `persisted?`, `id` | CRUD completo + force-recreate de tasks |
|
|
59
|
+
| `Node` | `all(filters)`, `find(id)`, `where(filters)` | `update(attrs)`, `destroy` | No `create` (los nodos se unen fuera de la gema) |
|
|
60
|
+
| `Task` | `all(filters)`, `find(id)`, `where(filters)` | `logs(query)`, `reload` | Read-only (generados por orquestador) |
|
|
61
|
+
| `Container` | `all(filters)`, `find(id)`, `where(filters)` | `start`, `stop`, `destroy`, `logs(query)` | **No `create`** (gap conocido, fuera F1) |
|
|
62
|
+
| `Image` | `all(filters)`, `find(id)`, `create(attrs)` | `destroy` | `create` = pull. **No soporta `X-Registry-Auth`** (registries privados con auth no funcionan) |
|
|
63
|
+
| `Network` | `all(filters)`, `find(id)`, `create(attrs)` | `update(attrs)`, `destroy` | CRUD completo |
|
|
64
|
+
| `Volume` | `all(filters)`, `find(id)`, `create(attrs)` | `destroy` | No `update` (Docker no lo soporta). Respuesta wrapped vía `root_key = "Volumes"` |
|
|
65
|
+
| `Config` | `all(filters)`, `find(id)`, `create(attrs)` | `destroy` | No `update` — recrear |
|
|
66
|
+
| `Secret` | `all(filters)`, `find(id)`, `create(attrs)` | `destroy` | No `update`. `Data` se filtra en logs (LogHelper) |
|
|
67
|
+
| `Swarm` | `.show` | — | Singleton, sólo info del cluster |
|
|
68
|
+
| `System` | `.info`, `.version`, `.up`, `.df` | — | Singleton, health/observabilidad |
|
|
114
69
|
|
|
115
|
-
|
|
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.
|
|
70
|
+
Detalle por símbolo en [`docs/glossary/glossary.md`](docs/glossary/glossary.md). Secuencias de operación en [`docs/behavior/behavior.md`](docs/behavior/behavior.md).
|
|
121
71
|
|
|
122
|
-
###
|
|
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?
|
|
72
|
+
### Operaciones típicas
|
|
127
73
|
|
|
128
74
|
```ruby
|
|
129
|
-
|
|
75
|
+
# Listar con filtros (serializa como JSON en query param `filters`)
|
|
76
|
+
DockerSwarm::Service.all(label: ["env=production"])
|
|
130
77
|
DockerSwarm::Node.all(role: ["manager"])
|
|
131
|
-
|
|
132
|
-
Los filtros se serializan como JSON en el query param `filters` del Docker API.
|
|
78
|
+
DockerSwarm::Container.all(status: ["running"])
|
|
133
79
|
|
|
134
|
-
|
|
80
|
+
# Lookup graceful (nil si 404)
|
|
81
|
+
service = DockerSwarm::Service.find("svc-id") # => Service | nil
|
|
135
82
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
end
|
|
142
|
-
```
|
|
143
|
-
`Communication` envuelve errores de `Excon::Error::Socket`. Los retries se aplican automáticamente (`max_retries`).
|
|
83
|
+
# Crear (auto-reload tras POST para hidratar Spec/Version completos)
|
|
84
|
+
svc = DockerSwarm::Service.create(
|
|
85
|
+
Name: "web",
|
|
86
|
+
TaskTemplate: { ContainerSpec: { Image: "nginx:latest" } }
|
|
87
|
+
)
|
|
144
88
|
|
|
145
|
-
|
|
89
|
+
# Update atómico (Version.Index extraído automáticamente)
|
|
90
|
+
service.update(Mode: { Replicated: { Replicas: 3 } })
|
|
146
91
|
|
|
147
|
-
|
|
92
|
+
# Force-recreate de tasks (equivalente a `docker service update --force`)
|
|
93
|
+
service.restart
|
|
148
94
|
|
|
149
|
-
|
|
150
|
-
service
|
|
151
|
-
service.valid? # usa validaciones ActiveModel
|
|
152
|
-
service.save # retorna false si invalid?
|
|
153
|
-
```
|
|
154
|
-
|
|
155
|
-
## Antipatrones
|
|
156
|
-
|
|
157
|
-
### Transformar atributos a snake_case
|
|
95
|
+
# Destroy graceful (nil si 404)
|
|
96
|
+
service.destroy
|
|
158
97
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
service.task_template["container_spec"]
|
|
98
|
+
# Logs raw
|
|
99
|
+
service.logs(stdout: 1, stderr: 1)
|
|
162
100
|
|
|
163
|
-
#
|
|
164
|
-
|
|
101
|
+
# Health check
|
|
102
|
+
DockerSwarm::System.up # => "OK" si daemon responde
|
|
165
103
|
```
|
|
166
|
-
**Razón:** Docker API usa PascalCase. La gema mantiene fidelidad. Con indifferent access, podés usar strings o symbols, pero siempre PascalCase.
|
|
167
104
|
|
|
168
|
-
###
|
|
105
|
+
### Jerarquía de errores
|
|
106
|
+
|
|
107
|
+
Todas heredan de `DockerSwarm::Error`. Tres formas de acceso equivalentes: `DockerSwarm::NotFound`, `DockerSwarm::Error::NotFound`, `DockerSwarm::Errors::NotFound`.
|
|
108
|
+
|
|
109
|
+
| Excepción | Status | Cuándo |
|
|
110
|
+
|---|---|---|
|
|
111
|
+
| `BadRequest` | 400 | Payload malformado |
|
|
112
|
+
| `Unauthorized` | 401 | TLS sin credenciales |
|
|
113
|
+
| `Forbidden` | 403 | Permisos insuficientes (ej: swarm op en worker) |
|
|
114
|
+
| `NotFound` | 404 | Recurso inexistente (capturado por `find`/`destroy`) |
|
|
115
|
+
| `NotAcceptable` | 406 | Headers Accept incompatibles (raro) |
|
|
116
|
+
| `RequestTimeout` | 408 | Daemon tardó (subí `read_timeout`) |
|
|
117
|
+
| `Conflict` | 409 | Nombre duplicado o `Version.Index` stale |
|
|
118
|
+
| `UnprocessableEntity` | 422 | Payload semánticamente inválido |
|
|
119
|
+
| `TooManyRequests` | 429 | Rate limiting |
|
|
120
|
+
| `InternalServerError` | 500 | Bug Docker o update sin `version` query param |
|
|
121
|
+
| `BadGateway` | 502 | Proxy entre cliente y daemon falló |
|
|
122
|
+
| `ServiceUnavailable` | 503 | Daemon reiniciando |
|
|
123
|
+
| `GatewayTimeout` | 504 | Proxy timeout |
|
|
124
|
+
| `Communication` | — | Socket caído / inalcanzable. `cause` mantiene el `Excon::Error` original |
|
|
125
|
+
|
|
126
|
+
## Gotchas / breaking
|
|
127
|
+
|
|
128
|
+
- **PascalCase fiel.** Los atributos NO se transforman: `service.Spec`, `service.TaskTemplate`, `service.Version` — no `snake_case`. Indifferent access soportado: `service.Spec[:Name]` ≡ `service.Spec["Name"]`.
|
|
129
|
+
- **`Version.Index` obligatorio en updates** (Service/Node/Network). La gema lo extrae automático de `self.Version["Index"]`. Si construís el request por afuera (`DockerSwarm.request`), tenés que pasarlo vos.
|
|
130
|
+
- **Retries automáticos sólo en métodos seguros** (GET/HEAD/PUT/DELETE/OPTIONS). POST/PATCH **no** reintentan para evitar duplicados — si el socket se cae durante un `create`, el caller decide qué hacer. Ver §3.5 de `docs/behavior/behavior.md`.
|
|
131
|
+
- **`Spec` se mergea con `deep_merge` en updates**, no se reemplaza. Pasale sólo los campos que cambian: `service.update(Mode: {...})`, no `service.update(Spec: {...completo})`.
|
|
132
|
+
- **`assign_attributes` muta antes de validar.** Si `update` falla por `valid?` o por el API, la instancia local quedó mutada. Hacé `reload` si necesitás estado limpio.
|
|
133
|
+
- **`Container.create` no existe** en la gema (gap intencional F1). Si necesitás crear containers standalone, usá `DockerSwarm.request(method: :post, path: "containers/create", ...)` directo.
|
|
134
|
+
- **Pull con registry privado no soportado.** `Image.create` no inyecta header `X-Registry-Auth`. Para registries privados, fallback a `DockerSwarm.request` con headers manuales.
|
|
135
|
+
- **`destroy` es graceful con 404** (retorna `nil`), no con 409. Si el recurso está en uso, `Conflict` se propaga.
|
|
136
|
+
- **Logs sensibles enmascarados** automáticamente: keys matching `password|pass|passwd|secret|token|api_key|auth|\bdata\b` → `[FILTERED]`. `\bdata\b` evita filtrar `metadata`/`database`.
|
|
137
|
+
|
|
138
|
+
## Testing
|
|
139
|
+
|
|
140
|
+
Mockear `DockerSwarm::Api.request`:
|
|
169
141
|
|
|
170
142
|
```ruby
|
|
171
|
-
|
|
172
|
-
|
|
143
|
+
expect(DockerSwarm::Api).to receive(:request).with(
|
|
144
|
+
hash_including(action: DockerSwarm::Api::ENDPOINTS[:services][:show])
|
|
145
|
+
).and_return({ "ID" => "svc-1", "Spec" => { "Name" => "web" }, "Version" => { "Index" => 1 } })
|
|
173
146
|
|
|
174
|
-
|
|
175
|
-
service.update(Mode: { Replicated: { Replicas: 5 } })
|
|
147
|
+
service = DockerSwarm::Service.find("svc-1")
|
|
176
148
|
```
|
|
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
149
|
|
|
181
|
-
|
|
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.
|
|
150
|
+
Para `create`: mockear **dos** llamadas (POST + show de reload). Para errores: `and_raise(DockerSwarm::NotFound)` etc.
|
|
189
151
|
|
|
190
|
-
|
|
152
|
+
## Integración (Rails)
|
|
191
153
|
|
|
192
154
|
```ruby
|
|
193
|
-
#
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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
|
|
155
|
+
# config/initializers/docker_swarm.rb
|
|
156
|
+
DockerSwarm.configure do |config|
|
|
157
|
+
config.socket_path = ENV.fetch("DOCKER_URL", "unix:///var/run/docker.sock")
|
|
158
|
+
config.logger = Rails.logger
|
|
159
|
+
config.log_level = Rails.logger.level
|
|
207
160
|
end
|
|
208
161
|
```
|
|
209
|
-
**Razón:** La jerarquía de errores mapea cada status HTTP. Capturar errores específicos permite manejar cada caso.
|
|
210
|
-
|
|
211
|
-
## Errores
|
|
212
162
|
|
|
213
|
-
|
|
163
|
+
Logs salen en formato KV (`component=docker_swarm.connection event=request_success ...`) compatible con parsers estructurados.
|
|
214
164
|
|
|
215
|
-
|
|
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 |
|
|
165
|
+
## Índice de artefactos
|
|
221
166
|
|
|
222
|
-
|
|
167
|
+
- [`docs/glossary/glossary.md`](docs/glossary/glossary.md) — definición de términos (primitivas Docker + conceptos internos).
|
|
168
|
+
- [`docs/behavior/behavior.md`](docs/behavior/behavior.md) — secuencias load-bearing (create+reload, update+Version, retry-policy, error-mapping, etc.).
|
|
169
|
+
- `docs/data/` — `n/a` (gema sin DB).
|
|
170
|
+
- `docs/api/`, `docs/interface/`, `docs/topology/` — F2 declaradas, **no implementadas**. El contrato resumido de arriba reside **transitoriamente** acá (RFC-008 §2 coexistencia transitoria con destino pendiente).
|
|
223
171
|
|
|
224
|
-
##
|
|
172
|
+
## Versionado del contrato
|
|
225
173
|
|
|
226
|
-
-
|
|
227
|
-
- [Catálogo de Errores](references/errores.md) — Todas las excepciones con causa y resolución
|
|
174
|
+
Este SKILL.md viaja version-locked con el release de la gema (`gemspec.files` incluye `skill/**/*`). Para consumir desde otro proyecto: el agente lee `Gem.loaded_specs["docker-swarm"].gem_dir + "/skill/SKILL.md"`. Links relativos del paquete del release; no `HEAD`/branch.
|