docker-swarm 0.7.2 → 0.9.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.
@@ -1,6 +1,6 @@
1
1
  # Glosario — docker-swarm
2
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
3
+ > meta: artefacto · RFC-009 · generado dev-enrich · anclado a `v0.9.0` · cobertura: completo inicial (primitivas Docker + arquitectura interna); no se acrecienta sin tocar el flujo/concepto
4
4
 
5
5
  ## 1. Resumen
6
6
 
@@ -8,81 +8,81 @@ Términos de negocio que la gema `docker-swarm` materializa. Dos grupos: **primi
8
8
 
9
9
  ## 2. Términos
10
10
 
11
- ### Service
11
+ ## Service
12
12
 
13
13
  Servicio de Docker Swarm: definición declarativa de un conjunto de tasks que corren en el cluster. La gema lo expone como CRUD completo + `restart` + `logs`. Update atómico vía `Version.Index`.
14
14
  **Binding:** [`DockerSwarm::Service`](../../lib/docker_swarm/models/service.rb)
15
15
 
16
- ### Node
16
+ ## Node
17
17
 
18
18
  Miembro físico del cluster Swarm (manager o worker). Read-only desde el punto de vista de creación: los nodos se unen al swarm fuera de la gema; la gema sólo permite update (rol/disponibilidad) y destroy.
19
19
  **Binding:** [`DockerSwarm::Node`](../../lib/docker_swarm/models/node.rb)
20
20
 
21
- ### Task
21
+ ## Task
22
22
 
23
23
  Unidad de ejecución de un Service en un Node específico. Read-only: las tasks se generan automáticamente por el orquestador a partir del Spec del Service. La gema sólo permite listar/inspeccionar/obtener logs.
24
24
  **Binding:** [`DockerSwarm::Task`](../../lib/docker_swarm/models/task.rb)
25
25
 
26
- ### Container
26
+ ## Container
27
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.
28
+ Container Docker standalone (no Swarm). La gema expone create/start/stop/destroy/logs. La creación estuvo fuera de scope en F1 —el caso de uso primario de la gema es Swarm— y entró con ADR-025 cláusula 1: operar datos on-host durante una migración necesita un **helper container efímero** con nombre determinista, que es lo que habilita adoptarlo en un reintento en vez de duplicarlo.
29
29
  **Binding:** [`DockerSwarm::Container`](../../lib/docker_swarm/models/container.rb)
30
30
 
31
- ### Image
31
+ ## Image
32
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).
33
+ Imagen Docker (registry o local). La gema cubre listar, pull (`Image.pull`, explícito y síncrono) y destroy. Build local no cubierto (no caso de uso de orquestación). El `create` genérico se retiró: `Image` ya no es Creatable.
34
34
  **Binding:** [`DockerSwarm::Image`](../../lib/docker_swarm/models/image.rb)
35
35
 
36
- ### Network
36
+ ## Network
37
37
 
38
38
  Red Docker (overlay para Swarm, bridge para Container). CRUD completo. Update soporta conectar/desconectar containers.
39
39
  **Binding:** [`DockerSwarm::Network`](../../lib/docker_swarm/models/network.rb)
40
40
 
41
- ### Volume
41
+ ## Volume
42
42
 
43
43
  Volumen Docker (named volume). CRUD sin update (Docker no soporta update de Volume). La respuesta del index viene envuelta en `{"Volumes": [...]}` — manejado vía `root_key`.
44
44
  **Binding:** [`DockerSwarm::Volume`](../../lib/docker_swarm/models/volume.rb)
45
45
 
46
- ### Config
46
+ ## Config
47
47
 
48
48
  Configuración inmutable distribuida en el cluster (archivos de configuración, manifests). CRUD sin update — Docker requiere recrear. Sólo Swarm.
49
49
  **Binding:** [`DockerSwarm::Config`](../../lib/docker_swarm/models/config.rb)
50
50
 
51
- ### Secret
51
+ ## Secret
52
52
 
53
53
  Dato sensible distribuido en el cluster (passwords, tokens, certs). Misma semántica que Config pero el `Data` se filtra automáticamente en logs vía `LogHelper`. Sólo Swarm.
54
54
  **Binding:** [`DockerSwarm::Secret`](../../lib/docker_swarm/models/secret.rb)
55
55
 
56
- ### Swarm
56
+ ## Swarm
57
57
 
58
58
  Cluster Docker Swarm como entidad singleton. Sólo `show` (info del cluster: ID, Version, Spec, JoinTokens). No CRUD — el cluster se inicializa/disuelve fuera de la gema.
59
59
  **Binding:** [`DockerSwarm::Swarm`](../../lib/docker_swarm/models/swarm.rb)
60
60
 
61
- ### System
61
+ ## System
62
62
 
63
63
  Daemon Docker como entidad singleton. Métodos estáticos: `info`, `version`, `up` (ping), `df` (disk usage). Útil para health checks y observabilidad.
64
64
  **Binding:** [`DockerSwarm::System`](../../lib/docker_swarm/models/system.rb)
65
65
 
66
66
  ---
67
67
 
68
- ### Base (ORM base)
68
+ ## Base (ORM base)
69
69
 
70
70
  Clase base de todos los modelos. Hereda de `ActiveModel::Model`. Provee accessors dinámicos PascalCase, `find`, `all`, `where`, `reload`, `payload_for_docker`. Centraliza el patrón ORM contra Docker Engine API.
71
71
  **Binding:** [`DockerSwarm::Base`](../../lib/docker_swarm/base.rb)
72
72
 
73
- ### Concern
73
+ ## Concern
74
74
 
75
75
  Mixin (`ActiveSupport::Concern`) que agrega capacidad CRUD/auxiliar a un modelo. La gema define cinco concerns ortogonales: cada modelo incluye los que aplican a su semántica Docker.
76
76
 
77
77
  | Concern | Símbolo | Aplica a |
78
78
  |---|---|---|
79
- | Creatable | [`DockerSwarm::Concerns::Creatable`](../../lib/docker_swarm/concerns/creatable.rb) | Service, Network, Volume, Config, Secret, Image |
79
+ | Creatable | [`DockerSwarm::Concerns::Creatable`](../../lib/docker_swarm/concerns/creatable.rb) | Service, Network, Volume, Config, Secret |
80
80
  | Updatable | [`DockerSwarm::Concerns::Updatable`](../../lib/docker_swarm/concerns/updatable.rb) | Service, Node, Network |
