docker-swarm 0.10.0 → 0.11.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 824016bc8286430e75b85e1daa74f5fa30039667230585bcd548ac76afacd1da
4
- data.tar.gz: c8d56047e0e282aa35dcdd3998780f0bfcd7c5ba1be6464068bff91b99276934
3
+ metadata.gz: 4b67b8f60093fc00f4305221d2f4619ebda26d59979db28cffa12947b1aa2b51
4
+ data.tar.gz: 6a220462c6f427c9ed883651d92c7e54f7eb058925d4eb1e3b6f61764c9db545
5
5
  SHA512:
6
- metadata.gz: bc733a3eb22266fe1ca82dcfbc56445bf0118a8fd9c077cf697849fe15004463571c94248cd48f236d42a67d2ccbb415419e8e1a05096842bb72fec035d63fc9
7
- data.tar.gz: e65002ee881d436b549918321b415b3acc224679c40690b08651ff48960f3d8c6ec3f00849ffc7830c91a8ab00d834940299e355e4225a814e231abc51a133c5
6
+ metadata.gz: 3a9189e2987390ed20f32ca0288d18ee96df35b637f0b675ef8d5ff0e9bddbe5743b8145afbc57dedf1e921e45a5f7031c4b9cd4c91992dfb1120a637aef9ad9
7
+ data.tar.gz: aa102d0979a97aed02dab90a0dacf03c89a81001750072957e9ec2ac5a6a8e12aa1ba9b4c08442c00a83af74bfb0291e7723e0b3123b4ae3cacee44fa58d4562
data/CHANGELOG.md CHANGED
@@ -4,6 +4,20 @@ All notable changes to this project will be documented in this file.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.11.0] — 2026-08-07
8
+
9
+ ### Correcciones
10
+ - **`Container.where(since:)` / `where(before:)` filtran de verdad, y `size` llega al Engine** (#35). Las dos claves son **filtros** de `GET /containers/json` según el spec v1.41 (`ContainerList`), no query params, pero el default de `Base.index_query_params` las ruteaba a la URL: el Engine **las ignoraba y devolvía la lista sin filtrar, sin ningún error** — resultado incorrecto silencioso. Y `size` (query param propio, pide el tamaño de los archivos del container) viajaba dentro de `?filters=` como filtro inválido. `Container` ahora declara `%i[all limit size]` — @Pslp
11
+ - **Mismo bug en `Image`**, con la misma evidencia: `/images/json` declara `all` y `digests` como query params propios y lista `before`/`since` entre sus filtros. `Image` ahora declara `%i[all digests]`.
12
+ - **Cambio de comportamiento observable:** `Container.where(since: id)` antes devolvía **todos** los containers y ahora filtra. Si algún consumidor compensaba filtrando en Ruby, el resultado no cambia; si dependía de recibir la lista completa, cambia.
13
+ - `Base` **no** se toca: su default (`%i[all force limit since before]`) no matchea el listado de ningún recurso, pero limpiarlo convertiría no-ops silenciosos en filtros inválidos hacia el Engine para los 7 modelos que hoy no declaran lista propia. Queda como decisión aparte.
14
+
15
+ ### Mejoras internas
16
+ - Bump `activesupport` y `activemodel` 8.1.3 → 8.1.3.1 (#25) — @dependabot. Las release notes de 8.1.3.1 declaran "No changes" para Active Support y Active Model: es el patch de la familia Rails, sin cambios propios en las dos gemas que la gema usa.
17
+ - Bump `excon` 1.5.0 → 1.6.0 (#20) — @dependabot. Verificado con la suite completa (203 ejemplos, incluida la integración contra un swarm real), no solo con el subconjunto de CI.
18
+ - Bump `actions/checkout` 6 → 7 en `main.yml` y `release.yml` (#14) — @dependabot.
19
+ - `docs/topology/topology.md`: versiones resueltas de las tres dependencias runtime al día.
20
+
7
21
  ## [0.10.0] — 2026-08-05
8
22
 
9
23
  ### Nuevas funcionalidades
@@ -65,6 +65,17 @@ Serialización: request body no-String → JSON (`Content-Type: application/json
65
65
 
66
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
67
 
68
+ **Partición real por listado**, verificada contra el spec v1.41 (`ContainerList`, `ImageList`, `ServiceList`, …). El resto de los recursos (`/networks`, `/volumes`, `/nodes`, `/tasks`, `/secrets`, `/configs`) declara **solo** `filters`:
69
+
70
+ | listado | query params propios | dónde caen `since`/`before` |
71
+ |---|---|---|
72
+ | `GET containers/json` | `all`, `limit`, `size` | **filtros** del recurso |
73
+ | `GET images/json` | `all`, `digests` | **filtros** del recurso |
74
+ | `GET services` | `status` | no existen |
75
+ | `GET networks` · `volumes` · `nodes` · `tasks` · `secrets` · `configs` | — (solo `filters`) | no existen |
76
+
77
+ Consecuencia para el default de `Base` (`%i[all force limit since before]`): **no matchea el listado de ningún recurso**. `since`/`before` son filtros donde existen, `force` no es query param de ningún listado (es de `DELETE`/prune) y `limit` solo aplica a containers. Un modelo que no declare su lista propia rutea `since`/`before` a la URL, donde el Engine **los ignora y devuelve la lista sin filtrar, sin error** — resultado incorrecto silencioso. `Container` e `Image` declaran la suya por eso (#35); los que solo aceptan `filters` no se ven afectados en la práctica, porque esas claves tampoco son filtros válidos ahí.
78
+
68
79
  **`?status=true` en `/services`** (API ≥ v1.41) agrega a cada elemento:
69
80
 
70
81
  ```json
@@ -48,7 +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
+ | `.index_query_params` | método de clase | `%i[all force limit since before]` por default; **override por modelo**: `Service` (`+ :status`), `Container` (`%i[all limit size]`), `Image` (`%i[all digests]`). 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. El default de `Base` no matchea el listado de ningún recurso concreto: `since`/`before`/`force` no son query params de ningún listado del spec v1.41 (ver #35) |
52
52
  | `#initialize(attributes = {})` | método de instancia | `assign_attributes` si presente |
53
53
  | `#assign_attributes(new_attributes)` | método de instancia | normaliza `Id`→`ID`; `deep_merge` del campo `Spec`; `ArgumentError` si no es Hash |
54
54
  | `#attributes` | método de instancia | `instance_values` sin internos de ActiveModel |
@@ -80,8 +80,8 @@ Inventario completo de opciones: [`docs/config/configuracion.md`](../config/conf
80
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` |
81
81
  | `DockerSwarm::Node` | clase < Base | Updatable, Deletable (sin `create`: los nodos se unen fuera de la gema) |
82
82
  | `DockerSwarm::Task` | clase < Base | Loggable (read-only; generadas por el orquestador) |
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 |
84
- | `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) |
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; `.index_query_params == %i[all limit size]` — `since`/`before` son **filtros** de `/containers/json`, no query params (#35) |
84
+ | `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
85
  | `DockerSwarm::Network` | clase < Base | Creatable, Updatable, Deletable |
86
86
  | `DockerSwarm::Volume` | clase < Base | Creatable, Deletable; `.root_key = "Volumes"` (respuesta wrapped) |
87
87
  | `DockerSwarm::Config` | clase < Base | Creatable, Deletable (sin `update`: recrear) |
data/docs/test/testing.md CHANGED
@@ -54,7 +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
+ - 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?`.
58
58
  - Infra de transporte: `api_spec`, `connection_spec`, `configuration_spec`, `log_helper_spec`, los 4 middleware specs.
59
59
  - `swarm`, `system` (singletons): `swarm_spec`, `system_spec`.
60
60
 
@@ -1,6 +1,6 @@
1
1
  # Topología — docker-swarm
2
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
3
+ > meta: artefacto · RFC-006 · generado arch-structure · anclado a `d731a8a` · cobertura: dependencias runtime (`.gemspec` + `Gemfile.lock`) y mapa de contexto de la gema
4
4
 
5
5
  ## 1. Resumen
6
6
 
@@ -14,9 +14,9 @@ Runtime declaradas en `docker-swarm.gemspec`; versiones resueltas en `Gemfile.lo
14
14
 
15
15
  | nombre | versión (constraint) | resuelta | rol |
16
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 |
17
+ | `activesupport` | `>= 6.0` | 8.1.3.1 | core-ext (`HashWithIndifferentAccess`, `deep_merge`, `blank?`, `demodulize`, `pluralize`) |
18
+ | `activemodel` | `>= 6.0` | 8.1.3.1 | `ActiveModel::Model` (validaciones, API de atributos) en `Base` |
19
+ | `excon` | `>= 0.80` | 1.6.0 | cliente HTTP con soporte nativo de Unix socket + stack de middlewares |
20
20
 
21
21
  Desarrollo / test (no se empaquetan): `rake ~> 13.0`, `rspec ~> 3.0`, `pry`, `rubocop-rails-omakase`.
22
22
 
@@ -45,7 +45,7 @@ No aplica: es una librería embebida en el proceso del consumidor (sin web/worke
45
45
 
46
46
  | afirmación | confidence | a verificar |
47
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 |
48
+ | `activesupport`/`activemodel` 8.1.3.1 son las resueltas hoy, pero el constraint `>= 6.0` admite Rails 6/7/8 | declared | `Gemfile.lock` fija 8.1.3.1; el `.gemspec` no pone techo |
49
49
  | La gema no abre puertos ni corre procesos propios | declared | sin `config/`, sin `bin/` server, sin Railtie/Engine |
50
50
 
51
51
  ## 4. Cobertura y fronteras
@@ -17,6 +17,21 @@ module DockerSwarm
17
17
  %w[name].freeze
18
18
  end
19
19
 
20
+ # `GET /containers/json` declara exactamente tres query params propios además de
21
+ # `filters`: `all`, `limit` y `size` (spec v1.41, `ContainerList`).
22
+ #
23
+ # No hereda el default de {DockerSwarm::Base} porque **`since` y `before` son
24
+ # filtros** de este recurso, no query params: ruteados a la URL el Engine los ignora
25
+ # y devuelve **la lista sin filtrar, sin error** — resultado incorrecto silencioso,
26
+ # peor que el rechazo ruidoso de #22. Y `size` faltaba: es query param propio (pide
27
+ # el tamaño de los archivos del container), así que viajaba dentro de `filters` como
28
+ # filtro inválido. Ver #35.
29
+ #
30
+ # @return [Array<Symbol>] Symbols (matchean contra las claves de `filters`)
31
+ def self.index_query_params
32
+ %i[all limit size].freeze
33
+ end
34
+
20
35
  # Starts the container
21
36
  # @return [Boolean] true if successful
22
37
  def start
@@ -15,6 +15,19 @@ module DockerSwarm
15
15
  DIGEST_STATUS = /\bDigest:\s*(sha256:[0-9a-f]+)/
16
16
 
17
17
  class << self
18
+ # `GET /images/json` declara dos query params propios además de `filters`: `all` y
19
+ # `digests` (spec v1.41, `ImageList`).
20
+ #
21
+ # Mismo caso que {DockerSwarm::Container}: **`since` y `before` son filtros** de
22
+ # este recurso (`<image-name>[:<tag>]`, `<image id>` o `<image@digest>`), así que
23
+ # el default de {DockerSwarm::Base} los mandaba a la URL, donde el Engine los ignora
24
+ # y devuelve la lista sin filtrar **sin error**. Ver #35.
25
+ #
26
+ # @return [Array<Symbol>] Symbols (matchean contra las claves de `filters`)
27
+ def index_query_params
28
+ %i[all digests].freeze
29
+ end
30
+
18
31
  # Pull explícito de una imagen (POST /images/create).
19
32
  #
20
33
  # Operación SÍNCRONA: consume el stream NDJSON de progreso hasta EOF, eleva
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DockerSwarm
4
- VERSION = "0.10.0"
4
+ VERSION = "0.11.0"
5
5
  end
data/skill/SKILL.md CHANGED
@@ -139,7 +139,7 @@ Todas heredan de `DockerSwarm::Error`. Tres formas de acceso equivalentes: `Dock
139
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})`.
140
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.
141
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`.
142
+ - **`where` parte las claves en dos: query params propios vs. `?filters=`.** Solo lo declarado en `index_query_params` viaja en la URL; **todo lo demás se serializa como filtro de Docker**. La lista es **por modelo**: `Container` → `%i[all limit size]`, `Image` → `%i[all digests]`, `Service` → default `+ :status`. 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`. ⚠️ **El default de `Base` (`%i[all force limit since before]`) no matchea ningún listado real** — `since`/`before` son *filtros* donde existen y `force` no es query param de ningún listado; un modelo sin lista propia los rutea a la URL y el Engine **devuelve la lista sin filtrar, sin error** (#35).
143
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`.
144
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í.
145
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:)`.
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.10.0
4
+ version: 0.11.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.10.0/CHANGELOG.md
139
+ changelog_uri: https://github.com/sequre/docker-swarm/blob/v0.11.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.10.0/skill/SKILL.md
141
+ documentation_uri: https://github.com/sequre/docker-swarm/blob/v0.11.0/skill/SKILL.md
142
142
  rubygems_mfa_required: 'true'
143
143
  rdoc_options: []
144
144
  require_paths: