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.
@@ -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).
@@ -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
- if attributes_to_assign.is_a?(Hash)
82
- attributes_to_assign = attributes_to_assign.with_indifferent_access
83
- normalized_attributes = {}
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
- attributes_to_assign.each do |key, value|
86
- normalized_key = key.to_s == "Id" ? "ID" : key.to_s
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
- super(normalized_attributes)
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
- # Combinar opciones por defecto de la gema con las de la petición
23
+ method = options[:method].to_s.downcase.to_sym
24
+ idempotent = IDEMPOTENT_METHODS.include?(method)
25
+
20
26
  request_options = {
21
- idempotent: true,
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
- # Intentamos recuperar la causa original si es una de nuestras excepciones.
41
- actual_error = (e.respond_to?(:cause) && e.cause&.class&.name&.include?("DockerSwarm::Error")) ? e.cause : e
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
- if actual_error.class.name.include?("DockerSwarm::Error")
57
+ case actual_error
58
+ when ::DockerSwarm::Error
52
59
  raise actual_error
53
- elsif actual_error.is_a?(Excon::Error::Socket)
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
- SENSITIVE_KEYS = /password|token|api_key|auth|secret|data/i.freeze
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DockerSwarm
4
- VERSION = "0.5.4"
4
+ VERSION = "0.7.0"
5
5
  end
data/skill/SKILL.md CHANGED
@@ -1,227 +1,174 @@
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). **Nota:** Los atributos dinámicos en Inspectable deben invocarse con `send(:ID)` para evitar `NameError` por interpretación como constante.
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)
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:port
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 # segundos
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
- ### 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.
52
+ Defaults son razonables: en local sin TLS, no necesitás bloque `configure`.
110
53
 
111
- ## FAQ
54
+ ### Símbolos públicos por modelo
112
55
 
113
- ### Como conecto a un Docker remoto por TCP?
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
- ```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.
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
- ### 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?
72
+ ### Operaciones típicas
127
73
 
128
74
  ```ruby
129
- DockerSwarm::Service.all(label: ["env=production", "app=web"])
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
- ### Como manejo errores de conexión?
80
+ # Lookup graceful (nil si 404)
81
+ service = DockerSwarm::Service.find("svc-id") # => Service | nil
135
82
 
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`).
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
- ### Puedo usar validaciones de ActiveModel?
89
+ # Update atómico (Version.Index extraído automáticamente)
90
+ service.update(Mode: { Replicated: { Replicas: 3 } })
146
91
 
147
- Sí. Todos los modelos heredan de `ActiveModel::Model`. Podés agregar validaciones custom:
92
+ # Force-recreate de tasks (equivalente a `docker service update --force`)
93
+ service.restart
148
94
 
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
95
+ # Destroy graceful (nil si 404)
96
+ service.destroy
158
97
 
159
- ```ruby
160
- # MAL
161
- service.task_template["container_spec"]
98
+ # Logs raw
99
+ service.logs(stdout: 1, stderr: 1)
162
100
 
163
- # BIEN
164
- service.TaskTemplate["ContainerSpec"]
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
- ### Reemplazar Spec en vez de mergear
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
- # MAL — pierde campos existentes del Spec
172
- service.update(Spec: { Mode: { Replicated: { Replicas: 5 } } })
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
- # BIEN — mergear solo lo que cambia
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
- ```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.
150
+ Para `create`: mockear **dos** llamadas (POST + show de reload). Para errores: `and_raise(DockerSwarm::NotFound)` etc.
189
151
 
190
- ### No capturar errores específicos
152
+ ## Integración (Rails)
191
153
 
192
154
  ```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
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
- Los errores más comunes. Ver [Catálogo de Errores](references/errores.md) para la referencia completa.
163
+ Logs salen en formato KV (`component=docker_swarm.connection event=request_success ...`) compatible con parsers estructurados.
214
164
 
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 |
165
+ ## Índice de artefactos
221
166
 
222
- Todas heredan de `DockerSwarm::Error`. Se acceden como `DockerSwarm::NotFound` (alias) o `DockerSwarm::Error::NotFound`.
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
- ## Referencias
172
+ ## Versionado del contrato
225
173
 
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
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.