81
81
  | Deletable | [`DockerSwarm::Concerns::Deletable`](../../lib/docker_swarm/concerns/deletable.rb) | Service, Node, Container, Network, Volume, Config, Secret, Image |
82
82
  | Loggable | [`DockerSwarm::Concerns::Loggable`](../../lib/docker_swarm/concerns/loggable.rb) | Service, Task, Container |
83
83
  | Inspectable | [`DockerSwarm::Concerns::Inspectable`](../../lib/docker_swarm/concerns/inspectable.rb) | Todos (vía Base) |
84
84
 
85
- ### Middleware
85
+ ## Middleware
86
86
 
87
87
  Capa Excon en el stack del cliente HTTP. Tres middlewares custom: serialización de body, parsing de respuesta con indifferent access, mapeo de status a excepción tipada.
88
88
 
@@ -92,32 +92,32 @@ Capa Excon en el stack del cliente HTTP. Tres middlewares custom: serialización
92
92
  | ResponseJSONParser | [`DockerSwarm::Middleware::ResponseJSONParser`](../../lib/docker_swarm/middleware/response_json_parser.rb) | Parsea JSON y aplica `with_indifferent_access` |
93
93
  | ErrorHandler | [`DockerSwarm::Middleware::ErrorHandler`](../../lib/docker_swarm/middleware/error_handler.rb) | Mapea 4xx/5xx → `DockerSwarm::Error::*` + log `business_error` |
94
94
 
95
- ### Connection
95
+ ## Connection
96
96
 
97
97
  Wrapper sobre el cliente Excon. Memoiza la conexión, aplica timeouts/retries de configuración, clasifica errores idempotentes vs no-idempotentes (post-fix correctness), y emite logs KV.
98
98
  **Binding:** [`DockerSwarm::Connection`](../../lib/docker_swarm/connection.rb)
99
99
 
100
- ### Dynamic Accessor
100
+ ## Dynamic Accessor
101
101
 
102
102
  Mecanismo por el cual los modelos exponen atributos no declarados. Docker Engine evoluciona y agrega campos: la gema usa `method_missing` + cache en `defined_attributes` (Set) para responder a cualquier campo PascalCase de la respuesta sin requerir update del código.
103
103
  **Binding:** [`DockerSwarm::Base#method_missing`](../../lib/docker_swarm/base.rb), [`DockerSwarm::Base.defined_attributes`](../../lib/docker_swarm/base.rb)
104
104
 
105
- ### Spec deep_merge
105
+ ## Spec deep_merge
106
106
 
107
107
  Estrategia de actualización parcial del campo `Spec` de un modelo. En vez de reemplazar Spec completo, `assign_attributes` hace `deep_merge` cuando la key es `Spec` y ambos valores son Hash. Razón: updates parciales no pierden campos anidados no tocados.
108
108
  **Binding:** [`DockerSwarm::Base#assign_attributes`](../../lib/docker_swarm/base.rb)
109
109
 
110
- ### Version.Index
110
+ ## Version.Index
111
111
 
112
112
  Mecanismo de control de concurrencia optimista de Docker para updates atómicos. Cada Service/Node tiene `Version.Index` que incrementa en cada cambio. Update requiere enviar el index actual como query param; si no coincide, Docker rechaza (500). La gema lo extrae automáticamente en `Updatable#update`.
113
113
  **Binding:** [`DockerSwarm::Concerns::Updatable#update`](../../lib/docker_swarm/concerns/updatable.rb)
114
114
 
115
- ### LogHelper
115
+ ## LogHelper
116
116
 
117
117
  Módulo de formateo de logs en KV (`key=value`) con masking automático de claves sensibles. La regex (`password|pass|passwd|secret|token|api_key|auth|\bdata\b`) reemplaza valores por `[FILTERED]`. `\bdata\b` evita falsos positivos en `metadata`/`database`.
118
118
  **Binding:** [`DockerSwarm::LogHelper`](../../lib/docker_swarm/log_helper.rb)
119
119
 
120
- ### payload_for_docker
120
+ ## payload_for_docker
121
121
 
122
122
  Transformación interna que prepara un modelo para enviarlo al API: descarta atributos internos (`ID`/`Version`/`CreatedAt`/`UpdatedAt`), extrae el contenido de `Spec` al root y mergea otros campos top-level. Resultado: el payload que Docker espera para `create`/`update`.
123
123
  **Binding:** [`DockerSwarm::Base#payload_for_docker`](../../lib/docker_swarm/base.rb)
@@ -126,7 +126,6 @@ Transformación interna que prepara un modelo para enviarlo al API: descarta atr
126
126
 
127
127
  | Término | Inferencia | Confidence | Verificar |
128
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
129
  | Spec deep_merge | "razón: updates parciales no pierden campos" | declared | confirmado en CLAUDE.md decisión arquitectura |
131
130
  | Dynamic Accessor | "Docker evoluciona y agrega campos" | declared | confirmado en CLAUDE.md decisión arquitectura |
132
131
 
@@ -138,4 +137,5 @@ Transformación interna que prepara un modelo para enviarlo al API: descarta atr
138
137
  - **Fuera de alcance:**
139
138
  - Términos técnicos puros sin significado de negocio (ej: `instance_values`, `attr_accessor`) — son detalles de implementación, no contrato.
140
139
  - Glossary del Docker Engine API (cómo funciona internamente Swarm, raft, gossip) — vive en docs de Docker, no se duplica acá.
140
+ - **Inferencia resuelta (2026-08-03):** §3 registraba como `inferred` la pregunta de si *"creación intencionalmente fuera de scope F1"* era una decisión de alcance o un gap a cubrir. Quedó resuelta: **era una decisión de alcance** (Swarm-first), y ADR-025 cláusula 1 **amplió el alcance** al aparecer un caso de uso real (el helper container efímero de la migración del ACS). La fila salió de §3 porque ya no es una inferencia pendiente.
141
141
  - **Cadencia:** incremental por PR a partir de acá; ausencia ≠ inexistencia (RFC-009).
@@ -0,0 +1,121 @@
1
+ # Interfaz — docker-swarm
2
+
3
+ > meta: artefacto · RFC-004 · generado arch-structure · anclado a `v0.9.0` · cobertura: API Ruby pública de la gema (`lib/docker_swarm/**`); símbolos internos marcados en §4
4
+
5
+ ## 1. Resumen
6
+
7
+ API Ruby pública de la gema. Entrypoint `DockerSwarm` (config + cliente HTTP). 11 modelos `ActiveModel`-compatibles heredan de `DockerSwarm::Base` y mezclan concerns (`Creatable`/`Updatable`/`Deletable`/`Loggable`). Atributos vía accessors dinámicos PascalCase (`method_missing`). Jerarquía de errores en `DockerSwarm::Error` (detalle en [`docs/errors/errors.md`](../errors/errors.md)).
8
+
9
+ ## 2. Cuerpo
10
+
11
+ Proyección RBS-conceptual: `símbolo · tipo · nota` (raíz → profundidad → alfabético). Firmas aplanadas; el wire-schema de los atributos PascalCase no es estático (ver §4).
12
+
13
+ ### Módulo raíz `DockerSwarm`
14
+
15
+ | símbolo | tipo | nota |
16
+ |---|---|---|
17
+ | `DockerSwarm` | módulo | namespace raíz |
18
+ | `DockerSwarm::VERSION` | constante | `"0.8.0"` (`version.rb`) |
19
+ | `DockerSwarm.configuration` | attr (r/w) | instancia de `Configuration`; lazy-init en `configure`/`connection` |
20
+ | `DockerSwarm.configure { \|config\| ... }` | método de módulo | crea/yields `Configuration`; aplica `log_level` al logger; resetea la conexión memoizada |
21
+ | `DockerSwarm.connection` | método de módulo | `Connection` memoizada (auto-`configure` si falta) |
22
+ | `DockerSwarm.request(options = {})` | método de módulo | delega en `connection.request`; entrypoint de bajo nivel (escape hatch para llamadas crudas) |
23
+
24
+ ### `DockerSwarm::Configuration` (`configuration.rb`)
25
+
26
+ | símbolo | tipo | nota |
27
+ |---|---|---|
28
+ | `#socket_path` | attr (r/w) | default `"unix:///var/run/docker.sock"` |
29
+ | `#logger` | attr (r/w) | default `Logger.new($stdout)` |
30
+ | `#log_level` | attr (r/w) | default `Logger::INFO` |
31
+ | `#read_timeout` / `#read_timeout=` | attr (r) + setter | default `60.0`; setter castea `to_f` |
32
+ | `#write_timeout` / `#write_timeout=` | attr (r) + setter | default `60.0`; setter castea `to_f` |
33
+ | `#connect_timeout` / `#connect_timeout=` | attr (r) + setter | default `10.0`; setter castea `to_f` |
34
+ | `#max_retries` / `#max_retries=` | attr (r) + setter | default `3`; setter castea `to_i` |
35
+
36
+ Inventario completo de opciones: [`docs/config/configuracion.md`](../config/configuracion.md).
37
+
38
+ ### `DockerSwarm::Base` (`base.rb`) — base de los modelos
39
+
40
+ `include ActiveModel::Model`, `include Concerns::Inspectable`.
41
+
42
+ | símbolo | tipo | nota |
43
+ |---|---|---|
44
+ | `.resource_name` | método de clase | `name.demodulize.downcase.pluralize`; override en `Swarm`/`System` |
45
+ | `.routes` | método de clase | `Api::ENDPOINTS[resource_name.to_sym]` |
46
+ | `.root_key` | método de clase | `nil` por default; override `"Volumes"` en `Volume` |
47
+ | `.defined_attributes` | método de clase | `Set` de accessors ya definidos (interno; cache de `method_missing`) |
48
+ | `.all(filters = {})` | método de clase | `GET index`; mapea a instancias; aplica `root_key`; `[]` si vacío |
49
+ | `.find(id)` | método de clase | `GET show`; `nil` si `Errors::NotFound` |
50
+ | `.where(filters)` | método de clase | alias de `all` |
51
+ | `#initialize(attributes = {})` | método de instancia | `assign_attributes` si presente |
52
+ | `#assign_attributes(new_attributes)` | método de instancia | normaliza `Id`→`ID`; `deep_merge` del campo `Spec`; `ArgumentError` si no es Hash |
53
+ | `#attributes` | método de instancia | `instance_values` sin internos de ActiveModel |
54
+ | `#serializable_hash` / `#as_json` | método de instancia | == `attributes` |
55
+ | `#payload_for_docker` | método de instancia | descarta `ID/Version/CreatedAt/UpdatedAt`; aplana `Spec` al root |
56
+ | `#persisted?` | método de instancia | `ID` presente |
57
+ | `#id` | método de instancia | == `self.ID` |
58
+ | `#reload` | método de instancia | re-`find` por `id` y re-asigna |
59
+ | `#method_missing` / `#respond_to_missing?` | método de instancia | accessors dinámicos PascalCase (atributos del recurso Docker) |
60
+
61
+ ### Concerns (`concerns/*.rb`)
62
+
63
+ | símbolo | tipo | nota |
64
+ |---|---|---|
65
+ | `Concerns::Creatable.create(attributes = {}, **opts)` | método de clase (mixin) | `new` + `save`; retorna la instancia. `opts` reservado `registry_auth:` (→ header `X-Registry-Auth`); el resto se pliega como atributos |
66
+ | `Concerns::Creatable#save(registry_auth: nil)` | método de instancia | `false` si `!valid?`; `update` si `persisted?`; si no `POST create` + `reload`. `registry_auth` viaja como header, nunca en el payload. Los `create_query_params` del modelo viajan por query string y se **excluyen** del payload |
67
+ | `Concerns::Creatable.create_query_params` | método de clase (mixin) | `[]` por default; override por modelo. Atributos que el Engine toma por query string en el `create` y **descarta en silencio** si van en el body |
68
+ | `Concerns::Creatable#query_params_for_docker` | método de instancia | los `create_query_params` seteados en esta instancia, con claves símbolo; `{}` si el modelo no declara ninguno |
69
+ | `Concerns::Updatable#update(new_attributes = {}, **opts)` | método de instancia | extrae `Version.Index`; `false` si `!valid?`; `POST update` con `?version=`. `opts` reservado `registry_auth:` (header) / `registry_auth_from:` (query `registryAuthFrom`, `spec`\|`previous-spec`, excluyente con `registry_auth`); el resto se pliega como atributos |
70
+ | `Concerns::Deletable.destroy(id)` | método de clase (mixin) | `DELETE destroy`; `nil` si `Errors::NotFound` |
71
+ | `Concerns::Deletable#destroy` | método de instancia | delega en `.destroy(self.ID)` |
72
+ | `Concerns::Loggable#logs(query_params = { stdout: 1, stderr: 1 })` | método de instancia | `GET logs`; retorna **texto ya demultiplexado** (sin los 8 bytes de cabecera por frame) — el demux lo hace `Middleware::LogStreamDemuxer`, el consumidor no ve el framing. Un stream de TTY (sin framing) pasa intacto |
73
+ | `Concerns::Inspectable#inspect` | método de instancia | render legible (ID/Name/Version/Spec) |
74
+
75
+ ### Modelos (`models/*.rb`)
76
+
77
+ | símbolo | tipo | concerns + métodos propios |
78
+ |---|---|---|
79
+ | `DockerSwarm::Service` | clase < Base | Creatable, Updatable, Deletable, Loggable; `#restart` (incrementa `TaskTemplate.ForceUpdate`); `create`/`update` aceptan `registry_auth:` (+ `update`: `registry_auth_from:`) para auth de registry privado |
80
+ | `DockerSwarm::Node` | clase < Base | Updatable, Deletable (sin `create`: los nodos se unen fuera de la gema) |
81
+ | `DockerSwarm::Task` | clase < Base | Loggable (read-only; generadas por el orquestador) |
82
+ | `DockerSwarm::Container` | clase < Base | Creatable, Deletable, Loggable; `#start`, `#stop`; `.create_query_params == %w[name]` (el Engine toma el nombre por query string — en el body lo descarta en silencio y el container nace con nombre aleatorio). El `create` **no** es gap intencional desde ADR-025 cláusula 1 |
83
+ | `DockerSwarm::Image` | clase < Base | Deletable + `.pull(image_reference, registry_auth: nil)`. **NO** es Creatable (`Image.create` retirado sin alias). `.pull` = pull explícito síncrono: consume el stream NDJSON hasta EOF, eleva `DockerSwarm::Error` ante frame `error`/`errorDetail`, retorna `{ status: :pulled, image_ref:, digest? }` (sin `find` posterior) |
84
+ | `DockerSwarm::Network` | clase < Base | Creatable, Updatable, Deletable |
85
+ | `DockerSwarm::Volume` | clase < Base | Creatable, Deletable; `.root_key = "Volumes"` (respuesta wrapped) |
86
+ | `DockerSwarm::Config` | clase < Base | Creatable, Deletable (sin `update`: recrear) |
87
+ | `DockerSwarm::Secret` | clase < Base | Creatable, Deletable (sin `update`); `Data` filtrado en logs |
88
+ | `DockerSwarm::Swarm` | clase < Base | `.resource_name = "swarm"`; `.show` (singleton, info del cluster) |
89
+ | `DockerSwarm::System` | clase < Base | `.resource_name = "system"`; `.info`, `.version`, `.up`, `.df` (singleton) |
90
+
91
+ ### Superficie de bajo nivel / soporte
92
+
93
+ | símbolo | tipo | nota |
94
+ |---|---|---|
95
+ | `DockerSwarm::Api::ENDPOINTS` | constante (frozen Hash) | mapa `recurso → {operación → {method, path}}` |
96
+ | `DockerSwarm::Api.request(action:, arguments: {}, query_params: {}, payload: nil)` | método de clase | formatea el `path` y delega en `DockerSwarm.request` |
97
+ | `DockerSwarm::Connection.new(socket_path, logger)` | clase | cliente Excon (Unix socket o TCP); `#request`, `#socket_path`, `#logger` |
98
+ | `DockerSwarm::Connection::IDEMPOTENT_METHODS` | constante | `%i[get head put delete options]` (los únicos con retry) |
99
+ | `DockerSwarm::LogHelper.format_kv(payload)` | método de módulo | formatea KV + masking de claves sensibles |
100
+ | `DockerSwarm::LogHelper::SENSITIVE_KEYS` | constante (Regexp) | `password\|pass\|...\|\bdata\b` |
101
+ | `DockerSwarm::RegistryAuth.resolve(registry_auth:, registry_auth_from:)` | método de módulo | traduce las opciones de auth a `[headers, query_params]` (`X-Registry-Auth` / `registryAuthFrom`); valida exclusión mutua + enum antes de la request. Usado por `Image.pull` / `#save` / `#update`; la credencial nunca toca payload ni estado del modelo |
102
+ | `DockerSwarm::RegistryAuth::{HEADER, QUERY, FROM_VALUES}` | constantes | `"X-Registry-Auth"` · `:registryAuthFrom` · `%w[spec previous-spec]` |
103
+ | `DockerSwarm::Error` + subclases + aliases + `DockerSwarm::Errors` | clases/módulo | jerarquía de errores — detalle en [`docs/errors/errors.md`](../errors/errors.md) |
104
+ | `DockerSwarm::Middleware::{RequestEncoder, LogStreamDemuxer, ResponseJSONParser, ErrorHandler}` | clases | middlewares Excon; públicos por require pero de uso interno (ver §4) |
105
+ | `DockerSwarm::Middleware::LogStreamDemuxer::{MULTIPLEXED_CONTENT_TYPE, RAW_CONTENT_TYPE, HEADER_SIZE, STREAM_TYPES}` | constantes | `"application/vnd.docker.multiplexed-stream"` · `"application/vnd.docker.raw-stream"` · `8` · `[0, 1, 2]` |
106
+
107
+ ## 3. Inferencias
108
+
109
+ | afirmación | confidence | a verificar |
110
+ |---|---|---|
111
+ | `Connection`, `Api` y los `Middleware::*` son de uso **interno** (un consumidor normal usa los modelos, no estas clases) | inferred | son `public` en Ruby; no hay marca `@api private`. `DockerSwarm.request`/`Api` son el escape-hatch documentado en `skill/SKILL.md` |
112
+ | `.defined_attributes` es interno (cache de `method_missing`), no superficie de consumo | inferred | público pero sin uso externo plausible |
113
+ | Los atributos PascalCase (`service.Spec`, `service.Version`, …) son la superficie real de datos, pero su set depende de la respuesta del Docker Engine API | declared | `method_missing` define accessors on-demand; el shape lo fija Docker, no la gema |
114
+
115
+ ## 4. Cobertura y fronteras
116
+
117
+ - **Wire-schema de atributos:** los modelos no declaran atributos estáticos — se materializan dinámicamente desde el JSON de Docker (`method_missing`). El shape de `Spec`/`TaskTemplate`/etc. es el del Docker Engine API v1.41, **fuera de este repo** (doc oficial Docker). `unspecified` acá a propósito.
118
+ - **`interface` vs `operaciones` (RFC-003):** esta gema NO expone superficie HTTP/CLI/eventos propia → `docs/api/operaciones` es `n/a`. Su superficie pública ES esta interfaz Ruby. Lo que la gema *consume* (Docker Engine API) vive en [`docs/consumed/`](../consumed/docker-engine-api.md).
119
+ - **Errores:** la jerarquía `DockerSwarm::Error` se cataloga en [`docs/errors/errors.md`](../errors/errors.md); acá solo se referencia.
120
+ - **Símbolos internos:** `Connection`, `Api`, `Middleware::*`, `Base.defined_attributes` se listan por completitud pero no son API de consumo recomendada; un cambio en ellos no rompe el contrato del consumidor típico (que usa los modelos).
121
+ - **Significado de negocio** de cada modelo/término → [`docs/glossary/glossary.md`](../glossary/glossary.md); secuencias → [`docs/behavior/behavior.md`](../behavior/behavior.md).
@@ -0,0 +1,54 @@
1
+ # Release — docker-swarm
2
+
3
+ > meta: artefacto · RFC-014 · generado arch-structure + enriquecido arch-enrich · anclado a `v0.9.0` · cobertura: §a estructura completa (versión · changelog · build-trigger · patrón); §b enrich completa (deploy · rollback · ambientes · dueño)
4
+
5
+ ## 1. Resumen
6
+
7
+ Gema Ruby publicada en RubyGems. Release por **tag `v*`**: el push del tag dispara el workflow `Publish to RubyGems` que hace `gem build` + `gem push`. Sin deploy de infraestructura (gema, no servicio): no hay `Dockerfile`, ni branches `production`/`staging`, ni ambientes.
8
+
9
+ ## 2. Cuerpo
10
+
11
+ ### §a Estructura (verificable del código)
12
+
13
+ | campo | valor | fuente |
14
+ |---|---|---|
15
+ | versión actual | `0.9.0` | `lib/docker_swarm/version.rb` (`DockerSwarm::VERSION`) |
16
+ | esquema de versión | SemVer (`MAJOR.MINOR.PATCH`) | `CHANGELOG.md` (breaking/mejoras/correcciones por bump) |
17
+ | artefacto liberado | gema `docker-swarm` a RubyGems | `docker-swarm.gemspec` (`spec.name`), `.github/workflows/release.yml` |
18
+ | changelog | `CHANGELOG.md`, formato Keep a Changelog, entradas fechadas por versión | `CHANGELOG.md` |
19
+ | licencia | MIT | `docker-swarm.gemspec` (`spec.license`) |
20
+ | build-trigger | push de tag `v*` | `.github/workflows/release.yml` (`on.push.tags: ['v*']`) |
21
+ | **patrón de trigger (RFC-014)** | **patrón 1** — publish per-repo-visible en workflow (tag → RubyGems) | `.github/workflows/release.yml` |
22
+ | runner de release | `ubuntu-latest`, Ruby `3.4.4` | `.github/workflows/release.yml` |
23
+ | pasos de publish | `gem build *.gemspec` + `gem push *.gem` | `.github/workflows/release.yml` |
24
+ | credencial de publish | `secrets.RUBYGEMS_API_KEY` (env `RUBYGEMS_API_KEY`) | `.github/workflows/release.yml` |
25
+ | MFA de publish | requerida (`rubygems_mfa_required = "true"`) | `docker-swarm.gemspec` (`spec.metadata`) |
26
+ | Ruby mínimo del release | `>= 3.2.0` | `docker-swarm.gemspec` (`required_ruby_version`) |
27
+ | contenido empaquetado | `lib`, `exe`, `skill`, `docs` + `README.md`, `CHANGELOG.md`, `LICENSE` | `docker-swarm.gemspec` (`spec.files`). Empaquetar `skill/` + `docs/` es **intencional**: la gema shippea su skill version-locked (RFC-008) y los artefactos de arquitectura para consumidores/agentes |
28
+ | CI (no gatea el release) | workflow `Ruby` (`main.yml`) corre en push a `main` / PR: `rspec` (sin `type:integration`) + `rubocop`. **Independiente** de `release.yml` — el tag `v*` publica sin exigir que el CI haya pasado | `.github/workflows/main.yml` |
29
+ | dependencias runtime | `activesupport >= 6.0`, `activemodel >= 6.0`, `excon >= 0.80` | `docker-swarm.gemspec` |
30
+
31
+ ### §b Deploy · rollback · ambientes · dueño (enrich)
32
+
33
+ | dimensión | valor | nota |
34
+ |---|---|---|
35
+ | deploy | Publicación a RubyGems por `gem push` al pushear el tag `v*`. Los consumidores la reciben vía Bundler (`gem 'docker-swarm'`) tras la propagación del índice de RubyGems (~minutos) | "Deploy" para una gema = disponibilidad en RubyGems; no hay infraestructura que desplegar |
36
+ | rollback | **Bump correctivo (preferido):** publicar un patch nuevo (`X.Y.Z+1`) con el fix. RubyGems **prohíbe** re-pushear el mismo número de versión (restricción técnica). `gem yank -v X.Y.Z` — RubyGems lo permite en cualquier momento (no hay restricción técnica), pero es **política del equipo** reservarlo a casos graves (secreto filtrado, gema rota/ininstalable) | Distinguir: el re-push prohibido es técnico de RubyGems; el yank restringido es decisión del equipo (un yank rompe a quien fijó esa versión → se prefiere avanzar) |
37
+ | ambientes | No aplica: artefacto único publicado en RubyGems, sin staging/producción. El único "ambiente" es la versión instalada por cada consumidor | La matriz de compatibilidad la fija `required_ruby_version` (`>= 3.2.0`), no un ambiente de deploy |
38
+ | dueño del release | Gabriel (mantenedor) — taggea `v*` y publica; @Pablo contribuye fixes | Derivado del historial de `CHANGELOG.md` y confirmado por el equipo |
39
+
40
+ ## 3. Inferencias
41
+
42
+ | afirmación | confidence | a verificar |
43
+ |---|---|---|
44
+ | Patrón de trigger = 1 (publish per-repo-visible por tag), no patrón 3 (branch `production`/`staging`) | declared | `release.yml` sólo escucha `tags: v*`; no existen branches `production`/`staging` (`git branch -a`) |
45
+ | Esquema de versión = SemVer | inferred | derivado del uso en `CHANGELOG.md` (0.7.0 marcó breaking changes); no hay política SemVer declarada explícita en el repo |
46
+ | No hay deploy de infra | declared | ausencia de `Dockerfile`, `docker-compose.yml`, `helm/`, branches de ambiente — es gema, no servicio |
47
+ | El gem se buildea bajo Ruby `3.4.4` aunque declara floor `>= 3.2.0` | declared | runner fijo en `release.yml` (`3.4.4`) vs `required_ruby_version` del gemspec (`>= 3.2.0`); no es defecto — el build no acopla el floor de compatibilidad |
48
+
49
+ ## 4. Cobertura y fronteras
50
+
51
+ - **Estructura (§a) y enrich (§b) completas** al ancla `cccfe63`; significado de §b aportado por el mantenedor.
52
+ - **Proceso operativo del release** (quién taggea, checklist, bump de versión) lo ejecuta la skill `gem-release`; este artefacto documenta el contrato, no el paso-a-paso de la skill.
53
+ - **Trade-off conocido:** el release (`release.yml`, tag `v*`) **no exige** que el CI (`main.yml`) haya pasado — un tag publica aunque los tests fallen. Hoy el gate es de facto (el dev corre la suite antes de taggear), no forzado por el pipeline. Endurecerlo (exigir CI verde antes de publicar) es una mejora de proceso pendiente, no documentada como decisión formal.
54
+ - **Fuera de alcance:** la config runtime de la gema vive en `docs/config/configuracion.md`; la suite y su gate CI en `docs/test/testing.md` (este artefacto sólo referencia el gate pre-release, no lo redefine).
@@ -0,0 +1,100 @@
1
+ # Test — docker-swarm
2
+
3
+ > meta: artefacto · RFC-013 · generado arch-structure + enriquecido arch-enrich · anclado a `v0.9.0` · cobertura: estructura de la suite (`spec/`, `.github/workflows/main.yml`); §e enriquecida, §f enriquecida, §g `unknown` (sin incidentes registrados), §h enriquecida
4
+
5
+ ## 1. Resumen
6
+
7
+ Suite RSpec en dos niveles: **unit** (mockean `DockerSwarm::Api`/`Excon`, sin daemon) e **integration** (`type: :integration`, requieren un Docker daemon real). CI corre solo unit + RuboCop; integration es local/opt-in. Sin herramienta de coverage configurada.
8
+
9
+ ## 2. Cuerpo
10
+
11
+ ### a. Suites, frameworks y niveles
12
+
13
+ Framework: **RSpec** (`~> 3.0`). `verify_partial_doubles = true` (mocks estrictos).
14
+
15
+ | subdirectorio | propósito | nivel | helper |
16
+ |---|---|---|---|
17
+ | `spec/docker/swarm/*_spec.rb` | api, configuration, connection, log_helper, registry_auth | unit | `spec_helper` |
18
+ | `spec/docker/swarm/middleware/*_spec.rb` | error_handler, log_stream_demuxer, request_encoder, response_json_parser | unit | `spec_helper` |
19
+ | `spec/docker/swarm/models/*_spec.rb` | base, container, image, network, node, service, task + `shared_crud_spec` | unit | `spec_helper` |
20
+ | `spec/integration/*_spec.rb` | containers, infra, security, services, system | integration | `integration_helper` |
21
+
22
+ Tag de nivel: las integration declaran `RSpec.describe "...", type: :integration` (`spec/integration/*_spec.rb:5`). Las unit no llevan tag → se filtran con `~type:integration`.
23
+
24
+ `shared_crud_spec.rb` = shared examples de CRUD reusados por los specs de modelo.
25
+
26
+ ### b. Comando de corrida
27
+
28
+ | contexto | comando | qué corre |
29
+ |---|---|---|
30
+ | unit (local) | `bundle exec rspec --tag ~type:integration` | todo menos integration |
31
+ | integration (local) | `bundle exec rspec` | incluye integration (requiere Docker socket; override `DOCKER_URL`) |
32
+ | lint | `bundle exec rubocop` | rubocop-rails-omakase |
33
+ | CI (`.github/workflows/main.yml`) | `bundle exec rspec --tag ~type:integration` + `bundle exec rubocop` | unit + lint, Ruby 3.4.4 |
34
+
35
+ CI **no** corre integration (no hay daemon en el runner). Se dispara en push a `main` y en `pull_request`.
36
+
37
+ ### c. Fixtures / Factories
38
+
39
+ - **Sin fixtures YAML ni FactoryBot.** Los datos de prueba se construyen inline en cada spec.
40
+ - **Unit:** mockean `DockerSwarm::Api.request` (o `Excon`) con `and_return`/`and_raise` (stubs de respuesta Docker). `spec_helper` configura `socket_path = "unix:///tmp/docker.sock"` y logger a `/dev/null`.
41
+ - **Integration:** crean recursos reales contra el daemon; helper `random_name(prefix)` (`integration_helper.rb:26`) genera nombres únicos con `SecureRandom.hex(4)` para evitar colisiones.
42
+
43
+ ### d. Configuración de coverage
44
+
45
+ Ninguna. No hay `SimpleCov`/`.simplecov` ni umbral declarado en el repo (verificado: sin `SimpleCov` en `spec/` ni en `Gemfile.lock`).
46
+
47
+ ### e. Gaps de cobertura (narrado)
48
+
49
+ > Cobertura declarada — qué flujos de negocio están ejercitados y cuáles no. No es el % de líneas (no hay SimpleCov, §d).
50
+
51
+ **Cubierto (unit, con mocks):**
52
+ - Mapeo de errores HTTP → excepción: `error_handler_spec` (subset de status verificado: 200, 404, 429, 500 — no los 14).
53
+ - Modelos con métodos propios: `service` (incl. lógica de update/version), `node`, `task`, `container` (start/stop), `network`, `base` (accessors dinámicos, `assign_attributes`/`Spec` merge).
54
+ - CRUD genérico de `config`, `secret`, `volume`: vía `shared_crud_spec` (`it_behaves_like "a crud resource"`) — no tienen spec dedicado pero **sí** están cubiertos (create/find/destroy). `image` salió del CRUD genérico (su `create` era un pull) → tiene spec propio (abajo).
55
+ - `image`: `image_spec` (dedicado) — `Image.pull` (stream NDJSON, extracción de digest del frame `Digest:`, error tipado ante `error`/`errorDetail`, forma polimórfica del body) + `Deletable` y listado.
56
+ - Auth de registry privado: `registry_auth_spec` (helper `RegistryAuth`: exclusión mutua `registry_auth`/`registry_auth_from`, enum del `from`, traducción a header/query) + bloque registry-auth en `service_spec` (create/update, no-exposición de la credencial en logs).
57
+ - Infra de transporte: `api_spec`, `connection_spec`, `configuration_spec`, `log_helper_spec`, los 4 middleware specs.
58
+ - `swarm`, `system` (singletons): `swarm_spec`, `system_spec`.
59
+
60
+ **Cubierto (integration, daemon real):** lifecycle de containers, services, infra (networks/volumes), system (info/version/up/df), security (config/secret create+find+destroy).
61
+
62
+ **Gaps declarados:**
63
+ - `error_handler_spec` ejercita **un subset** de los 14 status; 400/401/403/406/408/409/422/502/503/504 y el fallback genérico no tienen aserción dedicada (gap de contrato §f).
64
+ - `Service#restart`, `Loggable#logs` — cobertura unit no confirmada por spec dedicado; verificar.
65
+ - Path de error `Communication` (socket caído) y la política de retry idempotente: no confirmado que haya spec dedicado en `connection_spec` (verificar).
66
+
67
+ ### f. Contract-assessment (¿los tests ejercitan los contratos públicos?)
68
+
69
+ | contrato | RFC | cubierto | nota |
70
+ |---|---|---|---|
71
+ | Interfaz Ruby pública | RFC-004 | parcial | model specs + `shared_crud` ejercitan `all/find/create/update/destroy/logs`; falta aserción sistemática de toda la superficie |
72
+ | Errores públicos | RFC-020 | parcial | `error_handler_spec` cubre el mapeo status→excepción pero **solo 4 de 14 status** → contrato de errores incompletamente verificado |
73
+ | Docker Engine API consumida | RFC-018 | sí (integration) | los specs `type: :integration` ejercitan el contrato real contra el daemon; unit lo mockea |
74
+
75
+ ### g. Link a incidente
76
+
77
+ `unknown` — **por ausencia de fuente, no por verificación de que no existan.** No hay un tracker de incidentes vinculado a este repo ni `refs incidente/PR` en los specs o en el historial localizable; por tanto no se puede afirmar ni que haya ni que no haya tests nacidos de un incidente. No es "cero incidentes verificado". Se completará si/cuando un test de regresión se ate explícitamente al bug que previene.
78
+
79
+ ### h. PII / datos sensibles en fixtures
80
+
81
+ **Sin PII real ni secretos reales.** Clasificación (no valores):
82
+
83
+ - Nombres de recursos: generados con `random_name(prefix)` → `SecureRandom.hex(4)` (`integration_helper.rb:26`). Sintéticos.
84
+ - `Config`/`Secret` Data en integration: literales de prueba base64 (`Base64.strict_encode64("hello world")`, `"top secret"` — `security_spec.rb`). **No** son credenciales reales; son strings de test.
85
+ - Unit specs: payloads inline mockeados, sin datos reales.
86
+
87
+ No cruza RFC-026 (no hay PII de personas ni secretos productivos en fixtures).
88
+
89
+ ## 3. Inferencias
90
+
91
+ | afirmación | confidence | a verificar |
92
+ |---|---|---|
93
+ | Los specs de modelo unit mockean `Api`/`Excon` (no tocan socket) | inferred | `spec_helper` apunta a `/tmp/docker.sock` inexistente → necesariamente stubean; patrón confirmado en `skill/SKILL.md` |
94
+ | Integration requiere daemon Swarm activo | declared | `integration_helper` usa el socket real (`DOCKER_URL` o default) |
95
+
96
+ ## 4. Cobertura y fronteras
97
+
98
+ - **Contenido de cada test case** individual queda en el código, no acá.
99
+ - **Niveles:** solo unit e integration; no hay system/e2e ni matriz de versiones (`Appraisals`) — Ruby único (3.4.4) en CI.
100
+ - **Coverage real (%)** no medible desde el repo (sin tool); §d declara la ausencia, no un número.
@@ -0,0 +1,55 @@
1
+ # Topología — docker-swarm
2
+
3
+ > meta: artefacto · RFC-006 · generado arch-structure · anclado a `15bcd21` · cobertura: dependencias runtime (`.gemspec` + `Gemfile.lock`) y mapa de contexto de la gema
4
+
5
+ ## 1. Resumen
6
+
7
+ Gema cliente sin servidor propio. Tres dependencias runtime (`activesupport`, `activemodel`, `excon`). Se ubica entre el código Ruby consumidor y el Docker Engine API (vía socket Unix o TCP). No tiene base de datos, colas ni servicios adyacentes propios.
8
+
9
+ ## 2. Cuerpo
10
+
11
+ ### a. Dependencias
12
+
13
+ Runtime declaradas en `docker-swarm.gemspec`; versiones resueltas en `Gemfile.lock`.
14
+
15
+ | nombre | versión (constraint) | resuelta | rol |
16
+ |---|---|---|---|
17
+ | `activesupport` | `>= 6.0` | 8.1.3 | core-ext (`HashWithIndifferentAccess`, `deep_merge`, `blank?`, `demodulize`, `pluralize`) |
18
+ | `activemodel` | `>= 6.0` | 8.1.3 | `ActiveModel::Model` (validaciones, API de atributos) en `Base` |
19
+ | `excon` | `>= 0.80` | 1.5.0 | cliente HTTP con soporte nativo de Unix socket + stack de middlewares |
20
+
21
+ Desarrollo / test (no se empaquetan): `rake ~> 13.0`, `rspec ~> 3.0`, `pry`, `rubocop-rails-omakase`.
22
+
23
+ ### b. Grafo de contexto
24
+
25
+ ```mermaid
26
+ flowchart LR
27
+ Consumer["App Ruby consumidora"] -->|usa modelos| Gem["docker-swarm (gema)"]
28
+ Gem -->|ActiveModel / core-ext| AS["activesupport + activemodel"]
29
+ Gem -->|HTTP via Excon| Daemon["Docker Engine API (dockerd)"]
30
+ Daemon -.->|unix:///var/run/docker.sock o TCP| Gem
31
+ ```
32
+
33
+ La gema es el centro: arriba la consume una app Ruby; abajo habla con el daemon Docker por Excon. `activesupport`/`activemodel` son librerías embebidas, no servicios.
34
+
35
+ ### c. Modos de ejecución
36
+
37
+ No aplica: es una librería embebida en el proceso del consumidor (sin web/worker/cron propios). El único "modo" es el transporte hacia el daemon:
38
+
39
+ | transporte | configuración | default |
40
+ |---|---|---|
41
+ | Unix socket | `socket_path = "unix:///var/run/docker.sock"` | sí |
42
+ | TCP | `socket_path = "http://host:2375"` | no |
43
+
44
+ ## 3. Inferencias
45
+
46
+ | afirmación | confidence | a verificar |
47
+ |---|---|---|
48
+ | `activesupport`/`activemodel` 8.1.3 son las resueltas hoy, pero el constraint `>= 6.0` admite Rails 6/7/8 | declared | `Gemfile.lock` fija 8.1.3; el `.gemspec` no pone techo |
49
+ | La gema no abre puertos ni corre procesos propios | declared | sin `config/`, sin `bin/` server, sin Railtie/Engine |
50
+
51
+ ## 4. Cobertura y fronteras
52
+
53
+ - **Dependencias transitivas:** las de `activesupport` (concurrent-ruby, i18n, tzinfo, etc.) y `excon` (logger) están en `Gemfile.lock` pero no son edges del repo → fuera del grafo de contexto.
54
+ - **El daemon Docker** es la única dependencia externa de runtime real; su contrato consumido se detalla en [`docs/consumed/docker-engine-api.md`](../consumed/docker-engine-api.md).
55
+ - **Arquitectura upstream** (cómo está desplegado el cluster Swarm que la gema administra) es del operador, no del repo → fuera de alcance.
@@ -66,19 +66,23 @@ module DockerSwarm
66
66
  images: {
67
67
  index: { method: :get, path: "images/json" },
68
68
  show: { method: :get, path: "images/%<id>s/json" },
69
- create: { method: :post, path: "images/create?fromImage=%<id>s" },
69
+ pull: { method: :post, path: "images/create" },
70
70
  destroy: { method: :delete, path: "images/%<id>s" }
71
71
  }
72
72
  }.freeze
73
73
 
74
- def self.request(action:, arguments: {}, query_params: {}, payload: nil)
74
+ def self.request(action:, arguments: {}, query_params: {}, payload: nil, headers: {})
75
75
  path = format(action[:path], arguments)
76
- DockerSwarm.request(
76
+ options = {
77
77
  method: action[:method],
78
78
  path: path,
79
79
  query: query_params,
80
80
  body: payload
81
- )
81
+ }
82
+ # Solo forwardeamos headers cuando hay: sin ellos, la request queda
83
+ # idéntica a la actual (no pisamos los headers por defecto de Excon).
84
+ options[:headers] = headers if headers && !headers.empty?
85
+ DockerSwarm.request(**options)
82
86
  end
83
87
  end
84
88
  end
@@ -6,26 +6,52 @@ module DockerSwarm
6
6
  extend ActiveSupport::Concern
7
7
 
8
8
  class_methods do
9
- def create(attributes = {})
10
- resource = new(attributes)
11
- resource.save
9
+ # @param attributes [Hash] atributos Docker (hash posicional braceado)
10
+ # @param opts [Hash] keywords: atributos sueltos históricos + opción reservada
11
+ # +registry_auth+ (credencial para X-Registry-Auth). El resto se pliega como atributos.
12
+ def create(attributes = {}, **opts)
13
+ registry_auth = opts.delete(:registry_auth)
14
+ resource = new(attributes.merge(opts))
15
+ resource.save(registry_auth: registry_auth)
12
16
  resource
