docker-swarm 0.9.0 → 0.10.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 +10 -0
- data/README.md +1 -1
- data/docs/behavior/behavior.md +31 -3
- data/docs/consumed/docker-engine-api.md +18 -2
- data/docs/interface/interface.md +4 -3
- data/docs/release/release.md +2 -2
- data/docs/test/testing.md +2 -1
- data/lib/docker_swarm/base.rb +21 -2
- data/lib/docker_swarm/models/service.rb +24 -0
- data/lib/docker_swarm/version.rb +1 -1
- data/skill/SKILL.md +8 -1
- 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: 824016bc8286430e75b85e1daa74f5fa30039667230585bcd548ac76afacd1da
|
|
4
|
+
data.tar.gz: c8d56047e0e282aa35dcdd3998780f0bfcd7c5ba1be6464068bff91b99276934
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: bc733a3eb22266fe1ca82dcfbc56445bf0118a8fd9c077cf697849fe15004463571c94248cd48f236d42a67d2ccbb415419e8e1a05096842bb72fec035d63fc9
|
|
7
|
+
data.tar.gz: e65002ee881d436b549918321b415b3acc224679c40690b08651ff48960f3d8c6ec3f00849ffc7830c91a8ab00d834940299e355e4225a814e231abc51a133c5
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [Unreleased]
|
|
6
|
+
|
|
7
|
+
## [0.10.0] — 2026-08-05
|
|
8
|
+
|
|
9
|
+
### Nuevas funcionalidades
|
|
10
|
+
- **`Service.where(status: true)` puebla `ServiceStatus`** (#22). `status` es un query param **propio** de `GET /services` (Engine API ≥ v1.41), no un filtro, y hasta ahora la gema no tenía forma de mandarlo: caía en el JSON de `?filters=`, donde el Engine no lo acepta como filtro de `/services`. Con esto cada elemento del listado trae `ServiceStatus` (`RunningTasks` · `DesiredTasks` · `CompletedTasks`), que es el **único** lugar donde el Engine publica el deseado de un service en modo `global` — un replicado lo expone en `Spec.Mode.Replicated.Replicas`, pero para un global ese campo no existe. Sin `DesiredTasks` un global corriendo en 2 de 3 nodos elegibles es indistinguible de uno sano: solo se detecta el caso extremo de cero tasks — @Pslp
|
|
11
|
+
- La whitelist de query params del listado sale a `Base.index_query_params` (default `%i[all force limit since before]`, **override por modelo**), y `Service` la extiende con `:status`. Se resuelve así, y **no** subiendo `status` a `Base`, porque en `/containers/json` `status` **sí** es un filtro válido (`running`, `exited`, …): globalizarlo lo sacaría del `?filters=` y rompería `Container.where(status: "running")`. Hay un spec de regresión que lo fija.
|
|
12
|
+
- **Compatible hacia atrás:** sin pasar `status` el listado no cambia. Y **degrada en silencio**: la gema no fija `?version=`, así que en un Engine por debajo de v1.41 el parámetro se ignora sin error y `ServiceStatus` llega ausente → el consumidor tiene que tolerar `nil`, no hay señal de "no soportado".
|
|
13
|
+
- **Trampa del consumidor:** en un service `global` recién creado `DesiredTasks` vale **0** — el Engine publica los contadores antes de evaluar los nodos elegibles y lo completa ~1s después (medido contra un Engine `1.54`). En esa ventana `RunningTasks == DesiredTasks == 0`, así que comparar los dos números para decidir salud miente; `DesiredTasks.positive?` va como precondición. Documentado en `docs/consumed/` y `skill/SKILL.md`.
|
|
14
|
+
|
|
5
15
|
## [0.9.0] — 2026-08-03
|
|
6
16
|
|
|
7
17
|
### Nuevas funcionalidades
|
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 (12 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.
|
|
3
|
+
> meta: artefacto · RFC-007 · generado dev-enrich · anclado a `v0.10.0` · cobertura: 12 flujos load-bearing (8 backfill inicial + 4 nuevos: auth de registry privado, `Image.pull` síncrono, `Container.create`, partición query params/filters del listado)
|
|
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 (12)
|
|
12
12
|
|
|
13
13
|
1. `Service.create` + reload
|
|
14
14
|
2. `Service.update` con `Version.Index`
|
|
@@ -21,6 +21,7 @@ Flujos de ejecución load-bearing de `docker-swarm`: cómo se materializan en ru
|
|
|
21
21
|
9. Auth de registry privado (`RegistryAuth.resolve` → `X-Registry-Auth` / `registryAuthFrom`)
|
|
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
|
+
12. `Model.where` — partición query params propios vs. `?filters=` (`index_query_params`)
|
|
24
25
|
|
|
25
26
|
### No documentados (ausencia ≠ inexistencia, RFC-007)
|
|
26
27
|
|
|
@@ -327,9 +328,36 @@ sequenceDiagram
|
|
|
327
328
|
- **El atributo se excluye del payload**, no se duplica: mandarlo en los dos lados no da error pero deja el body con una clave que Docker ignora.
|
|
328
329
|
- Hereda el flujo §3.1: `valid?` antes del POST, `reload` después, y **sin retry** por ser `POST` (§3.5). Si el `create` falla por `Communication`, el caller decide — con nombre determinista puede adoptar el existente en el reintento.
|
|
329
330
|
|
|
331
|
+
### 3.12 `Model.where` — partición query params propios vs. `?filters=`
|
|
332
|
+
|
|
333
|
+
El Engine parte los parámetros de un listado en dos grupos, y la gema tiene que decidir a cuál va cada clave **antes** de armar la URL. `Base.index_query_params` es esa declaración; todo lo que no esté ahí se serializa dentro del JSON de `?filters=`.
|
|
334
|
+
|
|
335
|
+
La trampa: un query param propio que el modelo no declaró **no llega**. Viaja dentro de `filters`, donde el Engine lo rechaza o lo ignora según el recurso — no hay error que diga "esa clave iba en la URL".
|
|
336
|
+
|
|
337
|
+
```mermaid
|
|
338
|
+
flowchart TD
|
|
339
|
+
A["Model.where(status: true, name: 'web')"] --> B["_fetch_all"]
|
|
340
|
+
B --> C{"clave ∈ index_query_params?"}
|
|
341
|
+
C -->|sí| D["query params propios<br/>{ status: true }"]
|
|
342
|
+
C -->|no| E["docker_filters<br/>{ name: 'web' }"]
|
|
343
|
+
E --> F["downcase claves + Array(valor)<br/>→ JSON"]
|
|
344
|
+
D --> G["query = { status: true,<br/>filters: '{\"name\":[\"web\"]}' }"]
|
|
345
|
+
F --> G
|
|
346
|
+
G --> H["GET /services?status=true&filters=..."]
|
|
347
|
+
H --> I["array de Hash → instancias"]
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
**Notas load-bearing:**
|
|
351
|
+
- **La lista es por modelo, no global.** `Base` declara `%i[all force limit since before]`; `Service` la extiende con `:status`. Deliberadamente no se generaliza: `status` es query param propio en `/services` pero **filtro válido** en `/containers/json` (`running`, `exited`, …), así que subirlo a `Base` lo sacaría del `?filters=` donde containers lo necesita. Hay un spec de regresión que lo fija.
|
|
352
|
+
- **`Service.where(status: true)` es lo que habilita leer `ServiceStatus`** (`RunningTasks` · `DesiredTasks` · `CompletedTasks`), única superficie donde el Engine publica el **deseado** de un service en modo `global` — un replicado lo tiene en `Spec.Mode.Replicated.Replicas`, un global no tiene ese campo. Sin eso, un global corriendo en 2 de 3 nodos elegibles es indistinguible de uno sano: se ven las tasks que corren, no contra qué comparar.
|
|
353
|
+
- **Requiere API ≥ v1.41 y degrada en silencio.** La gema no fija `?version=` (§3.5 / [`docs/consumed/`](../consumed/docker-engine-api.md)); en un Engine anterior el parámetro se ignora sin error y `ServiceStatus` llega ausente → el consumidor tolera `nil`, no hay señal de "no soportado".
|
|
354
|
+
- **`DesiredTasks` arranca en 0 en un `global` recién creado** y el Engine lo completa después de evaluar los nodos (~1s, medido). En esa ventana `RunningTasks == DesiredTasks == 0`: el deseado no está calculado, así que comparar los dos números miente. Precondición de salud = `DesiredTasks.positive?`. Detalle en [`docs/consumed/`](../consumed/docker-engine-api.md).
|
|
355
|
+
- **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
|
+
- Sin filtros no hay query: `all` manda `query_params: {}` — el listado por default no cambió.
|
|
357
|
+
|
|
330
358
|
## 4. Cobertura y fronteras
|
|
331
359
|
|
|
332
|
-
- **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 =
|
|
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 = 12. Esta gema es chica; el backfill completo era factible y se hizo, y a partir de ahí se acreta por PR.
|
|
333
361
|
- **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.
|
|
334
362
|
- **Frontera con configuración:** `DockerSwarm.configure` es boot, no flujo de negocio. No se diagrama.
|
|
335
363
|
- **No localizable / fuera de alcance:**
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Dependencias consumidas — docker-swarm
|
|
2
2
|
|
|
3
|
-
> meta: artefacto · RFC-018 · generado arch-structure + enriquecido arch-enrich · anclado a `v0.
|
|
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
4
|
|
|
5
5
|
## 1. Resumen
|
|
6
6
|
|
|
@@ -39,7 +39,7 @@ Subset que la gema invoca, derivado de `Api::ENDPOINTS` (`api.rb:5-72`). `destin
|
|
|
39
39
|
| nodes | destroy | `DELETE nodes/%<id>s` | — / — |
|
|
40
40
|
| tasks | index / show | `GET tasks`, `GET tasks/%<id>s` | `?filters=` / array \| Hash |
|
|
41
41
|
| tasks | logs | `GET tasks/%<id>s/logs` | `?stdout/stderr/...` / stream multiplexado (demux en el cliente) |
|
|
42
|
-
| services | index / show | `GET services`, `GET services/%<id>s` | `?filters=` / array \| Hash |
|
|
42
|
+
| services | index / show | `GET services`, `GET services/%<id>s` | `?filters=` + `?status=` (query propio, **no** filtro; ≥ v1.41) / array \| Hash; con `?status=true` cada elemento trae `ServiceStatus` |
|
|
43
43
|
| services | create | `POST services/create` | payload (Spec aplanado) + header `X-Registry-Auth` (opcional, registry privado) / `{ID}` |
|
|
44
44
|
| services | update | `POST services/%<id>s/update` | `?version=` (+ `?registryAuthFrom=` opcional: `spec`\|`previous-spec`) + payload + header `X-Registry-Auth` (opcional; excluyente con `registryAuthFrom`) / — |
|
|
45
45
|
| services | destroy | `DELETE services/%<id>s` | — / — |
|
|
@@ -61,6 +61,22 @@ Subset que la gema invoca, derivado de `Api::ENDPOINTS` (`api.rb:5-72`). `destin
|
|
|
61
61
|
|
|
62
62
|
Serialización: request body no-String → JSON (`Content-Type: application/json`) vía `Middleware::RequestEncoder`; response `application/json` → `HashWithIndifferentAccess` recursivo vía `Middleware::ResponseJSONParser`.
|
|
63
63
|
|
|
64
|
+
**Query params propios vs. `?filters=` (listados).** El Engine parte los parámetros de un listado en dos: query params propios de la ruta (`?limit=`, `?all=`, `?status=`) y el JSON de `?filters=`. La gema declara los primeros en `Base.index_query_params` (override por modelo); **todo lo demás se serializa como filtro**. Un query param propio no declarado ahí no llega nunca: viaja dentro del JSON de `filters` y el Engine lo rechaza (si valida la clave) o lo ignora.
|
|
65
|
+
|
|
66
|
+
Los nombres **no** son globales — el mismo `status` es query param propio en `/services` y filtro válido en `/containers/json` (`running`, `exited`, …). De ahí que la lista sea por modelo y no una whitelist única.
|
|
67
|
+
|
|
68
|
+
**`?status=true` en `/services`** (API ≥ v1.41) agrega a cada elemento:
|
|
69
|
+
|
|
70
|
+
```json
|
|
71
|
+
{ "ServiceStatus": { "RunningTasks": 2, "DesiredTasks": 3, "CompletedTasks": 0 } }
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Es la **única** superficie donde el Engine publica el deseado de un service en modo `global`: un replicado lo expone en `Spec.Mode.Replicated.Replicas`, pero para un global ese campo no existe (el deseado es "una task por nodo elegible"). Sin `DesiredTasks` no hay número contra el cual comparar las tasks que corren.
|
|
75
|
+
|
|
76
|
+
Como la gema no fija `?version=`, la versión efectiva la decide el host (ver arriba): en un Engine por debajo de v1.41 el parámetro se **ignora sin error** y `ServiceStatus` llega ausente. El consumidor tiene que tolerar `nil` — no hay señal de "no soportado".
|
|
77
|
+
|
|
78
|
+
**`DesiredTasks: 0` no significa "sin nodos elegibles".** En un service `global` recién creado el Engine publica el objeto con los tres contadores en **0** y recién después evalúa los nodos (medido contra un Engine `1.54`, swarm de un nodo: `DesiredTasks` pasa de 0 a 1 en ~1s). O sea que hay una ventana en la que `RunningTasks == DesiredTasks == 0` — un consumidor que compare los dos números lee "sano" (0 de 0) o "degradado total" según cómo ordene la comparación, y en ninguno de los dos casos es cierto: el deseado todavía no está calculado. Para decidir salud hace falta `DesiredTasks.positive?` como precondición, no como resultado.
|
|
79
|
+
|
|
64
80
|
**Streams de logs (`containers`/`services`/`tasks` → `logs`).** Sin TTY el Engine multiplexa: 8 bytes de cabecera por frame (1 tipo de stream · 3 de relleno en cero · 4 de tamaño big-endian). `Middleware::LogStreamDemuxer` los saca, así que `Loggable#logs` entrega texto limpio.
|
|
65
81
|
|
|
66
82
|
El `Content-Type` **no alcanza** para decidir. `application/vnd.docker.multiplexed-stream` existe **desde la API v1.42**; su entrada de changelog dice, textual:
|
data/docs/interface/interface.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Interfaz — docker-swarm
|
|
2
2
|
|
|
3
|
-
> meta: artefacto · RFC-004 · generado arch-structure · anclado a `v0.
|
|
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
4
|
|
|
5
5
|
## 1. Resumen
|
|
6
6
|
|
|
@@ -15,7 +15,7 @@ Proyección RBS-conceptual: `símbolo · tipo · nota` (raíz → profundidad
|
|
|
15
15
|
| símbolo | tipo | nota |
|
|
16
16
|
|---|---|---|
|
|
17
17
|
| `DockerSwarm` | módulo | namespace raíz |
|
|
18
|
-
| `DockerSwarm::VERSION` | constante | `"0.
|
|
18
|
+
| `DockerSwarm::VERSION` | constante | `"0.10.0"` (`version.rb`) |
|
|
19
19
|
| `DockerSwarm.configuration` | attr (r/w) | instancia de `Configuration`; lazy-init en `configure`/`connection` |
|
|
20
20
|
| `DockerSwarm.configure { \|config\| ... }` | método de módulo | crea/yields `Configuration`; aplica `log_level` al logger; resetea la conexión memoizada |
|
|
21
21
|
| `DockerSwarm.connection` | método de módulo | `Connection` memoizada (auto-`configure` si falta) |
|
|
@@ -48,6 +48,7 @@ Inventario completo de opciones: [`docs/config/configuracion.md`](../config/conf
|
|
|
48
48
|
| `.all(filters = {})` | método de clase | `GET index`; mapea a instancias; aplica `root_key`; `[]` si vacío |
|
|
49
49
|
| `.find(id)` | método de clase | `GET show`; `nil` si `Errors::NotFound` |
|
|
50
50
|
| `.where(filters)` | método de clase | alias de `all` |
|
|
51
|
+
| `.index_query_params` | método de clase | `%i[all force limit since before]` por default; override por modelo (`+ :status` en `Service`). Claves que viajan como **query params propios** del listado; **todo lo no declarado se serializa dentro del JSON de `?filters=`** — si tampoco es filtro válido del recurso, el Engine lo rechaza o lo ignora |
|
|
51
52
|
| `#initialize(attributes = {})` | método de instancia | `assign_attributes` si presente |
|
|
52
53
|
| `#assign_attributes(new_attributes)` | método de instancia | normaliza `Id`→`ID`; `deep_merge` del campo `Spec`; `ArgumentError` si no es Hash |
|
|
53
54
|
| `#attributes` | método de instancia | `instance_values` sin internos de ActiveModel |
|
|
@@ -76,7 +77,7 @@ Inventario completo de opciones: [`docs/config/configuracion.md`](../config/conf
|
|
|
76
77
|
|
|
77
78
|
| símbolo | tipo | concerns + métodos propios |
|
|
78
79
|
|---|---|---|
|
|
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::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` |
|
|
80
81
|
| `DockerSwarm::Node` | clase < Base | Updatable, Deletable (sin `create`: los nodos se unen fuera de la gema) |
|
|
81
82
|
| `DockerSwarm::Task` | clase < Base | Loggable (read-only; generadas por el orquestador) |
|
|
82
83
|
| `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 |
|
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.
|
|
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)
|
|
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.10.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,6 @@
|
|
|
1
1
|
# Test — docker-swarm
|
|
2
2
|
|
|
3
|
-
> meta: artefacto · RFC-013 · generado arch-structure + enriquecido arch-enrich · anclado a `v0.
|
|
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
4
|
|
|
5
5
|
## 1. Resumen
|
|
6
6
|
|
|
@@ -54,6 +54,7 @@ Ninguna. No hay `SimpleCov`/`.simplecov` ni umbral declarado en el repo (verific
|
|
|
54
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
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
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
|
+
- 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). 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?`.
|
|
57
58
|
- Infra de transporte: `api_spec`, `connection_spec`, `configuration_spec`, `log_helper_spec`, los 4 middleware specs.
|
|
58
59
|
- `swarm`, `system` (singletons): `swarm_spec`, `system_spec`.
|
|
59
60
|
|
data/lib/docker_swarm/base.rb
CHANGED
|
@@ -46,14 +46,33 @@ module DockerSwarm
|
|
|
46
46
|
all(filters)
|
|
47
47
|
end
|
|
48
48
|
|
|
49
|
+
# Claves que el Engine toma como **query params propios** del listado
|
|
50
|
+
# (`?limit=`, `?all=`, …), no como *filters*.
|
|
51
|
+
#
|
|
52
|
+
# Todo lo que NO esté declarado acá se serializa dentro del JSON de `?filters=`.
|
|
53
|
+
# Si además no es un filtro válido del recurso, el Engine lo rechaza o lo ignora:
|
|
54
|
+
# el parámetro **no llega** y no hay forma de pedirlo desde la gema.
|
|
55
|
+
#
|
|
56
|
+
# Override en el modelo que declare uno propio (p. ej. `status` en
|
|
57
|
+
# {DockerSwarm::Service}). La lista **no** se generaliza acá por comodidad: un
|
|
58
|
+
# mismo nombre puede ser query param en un recurso y filtro legítimo en otro
|
|
59
|
+
# (`status` lo es en `/containers/json`), y globalizarlo lo saca del `?filters=`
|
|
60
|
+
# donde ese otro recurso lo necesita.
|
|
61
|
+
#
|
|
62
|
+
# @return [Array<Symbol>] nombres de query param
|
|
63
|
+
def index_query_params
|
|
64
|
+
%i[all force limit since before].freeze
|
|
65
|
+
end
|
|
66
|
+
|
|
49
67
|
private
|
|
50
68
|
|
|
51
69
|
def _fetch_all(filters = {})
|
|
52
70
|
query = {}
|
|
53
71
|
|
|
54
72
|
if filters.present?
|
|
55
|
-
|
|
56
|
-
|
|
73
|
+
query_keys = index_query_params
|
|
74
|
+
global_params = filters.slice(*query_keys)
|
|
75
|
+
docker_filters = filters.except(*query_keys)
|
|
57
76
|
|
|
58
77
|
query = global_params
|
|
59
78
|
|
|
@@ -9,6 +9,30 @@ module DockerSwarm
|
|
|
9
9
|
include Concerns::Deletable
|
|
10
10
|
include Concerns::Loggable
|
|
11
11
|
|
|
12
|
+
# `status` es un query param **propio** de `GET /services` (Engine API ≥ v1.41), no un
|
|
13
|
+
# filtro: con `?status=true` cada elemento del listado trae `ServiceStatus`
|
|
14
|
+
# (`RunningTasks` · `DesiredTasks` · `CompletedTasks`).
|
|
15
|
+
#
|
|
16
|
+
# Es el **único** lugar donde el Engine publica el deseado de un service en modo
|
|
17
|
+
# `global`. Un service replicado lo expone en `Spec.Mode.Replicated.Replicas`, pero
|
|
18
|
+
# para uno global ese campo no existe —el deseado es "una task por nodo elegible"—,
|
|
19
|
+
# así que sin `DesiredTasks` no hay número contra el cual comparar las tasks que
|
|
20
|
+
# corren: un global degradado *parcialmente* (2 de 3 nodos) es indistinguible de uno
|
|
21
|
+
# sano, y solo se detecta el caso extremo de cero tasks.
|
|
22
|
+
#
|
|
23
|
+
# Va acá y no en la whitelist de {DockerSwarm::Base} porque en `/containers/json`
|
|
24
|
+
# `status` **sí** es un filtro válido (`running`, `exited`, …): globalizarlo lo sacaría
|
|
25
|
+
# del `?filters=` y rompería `Container.where(status: "running")`.
|
|
26
|
+
#
|
|
27
|
+
# En un Engine por debajo de v1.41 el parámetro se ignora sin error y `ServiceStatus`
|
|
28
|
+
# llega ausente → el consumidor tiene que tolerar `nil`.
|
|
29
|
+
#
|
|
30
|
+
# @return [Array<Symbol>] Symbols (no Strings como +create_query_params+): acá se
|
|
31
|
+
# matchea contra las claves de +filters+.
|
|
32
|
+
def self.index_query_params
|
|
33
|
+
(super + %i[status]).freeze
|
|
34
|
+
end
|
|
35
|
+
|
|
12
36
|
# Restarts the service by incrementing ForceUpdate, which causes
|
|
13
37
|
# Docker to recreate all tasks.
|
|
14
38
|
#
|
data/lib/docker_swarm/version.rb
CHANGED
data/skill/SKILL.md
CHANGED
|
@@ -58,7 +58,7 @@ Defaults son razonables: en local sin TLS, no necesitás bloque `configure`.
|
|
|
58
58
|
|
|
59
59
|
| Modelo | Class methods | Instance methods | Notas |
|
|
60
60
|
|---|---|---|---|
|
|
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 |
|
|
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
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 |
|
|
@@ -80,6 +80,11 @@ DockerSwarm::Service.all(label: ["env=production"])
|
|
|
80
80
|
DockerSwarm::Node.all(role: ["manager"])
|
|
81
81
|
DockerSwarm::Container.all(status: ["running"])
|
|
82
82
|
|
|
83
|
+
# Listar pidiendo ServiceStatus (query param propio, NO filtro; Engine API >= v1.41)
|
|
84
|
+
DockerSwarm::Service.where(status: true).each do |svc|
|
|
85
|
+
svc.ServiceStatus # => { "RunningTasks" => 2, "DesiredTasks" => 3, "CompletedTasks" => 0 } | nil
|
|
86
|
+
end
|
|
87
|
+
|
|
83
88
|
# Lookup graceful (nil si 404)
|
|
84
89
|
service = DockerSwarm::Service.find("svc-id") # => Service | nil
|
|
85
90
|
|
|
@@ -134,6 +139,8 @@ Todas heredan de `DockerSwarm::Error`. Tres formas de acceso equivalentes: `Dock
|
|
|
134
139
|
- **`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})`.
|
|
135
140
|
- **`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.
|
|
136
141
|
- **`Container.create` manda el nombre por query string.** Docker **descarta en silencio** un `name:` en el body y responde `201`: el container nace con nombre aleatorio y un reintento duplica en vez de adoptar. La gema lo resuelve sola vía `Container.create_query_params == %w[name]` — pero si armás el request por afuera (`DockerSwarm.request`), el `?name=` es tuyo. Ver ADR-025 cláusula 1 y §3.11 de `docs/behavior/behavior.md`.
|
|
142
|
+
- **`where` parte las claves en dos: query params propios vs. `?filters=`.** Solo lo declarado en `Base.index_query_params` (`%i[all force limit since before]`, más `:status` en `Service`) viaja en la URL; **todo lo demás se serializa como filtro de Docker**. Si le pasás un query param propio que el modelo no declara, no llega: va dentro de `filters` y el Engine lo rechaza o lo ignora — sin error que indique que iba en la URL. Los nombres no son globales: `status` es query param en `/services` y **filtro válido** en `/containers/json`.
|
|
143
|
+
- **`ServiceStatus` solo aparece con `Service.where(status: true)`**, y es el **único** lugar donde el Engine publica el deseado de un service en modo `global` (un replicado lo tiene en `Spec.Mode.Replicated.Replicas`; un global no tiene ese campo). Sin `DesiredTasks` un global corriendo en 2 de 3 nodos elegibles es indistinguible de uno sano. Requiere API ≥ v1.41 y **degrada en silencio**: la gema no fija `?version=`, así que en un Engine anterior el parámetro se ignora y `ServiceStatus` llega `nil` — no hay señal de "no soportado", el consumidor tiene que tolerarlo. Y **`DesiredTasks: 0` no es "sin nodos elegibles"**: en un `global` recién creado el Engine publica los tres contadores en 0 y calcula el deseado después (~1s, medido), así que hay una ventana donde `RunningTasks == DesiredTasks == 0` y comparar los dos números miente — tratá `DesiredTasks.positive?` como precondición para decidir salud. Ver §3.12 de `docs/behavior/behavior.md`.
|
|
137
144
|
- **`logs` devuelve texto ya demultiplexado.** Sin TTY el Engine enmarca cada fragmento con 8 bytes de cabecera; `Middleware::LogStreamDemuxer` los saca en `Container`, `Service` y `Task`. Dos consecuencias: **`stdout` y `stderr` vienen intercalados** en un solo String (si necesitás un dato puntual, delimitalo en origen desde el `Cmd`), y **un frame partido entre chunks no se reensambla** — el demux es todo-o-nada, así que ante cualquier inconsistencia te devuelve el body intacto en vez de texto a medias. Con `follow: 1` el body no llega completo, así que no esperes demux ahí.
|
|
138
145
|
- **`Image.create` retirado (breaking).** Ya no existe (`Image` dejó de ser Creatable; el `create` estaba roto y sin consumidores). Usá `Image.pull(image_reference, registry_auth:)`.
|
|
139
146
|
- **Auth de registry privado soportado** vía credencial opaca base64url en `registry_auth:` — `Image.pull(ref, registry_auth:)`, `Service.create(..., registry_auth:)` y `Service#update(..., registry_auth:` / `registry_auth_from:)`. Viaja por header `X-Registry-Auth` (o query `registryAuthFrom`: `spec`\|`previous-spec`, excluyentes); la gema no la mintea ni decodifica. Ver flujos 3.9/3.10 de `docs/behavior/behavior.md`.
|
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.10.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.10.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.10.0/skill/SKILL.md
|
|
142
142
|
rubygems_mfa_required: 'true'
|
|
143
143
|
rdoc_options: []
|
|
144
144
|
require_paths:
|