docker-swarm 0.11.0 → 0.12.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 +22 -0
- data/README.md +1 -1
- data/docs/behavior/behavior.md +43 -3
- data/docs/consumed/docker-engine-api.md +5 -1
- data/docs/errors/errors.md +1 -1
- data/docs/glossary/glossary.md +2 -1
- data/docs/interface/interface.md +2 -1
- data/docs/release/release.md +2 -2
- data/docs/test/testing.md +3 -1
- data/lib/docker_swarm/api.rb +4 -1
- data/lib/docker_swarm/models/container.rb +109 -0
- data/lib/docker_swarm/version.rb +1 -1
- data/skill/SKILL.md +2 -2
- metadata +3 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4c0c5309bd00011e9ccffc6887da8771c9dac05d8bcacd08105fa2a83e05fc1a
|
|
4
|
+
data.tar.gz: 3d931bd5d1282db9ff5b8714361176720b09c83c2753f3875caab70194b7cc7b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1a77a34705c2774f62b025cb7091dd2bb3930dfac5b43ef3afec90cf93d37c819666320e30fdd93f600e44d7e91ee8a9bac3e687152ff754d3c79d4363a908ee
|
|
7
|
+
data.tar.gz: d7b1bb712c6a2920b98651eb43a3d885f18bc31c5a0a24ff58adce45b55d3792a9a536e4087e7c4a1dab397558ec35b791cb6773acf8a4f1a2d56a7f3e3c592c
|
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,28 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.12.0] — 2026-08-18
|
|
8
|
+
|
|
9
|
+
### Nuevas funcionalidades
|
|
10
|
+
- **`Container` gana `#update`, `#restart` y `#stats`** (#39). Las tres faltaban, y `Base#method_missing` hacía que la ausencia **no se notara**: devolvía `nil` sin levantar y `respond_to?` decía `true`, así que un consumidor no podía distinguir *"se hizo"* de *"no existe la implementación"*. Aguas abajo eso salía como `200 OK` con cuerpo nulo (`sequre/box_cluster_manager#43`) — @gedera
|
|
11
|
+
- **`#stats` fuerza `stream: false`, y eso es lo que hace que el método vuelva.** El endpoint streamea por default: medido contra un Engine **29.7.2**, sin ese parámetro emite un objeto por segundo y **la conexión no cierra**. En una llamada RPC cuelga, y el modo de falla no es un error sino una espera. El parámetro se **mergea** en vez de reemplazarse —a diferencia de `Loggable#logs`—, así que un caller que pasa su propio hash cambia qué mide, no se queda esperando; `stats(stream: true)` sigue disponible a propósito.
|
|
12
|
+
- **`#restart(timeout: nil)`** pega al endpoint real. **No** copia a `Service#restart`, que simula el reinicio incrementando `ForceUpdate` *porque los services de Swarm no tienen endpoint de restart*; los containers sí. Sin `timeout` no se manda `?t=`, que **no** es lo mismo que mandar `0`.
|
|
13
|
+
- **`#update` NO usa `Concerns::Updatable`.** Ese concern manda `?version=<Version.Index>` para la concurrencia optimista de *services*; el update de un container es otro endpoint —límites de recursos— que no tiene ese parámetro y lo **ignoraría en silencio**. Devuelve el **cuerpo** del Engine (`Warnings`), no un booleano: ahí avisa cuando un límite no se pudo aplicar. Y hace `reload` en vez de `assign_attributes`, porque el payload es plano (`Memory`) y el objeto lo tiene anidado (`HostConfig.Memory`) — asignarlo crearía un atributo fantasma.
|
|
14
|
+
- `Api::ENDPOINTS[:containers]` pasa de 7 a 10 rutas.
|
|
15
|
+
|
|
16
|
+
### Cambios de comportamiento
|
|
17
|
+
- **`save` sobre un `Container` ya persistido ahora levanta `ArgumentError`; antes devolvía `nil`** (#39). `Concerns::Creatable#save` llama `update(registry_auth:)` **sin atributos** cuando el objeto está persistido, así que el payload quedaba vacío — y el Engine **acepta el body vacío respondiendo `200 OK` sin aplicar nada** (medido). O sea que `c.Memory = X; c.save` perdía el cambio sin un solo error. **El `save` genérico no se soporta a propósito:** `POST /containers/{id}/update` no es *"guardar el objeto"*, y derivar el payload de los atributos locales exigiría una whitelist de campos que driftea contra la API. El mensaje del error redirige a `update("Memory" => …)`.
|
|
18
|
+
- **Alcance verificado antes de tomar la decisión:** ningún consumidor del fleet ni ningún spec de la gema llama `.save` sobre un container. Para el resto de los modelos no cambia nada — @gedera
|
|
19
|
+
|
|
20
|
+
### Seguridad
|
|
21
|
+
- **`Container#update` descarta `registry_auth`/`registry_auth_from` sobre el hash ya mergeado**, no sólo sobre los kwargs (#39). Una credencial pasada en el hash **posicional** habría viajado en el payload al Engine y aparecido en el log de `request_success` (`body=…`). Es la misma clase de fuga que corrigió #24 por el otro lado, en el camino nuevo — @gedera
|
|
22
|
+
|
|
23
|
+
### Documentación
|
|
24
|
+
- Seis capas movidas con la superficie nueva: `interface`, `consumed` (3 endpoints), `test`, `glossary`, `behavior` (**flujo 3.13**: el payload vacío que el Engine acepta, con la rama `save` → `ArgumentError`) y `errors` §4 (el tercer sitio que eleva `ArgumentError`). Los compuestos (`README.md`, `skill/SKILL.md`, el stanza de `AGENTS.md`) reindexados, con el conteo de flujos **12 → 13** — @gedera
|
|
25
|
+
- `docs/consumed/` §c dejaba de ser cierta como generalización: la frase del `?version=` se escribió para `Service#update` y ahora hay un `update` que **no** lo lleva. Sujeto calificado — @gedera
|
|
26
|
+
- `docs/release/` §a: el campo `versión actual` venía **stale en `0.10.0`** con la gema ya en `0.11.0`. Corregido, y declarado que a ese campo lo invalida **todo** release por construcción — @gedera
|
|
27
|
+
|
|
28
|
+
|
|
7
29
|
## [0.11.0] — 2026-08-07
|
|
8
30
|
|
|
9
31
|
### Correcciones
|
data/README.md
CHANGED
|
@@ -43,7 +43,7 @@ Documentación normada (RFC-001) por capa:
|
|
|
43
43
|
|---|---|---|
|
|
44
44
|
| Datos | — | `n/a` (gema sin DB) |
|
|
45
45
|
| Glosario | [`docs/glossary/glossary.md`](docs/glossary/glossary.md) | completo (primitivas + arquitectura interna) |
|
|
46
|
-
| Comportamiento | [`docs/behavior/behavior.md`](docs/behavior/behavior.md) | backfill on-demand + incremental (
|
|
46
|
+
| Comportamiento | [`docs/behavior/behavior.md`](docs/behavior/behavior.md) | backfill on-demand + incremental (13 flujos) |
|
|
47
47
|
| Configuración | [`docs/config/configuracion.md`](docs/config/configuracion.md) | inventario base (7 opciones, sin env vars) |
|
|
48
48
|
| Interfaz | [`docs/interface/interface.md`](docs/interface/interface.md) | API Ruby pública (11 modelos + Base + concerns) |
|
|
49
49
|
| Topología | [`docs/topology/topology.md`](docs/topology/topology.md) | 3 deps runtime + grafo de contexto |
|
data/docs/behavior/behavior.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Comportamiento — docker-swarm
|
|
2
2
|
|
|
3
|
-
> meta: artefacto · RFC-007 · generado dev-enrich · anclado a `v0.10.0` · cobertura:
|
|
3
|
+
> meta: artefacto · RFC-007 · generado dev-enrich · anclado a `v0.10.0` · cobertura: 13 flujos load-bearing (8 backfill inicial + 5 nuevos: auth de registry privado, `Image.pull` síncrono, `Container.create`, partición query params/filters del listado, `Container#update`/`#stats`) · refresh #39 (flujo 3.13: el payload vacío que el Engine acepta con 200, y el cuelgue de `stats`; el ancla sigue en `v0.10.0` — el re-anclaje va con la release 0.12.0, #40)
|
|
4
4
|
|
|
5
5
|
## 1. Resumen
|
|
6
6
|
|
|
@@ -8,7 +8,7 @@ Flujos de ejecución load-bearing de `docker-swarm`: cómo se materializan en ru
|
|
|
8
8
|
|
|
9
9
|
## 2. Cobertura declarada
|
|
10
10
|
|
|
11
|
-
### Documentados (
|
|
11
|
+
### Documentados (13)
|
|
12
12
|
|
|
13
13
|
1. `Service.create` + reload
|
|
14
14
|
2. `Service.update` con `Version.Index`
|
|
@@ -22,6 +22,7 @@ Flujos de ejecución load-bearing de `docker-swarm`: cómo se materializan en ru
|
|
|
22
22
|
10. `Image.pull` síncrono (stream NDJSON → error tipado → resultado explícito)
|
|
23
23
|
11. `Container.create` con nombre por query string (`create_query_params`)
|
|
24
24
|
12. `Model.where` — partición query params propios vs. `?filters=` (`index_query_params`)
|
|
25
|
+
13. `Container#update` — el payload vacío que el Engine acepta (y `#stats`, que cuelga en vez de fallar)
|
|
25
26
|
|
|
26
27
|
### No documentados (ausencia ≠ inexistencia, RFC-007)
|
|
27
28
|
|
|
@@ -355,9 +356,48 @@ flowchart TD
|
|
|
355
356
|
- **Las claves se matchean como símbolos.** `where("status" => true)` cae en `docker_filters` (`slice(:status)` no matchea la string). Comportamiento preexistente y común a todos los query params del listado, no introducido por `:status`.
|
|
356
357
|
- Sin filtros no hay query: `all` manda `query_params: {}` — el listado por default no cambió.
|
|
357
358
|
|
|
359
|
+
### 3.13 `Container#update` — el payload vacío que el Engine acepta
|
|
360
|
+
|
|
361
|
+
`POST /containers/{id}/update` **no es "guardar el objeto"**: es un endpoint angosto de límites de recursos, y **acepta un body vacío respondiendo `200 OK` sin aplicar nada** (medido contra Engine 29.7.2: body `{}` → `{"Warnings":null}`, `Memory` sin cambiar).
|
|
362
|
+
|
|
363
|
+
Eso importa porque `Concerns::Creatable#save` **llama a `update`** cuando el objeto ya está persistido, y sin atributos. La cadena completa deja el payload en `{}`, así que un `save` se llevaría los cambios locales sin un solo error — el mismo modo de falla silencioso de §3.11, por la otra puerta.
|
|
364
|
+
|
|
365
|
+
```mermaid
|
|
366
|
+
sequenceDiagram
|
|
367
|
+
actor Caller
|
|
368
|
+
participant Container as DockerSwarm::Container
|
|
369
|
+
participant Api
|
|
370
|
+
participant Docker
|
|
371
|
+
|
|
372
|
+
alt save sobre un objeto persistido
|
|
373
|
+
Caller->>Container: c.Memory = 128.MB; c.save
|
|
374
|
+
Container->>Container: Creatable#save → persisted? → update(registry_auth: nil)
|
|
375
|
+
Container->>Container: {}.merge({registry_auth: nil}).except(:registry_auth, …) → {}
|
|
376
|
+
Container-->>Caller: ArgumentError ("requires at least one attribute")
|
|
377
|
+
Note over Container,Docker: NO se emite request: el 200 no-op nunca ocurre
|
|
378
|
+
else update con límites explícitos
|
|
379
|
+
Caller->>Container: c.update("Memory" => 134217728)
|
|
380
|
+
Container->>Container: merge + except → payload no vacío
|
|
381
|
+
Note over Container,Api: sin ?version= — eso es de Service#update
|
|
382
|
+
Container->>Api: request(:update, payload:)
|
|
383
|
+
Api->>Docker: POST /containers/{id}/update
|
|
384
|
+
Docker-->>Api: 200 { Warnings: … }
|
|
385
|
+
Api-->>Container: { Warnings: … }
|
|
386
|
+
Container->>Container: reload
|
|
387
|
+
Container-->>Caller: { Warnings: … }
|
|
388
|
+
end
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
**Notas load-bearing:**
|
|
392
|
+
- **No usa `Concerns::Updatable`.** Ese concern manda `?version=<Version.Index>` para la concurrencia optimista de *services*; el update de un container no tiene ese parámetro y el Engine lo **ignora en silencio**. Ver la calificación del sujeto en `docs/consumed/docker-engine-api.md` §c.
|
|
393
|
+
- **Devuelve el cuerpo, no un booleano.** El Engine avisa por `Warnings` cuando un límite no se pudo aplicar; colapsarlo a `true` se comería la señal. Es una asimetría deliberada con `#start`/`#stop` (§3.7), que sí devuelven `true`.
|
|
394
|
+
- **`reload`, y no `assign_attributes`** como hace `Updatable`: el payload es **plano** (`Memory`) y el objeto lo tiene **anidado** (`HostConfig.Memory`), así que asignarlo crearía un atributo fantasma en vez de actualizar el real.
|
|
395
|
+
- **El `except` va sobre el merge**, no sólo sobre los kwargs: una credencial pasada en el hash **posicional** llegaría al payload y de ahí al log (`body=…`).
|
|
396
|
+
- **`#stats` comparte la forma del problema**, con otra cara: su default (`stream=true`) no falla, **cuelga** — la conexión queda abierta emitiendo un objeto por segundo. Por eso el método fuerza `stream: false` **mergeando** (no reemplazando como `Loggable#logs` en §3.8): un caller que pasa su propio hash cambia qué mide, no se queda esperando para siempre.
|
|
397
|
+
|
|
358
398
|
## 4. Cobertura y fronteras
|
|
359
399
|
|
|
360
|
-
- **Cobertura (RFC-007 backfill on-demand + incremental):** 8 flujos load-bearing en el backfill inicial + 2 agregados con el soporte de auth de registry privado (auth de registry privado, `Image.pull` síncrono) + 1 con el `create` de containers + 1 con la partición query params/filters del listado =
|
|
400
|
+
- **Cobertura (RFC-007 backfill on-demand + incremental):** 8 flujos load-bearing en el backfill inicial + 2 agregados con el soporte de auth de registry privado (auth de registry privado, `Image.pull` síncrono) + 1 con el `create` de containers + 1 con la partición query params/filters del listado + 1 con `Container#update`/`#stats` (#39) = 13. Esta gema es chica; el backfill completo era factible y se hizo, y a partir de ahí se acreta por PR.
|
|
361
401
|
- **Frontera con glosario:** términos (Service, Spec, Version.Index, etc.) viven en [`docs/glossary/glossary.md`](../glossary/glossary.md). Esta capa documenta secuencias, no significado.
|
|
362
402
|
- **Frontera con configuración:** `DockerSwarm.configure` es boot, no flujo de negocio. No se diagrama.
|
|
363
403
|
- **No localizable / fuera de alcance:**
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# Dependencias consumidas — docker-swarm
|
|
2
2
|
|
|
3
3
|
> meta: artefacto · RFC-018 · generado arch-structure + enriquecido arch-enrich · anclado a `v0.10.0` · cobertura: superficie del Docker Engine API consumida por la gema (`api.rb` ENDPOINTS + `connection.rb`); §c/§e enriquecidas 1/1
|
|
4
|
+
> · refresh #39 (3 endpoints nuevos de containers: `update`/`restart`/`stats`; el ancla sigue en `v0.10.0` — el re-anclaje va con la release 0.12.0, #40)
|
|
4
5
|
|
|
5
6
|
## 1. Resumen
|
|
6
7
|
|
|
@@ -54,6 +55,9 @@ Subset que la gema invoca, derivado de `Api::ENDPOINTS` (`api.rb:5-72`). `destin
|
|
|
54
55
|
| containers | start / stop | `POST containers/%<id>s/start`, `POST containers/%<id>s/stop` | — / — |
|
|
55
56
|
| containers | destroy | `DELETE containers/%<id>s` | — / — |
|
|
56
57
|
| containers | logs | `GET containers/%<id>s/logs` | `?stdout/stderr/...` / stream multiplexado (demux en el cliente) |
|
|
58
|
+
| containers | update | `POST containers/%<id>s/update` | payload de límites de recursos, **sin `?version=`** (eso es de services) / `{Warnings}` |
|
|
59
|
+
| containers | restart | `POST containers/%<id>s/restart` | `?t=` opcional (segundos antes de matar) / — |
|
|
60
|
+
| containers | stats | `GET containers/%<id>s/stats` | **`?stream=false` por default** (pisable explícitamente con `stats(stream: true)`; con el default del Engine la llamada **no vuelve**) / Hash de métricas |
|
|
57
61
|
| images | index | `GET images/json` | — / array |
|
|
58
62
|
| images | show | `GET images/%<id>s/json` | — / Hash |
|
|
59
63
|
| images | pull | `POST images/create?fromImage=<ref>` | header `X-Registry-Auth` (opcional, registry privado) / stream NDJSON de progreso |
|
|
@@ -123,7 +127,7 @@ El daemon responde con status HTTP; `Middleware::ErrorHandler` los mapea a la je
|
|
|
123
127
|
|
|
124
128
|
- `GET`/`DELETE`/`PUT` son seguros de reintentar: re-listar, re-borrar (404 → `nil` graceful) o re-actualizar produce el mismo estado final → la gema los reintenta automáticamente ante caída de socket.
|
|
125
129
|
- `POST create` (services/networks/volumes/configs/secrets/containers) **no** se reintenta: un replay tras fallo parcial podría crear un recurso duplicado (el daemon no deduplica por nombre en todos los recursos). El caller decide qué hacer si un `create` falla por `Communication`.
|
|
126
|
-
- `POST update`/`start`/`stop`/`restart` tampoco se reintentan (son POST)
|
|
130
|
+
- `POST update`/`start`/`stop`/`restart` tampoco se reintentan (son POST). ⚠️ **El `?version=` es de `Service#update`, NO de containers**: ahí un replay con versión vieja daría 409 `Conflict` en vez de un duplicado. `Container#update` **no lo lleva** (#39) — es otro endpoint, de límites de recursos: un replay re-aplica el mismo límite, así que es idempotente en efecto. `GET stats` cae bajo la regla genérica de arriba (`IDEMPOTENT_METHODS`).
|
|
127
131
|
- **Sin backoff** es aceptable acá: el socket Unix local rara vez está transitoriamente saturado; ante un daemon caído, 3 reintentos inmediatos fallan rápido y se propaga `Communication`.
|
|
128
132
|
|
|
129
133
|
#### e. Degradación (si la dependencia cae)
|
data/docs/errors/errors.md
CHANGED
|
@@ -100,4 +100,4 @@ No aplica un shape propio tipo RFC 7807: la gema es un cliente, no un servidor.
|
|
|
100
100
|
- **Solo errores públicos:** estas excepciones cruzan la frontera de la gema hacia el consumidor. No hay excepciones internas rescatadas-y-tragadas que documentar (salvo `JSON::ParserError` en `ResponseJSONParser`, que se traga y retorna el body crudo — interno, no contrato).
|
|
101
101
|
- **Frontera con consumed (RFC-018):** este catálogo = lo que la gema **emite**. El mapeo "error del proveedor Docker → excepción nuestra" lo referencia [`docs/consumed/docker-engine-api.md`](../consumed/docker-engine-api.md) §d, que apunta acá.
|
|
102
102
|
- **Política §c:** enriquecida (recomendación al consumidor); el matiz por-status lo confirma el humano contra el comportamiento real del daemon (ver §3).
|
|
103
|
-
- **Validación de input del caller (`ArgumentError`, stdlib):** `RegistryAuth.resolve`/`validate!` (exclusión mutua `registry_auth`/`registry_auth_from` + enum del `from`)
|
|
103
|
+
- **Validación de input del caller (`ArgumentError`, stdlib):** `RegistryAuth.resolve`/`validate!` (exclusión mutua `registry_auth`/`registry_auth_from` + enum del `from`), `Base#assign_attributes` (no-Hash) y `Container#update` (payload vacío tras descartar los `registry_auth`: el Engine lo aceptaría con `200 OK` sin aplicar nada, #39) elevan `ArgumentError` ante input inválido del caller — fail-fast, antes de tocar el daemon. Es contrato público de esas firmas (documentado en [`docs/interface/interface.md`](../interface/interface.md)), **no** parte de la jerarquía `DockerSwarm::Error` → por eso no está en §a/§c.
|
data/docs/glossary/glossary.md
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# Glosario — docker-swarm
|
|
2
2
|
|
|
3
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
|
+
> · refresh #39 (la superficie que expone la gema para Container; el ancla sigue en `v0.9.0` — el re-anclaje va con la release 0.12.0, #40)
|
|
4
5
|
|
|
5
6
|
## 1. Resumen
|
|
6
7
|
|
|
@@ -25,7 +26,7 @@ Unidad de ejecución de un Service en un Node específico. Read-only: las tasks
|
|
|
25
26
|
|
|
26
27
|
## Container
|
|
27
28
|
|
|
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
|
+
Container Docker standalone (no Swarm). La gema expone create/start/stop/restart/stats/update/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
30
|
**Binding:** [`DockerSwarm::Container`](../../lib/docker_swarm/models/container.rb)
|
|
30
31
|
|
|
31
32
|
## Image
|
data/docs/interface/interface.md
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# Interfaz — docker-swarm
|
|
2
2
|
|
|
3
3
|
> meta: artefacto · RFC-004 · generado arch-structure · anclado a `v0.10.0` · cobertura: API Ruby pública de la gema (`lib/docker_swarm/**`); símbolos internos marcados en §4
|
|
4
|
+
> · refresh #39 (superficie de `Container`: `#restart`/`#stats`/`#update`; el ancla sigue en `v0.10.0` — el re-anclaje va con la release 0.12.0, #40)
|
|
4
5
|
|
|
5
6
|
## 1. Resumen
|
|
6
7
|
|
|
@@ -80,7 +81,7 @@ Inventario completo de opciones: [`docs/config/configuracion.md`](../config/conf
|
|
|
80
81
|
| `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; `.index_query_params` agrega `:status` → `where(status: true)` puebla `ServiceStatus` (`RunningTasks`/`DesiredTasks`/`CompletedTasks`), único lugar donde el Engine publica el deseado de un service `global` |
|
|
81
82
|
| `DockerSwarm::Node` | clase < Base | Updatable, Deletable (sin `create`: los nodos se unen fuera de la gema) |
|
|
82
83
|
| `DockerSwarm::Task` | clase < Base | Loggable (read-only; generadas por el orquestador) |
|
|
83
|
-
| `DockerSwarm::Container` | clase < Base | Creatable, Deletable, Loggable; `#start`, `#stop
|
|
84
|
+
| `DockerSwarm::Container` | clase < Base | Creatable, Deletable, Loggable; `#start`, `#stop`, `#restart(timeout: nil)`, `#stats(query_params = {})`, `#update(attrs = {}, **opts)` (#39). **`#update` NO usa `Concerns::Updatable`**: ese concern manda `?version=` para la concurrencia optimista de *services*, y el update de un container es otro endpoint (límites de recursos) sin ese param. Devuelve el cuerpo del Engine (`Warnings`), **no** un booleano, y `reload`ea; **levanta `ArgumentError` con payload vacío**, así que `save` sobre un container persistido falla fuerte en vez de postear `{}` y perder los cambios en silencio. **`#stats` fuerza `stream: false`** — con el default el endpoint streamea y la llamada nunca vuelve; `.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; `.index_query_params == %i[all limit size]` — `since`/`before` son **filtros** de `/containers/json`, no query params (#35) |
|
|
84
85
|
| `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); `.index_query_params == %i[all digests]` — `since`/`before` son **filtros** de `/images/json`, no query params (#35) |
|
|
85
86
|
| `DockerSwarm::Network` | clase < Base | Creatable, Updatable, Deletable |
|
|
86
87
|
| `DockerSwarm::Volume` | clase < Base | Creatable, Deletable; `.root_key = "Volumes"` (respuesta wrapped) |
|
data/docs/release/release.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Release — docker-swarm
|
|
2
2
|
|
|
3
|
-
> meta: artefacto · RFC-014 · generado arch-structure + enriquecido arch-enrich · anclado a `v0.10.0` · cobertura: §a estructura completa (versión · changelog · build-trigger · patrón); §b enrich completa (deploy · rollback · ambientes · dueño)
|
|
3
|
+
> meta: artefacto · RFC-014 · generado arch-structure + enriquecido arch-enrich · anclado a `v0.10.0` · cobertura: §a estructura completa (versión · changelog · build-trigger · patrón); §b enrich completa (deploy · rollback · ambientes · dueño) · refresh v0.12.0 (#40 — el campo `versión actual` venía **stale en `0.10.0`** con la gema ya en `0.11.0`: el release anterior no lo movió. Corregido acá, y de paso queda dicho que a este campo lo invalida **todo** release por construcción, no sólo los que tocan esta capa)
|
|
4
4
|
|
|
5
5
|
## 1. Resumen
|
|
6
6
|
|
|
@@ -12,7 +12,7 @@ Gema Ruby publicada en RubyGems. Release por **tag `v*`**: el push del tag dispa
|
|
|
12
12
|
|
|
13
13
|
| campo | valor | fuente |
|
|
14
14
|
|---|---|---|
|
|
15
|
-
| versión actual | `0.
|
|
15
|
+
| versión actual | `0.12.0` | `lib/docker_swarm/version.rb` (`DockerSwarm::VERSION`) |
|
|
16
16
|
| esquema de versión | SemVer (`MAJOR.MINOR.PATCH`) | `CHANGELOG.md` (breaking/mejoras/correcciones por bump) |
|
|
17
17
|
| artefacto liberado | gema `docker-swarm` a RubyGems | `docker-swarm.gemspec` (`spec.name`), `.github/workflows/release.yml` |
|
|
18
18
|
| changelog | `CHANGELOG.md`, formato Keep a Changelog, entradas fechadas por versión | `CHANGELOG.md` |
|
data/docs/test/testing.md
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# Test — docker-swarm
|
|
2
2
|
|
|
3
3
|
> meta: artefacto · RFC-013 · generado arch-structure + enriquecido arch-enrich · anclado a `v0.10.0` · cobertura: estructura de la suite (`spec/`, `.github/workflows/main.yml`); §e enriquecida, §f enriquecida, §g `unknown` (sin incidentes registrados), §h enriquecida
|
|
4
|
+
> · refresh #39 (cobertura de `update`/`restart`/`stats`, unit + integration; el ancla sigue en `v0.10.0` — el re-anclaje va con la release 0.12.0, #40)
|
|
4
5
|
|
|
5
6
|
## 1. Resumen
|
|
6
7
|
|
|
@@ -50,11 +51,12 @@ Ninguna. No hay `SimpleCov`/`.simplecov` ni umbral declarado en el repo (verific
|
|
|
50
51
|
|
|
51
52
|
**Cubierto (unit, con mocks):**
|
|
52
53
|
- 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
|
+
- Modelos con métodos propios: `service` (incl. lógica de update/version), `node`, `task`, `container` (start/stop/restart/stats/update), `network`, `base` (accessors dinámicos, `assign_attributes`/`Spec` merge).
|
|
54
55
|
- 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
56
|
- `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
57
|
- 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
58
|
- Partición query params propios vs. `?filters=` del listado (`index_query_params`): `base_spec` (default, override, partición mixta), `service_spec` (`status: true` → query param; `ServiceStatus` expuesto y tolerancia a su ausencia), `container_spec` (**regresión**: `status` sigue viajando como filtro; y #35: los tres query params de `ContainerList`, `since` ruteado a filtros, `size` a la URL, `force` ausente), `image_spec` (#35: `%i[all digests]`, `since` a filtros, `digests` a la URL). Integration: `services_spec` verifica contra el daemon que `ServiceStatus` aparece **solo** con `status: true`, y un `context` en modo **`global`** pinnea el caso que justifica la feature — `DesiredTasks` legible donde `Spec.Mode.Replicated` no existe. Ese context es el que vuelve necesario el poll del helper `listed_with_status`: en un global el deseado arranca en 0 y el Engine lo completa después (~1s), así que la condición de corte es `DesiredTasks.positive?`, no `ServiceStatus.present?`.
|
|
59
|
+
- Superficie de `Container` que faltaba (#39): `container_spec` cubre `restart` (pega al endpoint real, **no** simula con `ForceUpdate` como `Service`; `t` sólo si le pasan `timeout`), `stats` (**fuerza `stream: false`** — si ese ejemplo se cae, el método deja de volver; y **mergea** en vez de reemplazar, para que un caller no se cuelgue sin querer) y `update` (sin `?version=`; devuelve el cuerpo y no un booleano; `reload`ea; **levanta con payload vacío**, que es lo que hace que `save` sobre un persistido falle fuerte en vez de postear `{}`; descarta `registry_auth` tanto como kwarg como clave String del hash posicional). Integration: `containers_spec` corre los tres contra el daemon — **`stats` bajo `Timeout.timeout(15)`, que es lo único que un unitario no puede cubrir** (mockeando `Api.request` se verifica que mandamos `stream: false`, no que eso evite el cuelgue), `restart` comparando `StartedAt`, y `update` verificando el límite en el objeto local **y** en el Engine.
|
|
58
60
|
- Infra de transporte: `api_spec`, `connection_spec`, `configuration_spec`, `log_helper_spec`, los 4 middleware specs.
|
|
59
61
|
- `swarm`, `system` (singletons): `swarm_spec`, `system_spec`.
|
|
60
62
|
|
data/lib/docker_swarm/api.rb
CHANGED
|
@@ -61,7 +61,10 @@ module DockerSwarm
|
|
|
61
61
|
start: { method: :post, path: "containers/%<id>s/start" },
|
|
62
62
|
stop: { method: :post, path: "containers/%<id>s/stop" },
|
|
63
63
|
destroy: { method: :delete, path: "containers/%<id>s" },
|
|
64
|
-
logs: { method: :get, path: "containers/%<id>s/logs" }
|
|
64
|
+
logs: { method: :get, path: "containers/%<id>s/logs" },
|
|
65
|
+
update: { method: :post, path: "containers/%<id>s/update" },
|
|
66
|
+
restart: { method: :post, path: "containers/%<id>s/restart" },
|
|
67
|
+
stats: { method: :get, path: "containers/%<id>s/stats" }
|
|
65
68
|
},
|
|
66
69
|
images: {
|
|
67
70
|
index: { method: :get, path: "images/json" },
|
|
@@ -45,5 +45,114 @@ module DockerSwarm
|
|
|
45
45
|
Api.request(action: self.class.routes[:stop], arguments: { id: self.ID })
|
|
46
46
|
true
|
|
47
47
|
end
|
|
48
|
+
|
|
49
|
+
# Actualiza los límites de recursos del container (+POST /containers/{id}/update+).
|
|
50
|
+
#
|
|
51
|
+
# ⚠️ **NO usa {Concerns::Updatable}, y no es un olvido.** Ese concern está escrito para
|
|
52
|
+
# services de Swarm: manda +?version=<Version.Index>+ para el control de concurrencia
|
|
53
|
+
# optimista y serializa con +payload_for_docker+. El update de un **container** no tiene
|
|
54
|
+
# +version+ — es otro endpoint con otra semántica (cpu, memoria, reinicio) — así que
|
|
55
|
+
# incluirlo mandaría un query param que el Engine **ignora en silencio**.
|
|
56
|
+
#
|
|
57
|
+
# 🚦 **La firma absorbe kwargs a propósito.** {Concerns::Creatable#save} llama
|
|
58
|
+
# +update(registry_auth:)+ cuando el objeto ya está persistido; con una firma
|
|
59
|
+
# +update(new_attributes = {})+ ese kwarg se convierte en Hash posicional (Ruby 3) y
|
|
60
|
+
# terminaría **posteado como atributo**: +{"registry_auth": null}+ hacia el Engine, sin
|
|
61
|
+
# error. Los dos de registry se descartan acá porque son cosa de +Service+ (header
|
|
62
|
+
# +X-Registry-Auth+ / query +registryAuthFrom+); un container no autentica en su update.
|
|
63
|
+
#
|
|
64
|
+
# Devuelve el cuerpo de la respuesta —Docker responde +{"Warnings": [...]}+— y **no un
|
|
65
|
+
# booleano**: el Engine avisa por ahí cuando un límite no se pudo aplicar. Colapsarlo a
|
|
66
|
+
# +true+ se comería justamente la señal.
|
|
67
|
+
#
|
|
68
|
+
# 🚦 **Un payload vacío levanta, y por eso `save` sobre un container persistido NO funciona
|
|
69
|
+
# — falla fuerte.** {Concerns::Creatable#save} llama +update(registry_auth:)+ **sin
|
|
70
|
+
# atributos**, así que el payload queda en +{}+ y el Engine responde **+200 OK+ sin aplicar
|
|
71
|
+
# nada** (medido: body +{}+ → +{"Warnings":null}+, +Memory+ sin cambiar). Dejarlo pasar
|
|
72
|
+
# convertiría a +save+ en una promesa vacía: el caller asigna atributos, llama +save+, y
|
|
73
|
+
# los pierde sin un solo error.
|
|
74
|
+
#
|
|
75
|
+
# No se soporta el +save+ genérico a propósito: +POST /containers/{id}/update+ **no es
|
|
76
|
+
# "guardar el objeto"**, es un endpoint angosto de límites de recursos. Derivar el payload
|
|
77
|
+
# de los atributos locales exigiría una whitelist de los campos que Docker acepta, que
|
|
78
|
+
# driftea contra la API. Mejor decirlo que fingirlo.
|
|
79
|
+
#
|
|
80
|
+
# @param new_attributes [Hash] límites de recursos en el shape de Docker (+Memory+,
|
|
81
|
+
# +NanoCpus+, +RestartPolicy+, …)
|
|
82
|
+
# @param opts [Hash] atributos sueltos; +registry_auth+ y +registry_auth_from+ se descartan
|
|
83
|
+
# @raise [ArgumentError] si no queda ningún atributo para mandar
|
|
84
|
+
# @return [Hash] cuerpo de la respuesta del Engine (+Warnings+)
|
|
85
|
+
def update(new_attributes = {}, **opts)
|
|
86
|
+
# El `except` va sobre el MERGE, no sólo sobre `opts`: un caller que pase
|
|
87
|
+
# `{registry_auth: "…", Memory: …}` como Hash **posicional** metería la credencial en el
|
|
88
|
+
# payload, y de ahí al log (`body=…`) — la misma fuga que arregló #24 por el otro lado.
|
|
89
|
+
payload = new_attributes.merge(opts).except(:registry_auth, :registry_auth_from,
|
|
90
|
+
"registry_auth", "registry_auth_from")
|
|
91
|
+
|
|
92
|
+
if payload.empty?
|
|
93
|
+
raise ArgumentError,
|
|
94
|
+
"Container#update requires at least one attribute. An empty payload gets a 200 OK " \
|
|
95
|
+
"from the Engine and applies nothing. Coming from `save`? Containers do not support " \
|
|
96
|
+
"the generic save: use `update(\"Memory\" => ...)` with explicit resource limits."
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
response = Api.request(
|
|
100
|
+
action: self.class.routes[:update],
|
|
101
|
+
arguments: { id: self.ID },
|
|
102
|
+
payload: payload
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
# El estado local queda stale si no se recarga: el payload es **plano** (`Memory`) pero el
|
|
106
|
+
# objeto lo tiene anidado (`HostConfig.Memory`), así que un `assign_attributes` —lo que
|
|
107
|
+
# hace {Concerns::Updatable}— crearía un atributo fantasma en vez de actualizar el real.
|
|
108
|
+
# `reload` trae la forma correcta del Engine; es el mismo cierre que usa `save` al crear.
|
|
109
|
+
reload
|
|
110
|
+
response
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Reinicia el container (+POST /containers/{id}/restart+).
|
|
114
|
+
#
|
|
115
|
+
# ⚠️ **No se parece a {Service#restart}, y está bien.** El hermano simula el restart
|
|
116
|
+
# incrementando +ForceUpdate+ **porque los services de Swarm no tienen endpoint de
|
|
117
|
+
# restart**. Los containers sí lo tienen, así que copiar ese workaround sería arrastrar
|
|
118
|
+
# una vuelta que acá no hace falta.
|
|
119
|
+
#
|
|
120
|
+
# @param timeout [Integer, nil] segundos a esperar antes de matar el proceso (+t+ de
|
|
121
|
+
# Docker). +nil+ deja el default del Engine, que **no** es lo mismo que mandar +0+
|
|
122
|
+
# (eso mataría sin gracia).
|
|
123
|
+
# @return [Boolean] true si el Engine aceptó el reinicio
|
|
124
|
+
def restart(timeout: nil)
|
|
125
|
+
Api.request(
|
|
126
|
+
action: self.class.routes[:restart],
|
|
127
|
+
arguments: { id: self.ID },
|
|
128
|
+
query_params: timeout.nil? ? {} : { t: timeout }
|
|
129
|
+
)
|
|
130
|
+
true
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Métricas de uso del container (+GET /containers/{id}/stats+).
|
|
134
|
+
#
|
|
135
|
+
# 🚦 **`stream: false` NO es un default cómodo: es lo que hace que el método vuelva.**
|
|
136
|
+
# El endpoint streamea por default —+stream=true+— y la conexión queda abierta emitiendo
|
|
137
|
+
# un objeto por segundo. Medido contra un Engine **29.7.2**: sin el parámetro, 6 objetos
|
|
138
|
+
# en 6 s y la conexión **no cierra** (+exit 28+ de curl); con +stream=false+, un objeto y
|
|
139
|
+
# cierra en 1 s. En una llamada RPC el default **cuelga** — y el modo de falla no es un
|
|
140
|
+
# error, es una espera. Mismo patrón que ADR-027: el default de Docker no es el que sirve.
|
|
141
|
+
#
|
|
142
|
+
# Por eso el parámetro se **mergea** en vez de reemplazarse, que es como lo hace
|
|
143
|
+
# {Concerns::Loggable#logs}: ahí un caller que pasa su propio hash sólo cambia qué streams
|
|
144
|
+
# lee; acá lo dejaría colgado. Se puede pisar a propósito (+stats(stream: true)+), pero no
|
|
145
|
+
# por accidente.
|
|
146
|
+
#
|
|
147
|
+
# @param query_params [Hash] parámetros extra (+one-shot+, …). +stream+ va en +false+
|
|
148
|
+
# salvo que se lo pise explícitamente.
|
|
149
|
+
# @return [Hash] snapshot de métricas parseado
|
|
150
|
+
def stats(query_params = {})
|
|
151
|
+
Api.request(
|
|
152
|
+
action: self.class.routes[:stats],
|
|
153
|
+
arguments: { id: self.ID },
|
|
154
|
+
query_params: { stream: false }.merge(query_params)
|
|
155
|
+
)
|
|
156
|
+
end
|
|
48
157
|
end
|
|
49
158
|
end
|
data/lib/docker_swarm/version.rb
CHANGED
data/skill/SKILL.md
CHANGED
|
@@ -20,7 +20,7 @@ triggers:
|
|
|
20
20
|
- "docker-swarm gem"
|
|
21
21
|
- "Docker Engine API desde Ruby"
|
|
22
22
|
- "Service.create / Service.update / Service.restart"
|
|
23
|
-
- "Container.create / Container.start / Container.stop"
|
|
23
|
+
- "Container.create / Container.start / Container.stop / Container.restart / Container.stats / Container.update"
|
|
24
24
|
- "helper container efímero"
|
|
25
25
|
- "logs de un servicio Docker"
|
|
26
26
|
- "Version.Index"
|
|
@@ -61,7 +61,7 @@ Defaults son razonables: en local sin TLS, no necesitás bloque `configure`.
|
|
|
61
61
|
| `Service` | `all(filters)`, `find(id)`, `where(filters)`, `create(attrs)` | `update(attrs)`, `restart`, `destroy`, `logs(query)`, `reload`, `persisted?`, `id` | CRUD completo + force-recreate de tasks; `create`/`update` aceptan `registry_auth:` (+ `update`: `registry_auth_from:`) para auth de registry privado; `where(status: true)` puebla `ServiceStatus` (único deseado legible de un service `global`) |
|
|
62
62
|
| `Node` | `all(filters)`, `find(id)`, `where(filters)` | `update(attrs)`, `destroy` | No `create` (los nodos se unen fuera de la gema) |
|
|
63
63
|
| `Task` | `all(filters)`, `find(id)`, `where(filters)` | `logs(query)`, `reload` | Read-only (generados por orquestador) |
|
|
64
|
-
| `Container` | `all(filters)`, `find(id)`, `where(filters)`, `create(attrs)` | `start`, `stop`, `destroy`, `logs(query)` | `create` manda `name` por query string (`create_query_params`) — en el body Docker lo descarta en silencio |
|
|
64
|
+
| `Container` | `all(filters)`, `find(id)`, `where(filters)`, `create(attrs)` | `start`, `stop`, `restart(timeout:)`, `stats(params)`, `update(attrs)`, `destroy`, `logs(query)` | `create` manda `name` por query string (`create_query_params`) — en el body Docker lo descarta en silencio. **`stats` fuerza `stream: false`**: con el default la llamada no vuelve. **`update` levanta con payload vacío**, así que `save` sobre un container persistido NO funciona: no se soporta el save genérico (#39) |
|
|
65
65
|
| `Image` | `all(filters)`, `find(id)`, `pull(image_reference, registry_auth:)` | `destroy` | **No `create`** (retirado; `Image` ya no es Creatable). `pull` = pull explícito síncrono → `{status, image_ref, digest?}`; **soporta `X-Registry-Auth`** para registries privados |
|
|
66
66
|
| `Network` | `all(filters)`, `find(id)`, `create(attrs)` | `update(attrs)`, `destroy` | CRUD completo |
|
|
67
67
|
| `Volume` | `all(filters)`, `find(id)`, `create(attrs)` | `destroy` | No `update` (Docker no lo soporta). Respuesta wrapped vía `root_key = "Volumes"` |
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: docker-swarm
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.12.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Gabriel
|
|
@@ -136,9 +136,9 @@ licenses:
|
|
|
136
136
|
metadata:
|
|
137
137
|
homepage_uri: https://github.com/sequre/docker-swarm
|
|
138
138
|
source_code_uri: https://github.com/sequre/docker-swarm
|
|
139
|
-
changelog_uri: https://github.com/sequre/docker-swarm/blob/v0.
|
|
139
|
+
changelog_uri: https://github.com/sequre/docker-swarm/blob/v0.12.0/CHANGELOG.md
|
|
140
140
|
bug_tracker_uri: https://github.com/sequre/docker-swarm/issues
|
|
141
|
-
documentation_uri: https://github.com/sequre/docker-swarm/blob/v0.
|
|
141
|
+
documentation_uri: https://github.com/sequre/docker-swarm/blob/v0.12.0/skill/SKILL.md
|
|
142
142
|
rubygems_mfa_required: 'true'
|
|
143
143
|
rdoc_options: []
|
|
144
144
|
require_paths:
|