13
17
  end
18
+
19
+ # Atributos que el Engine toma por **query string** en el +create+, no en el body.
20
+ # Un atributo declarado acá viaja en la URL y se excluye del payload: mandarlo en
21
+ # el body no le da error a Docker, lo **descarta en silencio**.
22
+ # Override en el modelo que lo necesite (p. ej. +name+ en {DockerSwarm::Container}).
23
+ # @return [Array<String>] nombres de atributo
24
+ def create_query_params
25
+ [].freeze
26
+ end
14
27
  end
15
28
 
16
- def save
29
+ # @param registry_auth [String, nil] credencial opaca para el header X-Registry-Auth
30
+ def save(registry_auth: nil)
17
31
  return false unless valid?
18
- return update if persisted?
32
+ return update(registry_auth: registry_auth) if persisted?
19
33
 
34
+ headers, = RegistryAuth.resolve(registry_auth: registry_auth)
20
35
  response = Api.request(
21
36
  action: self.class.routes[:create],
22
- payload: payload_for_docker
37
+ query_params: query_params_for_docker,
38
+ payload: payload_for_docker.except(*self.class.create_query_params),
39
+ headers: headers
23
40
  )
24
41
 
25
42
  self.ID = response["ID"] || response["Id"] || response["Name"]
26
43
  reload
27
44
  true
28
45
  end
46
+
47
+ # Los +create_query_params+ que este recurso tiene seteados, listos para la URL.
48
+ # @return [Hash{Symbol => Object}] vacío si el modelo no declara ninguno
49
+ def query_params_for_docker
50
+ keys = self.class.create_query_params
51
+ return {} if keys.empty?
52
+
53
+ attributes.slice(*keys).compact.symbolize_keys
54
+ end
29
55
  end
30
56
  end
31
57
  end
@@ -5,16 +5,27 @@ module DockerSwarm
5
5
  module Updatable
6
6
  extend ActiveSupport::Concern
7
7
 
8
- def update(new_attributes = {})
8
+ # @param new_attributes [Hash] atributos Docker (hash posicional braceado)
9
+ # @param opts [Hash] keywords: atributos sueltos históricos + opciones reservadas
10
+ # +registry_auth+ (header X-Registry-Auth) / +registry_auth_from+ (query registryAuthFrom,
11
+ # excluyente con registry_auth). El resto se pliega como atributos.
12
+ def update(new_attributes = {}, **opts)
13
+ registry_auth = opts.delete(:registry_auth)
14
+ registry_auth_from = opts.delete(:registry_auth_from)
15
+ # Valida (excluyente + enum) y resuelve los canales ANTES de mutar el objeto.
16
+ headers, auth_query = RegistryAuth.resolve(registry_auth: registry_auth, registry_auth_from: registry_auth_from)
17
+
9
18
  current_version = self.Version&.dig("Index")
10
- assign_attributes(new_attributes) if new_attributes.present?
19
+ attributes = new_attributes.merge(opts)
20
+ assign_attributes(attributes) if attributes.present?
11
21
  return false unless valid?
12
22
 
13
23
  Api.request(
14
24
  action: self.class.routes[:update],
15
25
  arguments: { id: self.ID },
16
- query_params: { version: current_version },
17
- payload: payload_for_docker
26
+ query_params: { version: current_version }.merge(auth_query),
27
+ payload: payload_for_docker,
28
+ headers: headers
18
29
  )
19
30
 
20
31
  true