docker-swarm 0.6.0 → 0.7.1

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.
data/skill/SKILL.md CHANGED
@@ -1,230 +1,174 @@
1
- # DockerSwarm Expert
2
-
3
- Skill de conocimiento completo sobre DockerSwarm. Consultame para cualquier pregunta sobre integración, arquitectura, API, errores y antipatrones.
4
-
5
- ## Glosario
6
-
7
- **Base** Clase ORM base que hereda de ActiveModel::Model. Provee accessors dinámicos PascalCase, `find`, `all`, `where`, `reload`, `payload_for_docker`. Todos los modelos heredan de ella.
8
-
9
- **Concern** Mixin ActiveSupport::Concern que agrega comportamiento CRUD a un modelo: Creatable (POST), Updatable (POST con Version.Index), Deletable (DELETE), Loggable (logs streaming), Inspectable (#inspect legible). **Nota:** Los atributos dinámicos en Inspectable deben invocarse con `send(:ID)` para evitar `NameError` por interpretación como constante.
10
-
11
- **Middleware** Capa Excon que procesa request/response: RequestEncoder (serialización body), ResponseJSONParser (parsing + indifferent access), ErrorHandler (status HTTP → excepción).
12
-
13
- **Deep Indifferent Access** Toda respuesta JSON se convierte recursivamente a `HashWithIndifferentAccess`, permitiendo acceso por symbol o string en cualquier nivel de anidamiento.
14
-
15
- **Dynamic Accessor** — Cuando Docker devuelve un atributo nuevo (ej: `Spec`, `Status`), Base crea `attr_accessor` dinámicamente via `method_missing`. Se cachea en `defined_attributes` (Set) para no redefinir.
16
-
17
- **Version.Index** — Mecanismo de Docker para updates atómicos. Updatable extrae `Version["Index"]` y lo envía como query param para evitar race conditions.
18
-
19
- **LogHelper** Módulo que formatea logs en KV (`key=value`) y enmascara campos sensibles matching `/password|token|api_key|auth|secret|data/i` → `[FILTERED]`.
20
-
21
- ## Arquitectura
22
-
23
- ### Responsabilidad core
24
-
25
- ORM ligero compatible con ActiveModel para Docker Engine API. Comunica via Excon sobre Unix socket (default) o TCP. Todos los requests pasan por una cadena de middlewares.
26
-
27
- ### Mapa de componentes
28
-
29
- ```
30
- ┌──────────────┐ ┌──────────────┐ ┌──────────────┐
31
- │ Modelo │────>│ Api │────>│ Connection │
32
- │ (Service, │ │ (ENDPOINTS │ │ (Excon + │
33
- │ Node...) │ │ + request) │ │ Timeouts) │
34
- └──────────────┘ └──────────────┘ └──────────────┘
35
-
36
-
37
- ┌───────────────────────┐
38
- │ Middleware Stack │
39
- │ RequestEncoder │
40
- │ ResponseJSONParser │
41
- │ ErrorHandler │
42
- └───────────────────────┘
43
- ```
44
-
45
- ### Flujo en runtime
46
-
47
- 1. Modelo invoca `Api.request(action:, arguments:, query_params:, payload:)`.
48
- 2. Api formatea el path con `format()` y delega a `DockerSwarm.request`.
49
- 3. Connection crea/reutiliza cliente Excon singleton con middlewares.
50
- 4. RequestEncoder serializa body (JSON default, form-urlencoded, multipart).
51
- 5. Excon envía al Docker daemon (socket o TCP).
52
- 6. ResponseJSONParser parsea JSON y aplica `with_indifferent_access` recursivo.
53
- 7. ErrorHandler mapea status 4xx/5xx a excepciones tipadas.
54
- 8. Connection mide duración con `CLOCK_MONOTONIC` y loguea en KV.
55
-
56
- ### Decisiones de diseño
57
-
58
- - **PascalCase fiel**: Los atributos mantienen el naming de Docker (`Spec`, `TaskTemplate`, `ContainerSpec`). No se transforman a snake_case.
59
- - **Spec merging**: `assign_attributes` hace `deep_merge` cuando el atributo es `Spec` para no perder campos anidados en updates.
60
- - **Singleton connection**: `@client ||= Excon.new(...)` — se reutiliza, se resetea al cambiar configuración.
61
- - **Retries en Excon**: `idempotent: true` + `retry_errors: [Socket, Timeout]` con `max_retries` configurable.
62
-
63
- ## API Pública (resumen)
1
+ ---
2
+ name: docker-swarm
3
+ description: >-
4
+ ORM y cliente API Ruby para Docker Swarm. Expone las primitivas del Docker
5
+ Engine (Service, Node, Task, Container, Network, Volume, Config, Secret,
6
+ Swarm, System, Image) como modelos ActiveModel con CRUD, control de
7
+ concurrencia optimista (Version.Index), retries seguros por método HTTP,
8
+ logging KV con masking de secrets y jerarquía de errores tipada. ACTIVAR
9
+ cuando el caller necesita orquestar Docker desde Ruby listar/crear/
10
+ actualizar/eliminar recursos del cluster, leer logs de services/tasks/
11
+ containers, hacer health-check del daemon (System.up/info/df), filtrar por
12
+ labels, o capturar errores tipados de Docker (Conflict/NotFound/
13
+ Communication). NO activar para builds de imágenes (no implementado), pull
14
+ con auth de registry privado (no implementado), o flujos que no son
15
+ Swarm (Docker Compose, raw containers).
16
+ triggers:
17
+ - "DockerSwarm::"
18
+ - "docker-swarm gem"
19
+ - "Docker Engine API desde Ruby"
20
+ - "Service.create / Service.update / Service.restart"
21
+ - "Container.start / Container.stop"
22
+ - "logs de un servicio Docker"
23
+ - "Version.Index"
24
+ ---
25
+
26
+ # docker-swarm — Skill
27
+
28
+ ## Qué es / cuándo usar
29
+
30
+ Gema Ruby que provee ORM `ActiveModel`-compatible sobre Docker Engine API. Cliente HTTP `Excon` directo (Unix socket o TCP). Usá esta dep para orquestación programática de Docker Swarm: lifecycle de recursos, logs, health checks y observabilidad.
31
+
32
+ No es:
33
+ - Build/compose tool — no construye imágenes, no parsea `docker-compose.yml`.
34
+ - Driver Kubernetes — sólo Swarm.
35
+
36
+ ## Contrato resumido (piso mínimo)
64
37
 
65
38
  ### Configuración
66
39
 
67
40
  ```ruby
68
41
  DockerSwarm.configure do |config|
69
- config.socket_path = "unix:///var/run/docker.sock" # o http://host:port
42
+ config.socket_path = "unix:///var/run/docker.sock" # o "http://host:2375"
70
43
  config.logger = Logger.new($stdout)
71
44
  config.log_level = Logger::INFO
72
- config.read_timeout = 60.0 # segundos
45
+ config.read_timeout = 60.0
73
46
  config.write_timeout = 60.0
74
47
  config.connect_timeout = 10.0
75
48
  config.max_retries = 3
76
49
  end
77
50
  ```
78
51
 
79
- ### Operaciones comunes
80
-
81
- ```ruby
82
- # Listar
83
- DockerSwarm::Service.all
84
- DockerSwarm::Service.all(label: ["app=web"])
85
-
86
- # Buscar
87
- service = DockerSwarm::Service.find("service_id") # nil si no existe
52
+ Defaults son razonables: en local sin TLS, no necesitás bloque `configure`.
88
53
 
89
- # Crear
90
- svc = DockerSwarm::Service.create(Name: "web", TaskTemplate: { ... })
54
+ ### Símbolos públicos por modelo
91
55
 
92
- # Actualizar (maneja Version.Index automáticamente)
93
- service.update(Spec: { Replicas: 3 })
56
+ | Modelo | Class methods | Instance methods | Notas |
57
+ |---|---|---|---|
58
+ | `Service` | `all(filters)`, `find(id)`, `where(filters)`, `create(attrs)` | `update(attrs)`, `restart`, `destroy`, `logs(query)`, `reload`, `persisted?`, `id` | CRUD completo + force-recreate de tasks |
59
+ | `Node` | `all(filters)`, `find(id)`, `where(filters)` | `update(attrs)`, `destroy` | No `create` (los nodos se unen fuera de la gema) |
60
+ | `Task` | `all(filters)`, `find(id)`, `where(filters)` | `logs(query)`, `reload` | Read-only (generados por orquestador) |
61
+ | `Container` | `all(filters)`, `find(id)`, `where(filters)` | `start`, `stop`, `destroy`, `logs(query)` | **No `create`** (gap conocido, fuera F1) |
62
+ | `Image` | `all(filters)`, `find(id)`, `create(attrs)` | `destroy` | `create` = pull. **No soporta `X-Registry-Auth`** (registries privados con auth no funcionan) |
63
+ | `Network` | `all(filters)`, `find(id)`, `create(attrs)` | `update(attrs)`, `destroy` | CRUD completo |
64
+ | `Volume` | `all(filters)`, `find(id)`, `create(attrs)` | `destroy` | No `update` (Docker no lo soporta). Respuesta wrapped vía `root_key = "Volumes"` |
65
+ | `Config` | `all(filters)`, `find(id)`, `create(attrs)` | `destroy` | No `update` — recrear |
66
+ | `Secret` | `all(filters)`, `find(id)`, `create(attrs)` | `destroy` | No `update`. `Data` se filtra en logs (LogHelper) |
67
+ | `Swarm` | `.show` | — | Singleton, sólo info del cluster |
68
+ | `System` | `.info`, `.version`, `.up`, `.df` | — | Singleton, health/observabilidad |
94
69
 
95
- # Reiniciar (fuerza recreación de tasks)
96
- service.restart
97
-
98
- # Eliminar (graceful con 404)
99
- service.destroy
100
-
101
- # Logs
102
- service.logs(stdout: 1, stderr: 1)
103
-
104
- # Sistema
105
- DockerSwarm::System.up # ping
106
- DockerSwarm::System.info # daemon info
107
- DockerSwarm::System.version # versión Docker
108
- DockerSwarm::System.df # disk usage
109
- DockerSwarm::Swarm.show # info del cluster
110
- ```
70
+ Detalle por símbolo en [`docs/glossary/glossary.md`](docs/glossary/glossary.md). Secuencias de operación en [`docs/behavior/behavior.md`](docs/behavior/behavior.md).
111
71
 
112
- Ver [API Detallada](references/api-detallada.md) para la referencia completa de todos los modelos.
113
-
114
- ## FAQ
115
-
116
- ### Como conecto a un Docker remoto por TCP?
72
+ ### Operaciones típicas
117
73
 
118
74
  ```ruby
119
- DockerSwarm.configure do |config|
120
- config.socket_path = "http://192.168.1.100:2375"
121
- end
122
- ```
123
- Connection detecta si empieza con `unix://` (socket) o no (TCP) y configura Excon.
124
-
125
- ### Por qué los atributos son PascalCase y no snake_case?
126
-
127
- Docker Engine API usa PascalCase en todos sus JSON. La gema mantiene fidelidad 1:1 para evitar confusión al leer la documentación de Docker. Se accede igual: `service.Spec`, `node.Status`.
128
-
129
- ### Como filtro recursos con labels?
130
-
131
- ```ruby
132
- DockerSwarm::Service.all(label: ["env=production", "app=web"])
75
+ # Listar con filtros (serializa como JSON en query param `filters`)
76
+ DockerSwarm::Service.all(label: ["env=production"])
133
77
  DockerSwarm::Node.all(role: ["manager"])
134
- ```
135
- Los filtros se serializan como JSON en el query param `filters` del Docker API.
136
-
137
- ### Como manejo errores de conexión?
138
-
139
- ```ruby
140
- begin
141
- DockerSwarm::System.up
142
- rescue DockerSwarm::Communication => e
143
- # Socket caído o inalcanzable
144
- end
145
- ```
146
- `Communication` envuelve errores de `Excon::Error::Socket`. Los retries se aplican automáticamente (`max_retries`).
78
+ DockerSwarm::Container.all(status: ["running"])
147
79
 
148
- ### Puedo usar validaciones de ActiveModel?
80
+ # Lookup graceful (nil si 404)
81
+ service = DockerSwarm::Service.find("svc-id") # => Service | nil
149
82
 
150
- Sí. Todos los modelos heredan de `ActiveModel::Model`. Podés agregar validaciones custom:
83
+ # Crear (auto-reload tras POST para hidratar Spec/Version completos)
84
+ svc = DockerSwarm::Service.create(
85
+ Name: "web",
86
+ TaskTemplate: { ContainerSpec: { Image: "nginx:latest" } }
87
+ )
151
88
 
152
- ```ruby
153
- service = DockerSwarm::Service.new(Name: "")
154
- service.valid? # usa validaciones ActiveModel
155
- service.save # retorna false si invalid?
156
- ```
89
+ # Update atómico (Version.Index extraído automáticamente)
90
+ service.update(Mode: { Replicated: { Replicas: 3 } })
157
91
 
158
- ## Antipatrones
92
+ # Force-recreate de tasks (equivalente a `docker service update --force`)
93
+ service.restart
159
94
 
160
- ### Transformar atributos a snake_case
95
+ # Destroy graceful (nil si 404)
96
+ service.destroy
161
97
 
162
- ```ruby
163
- # MAL
164
- service.task_template["container_spec"]
98
+ # Logs raw
99
+ service.logs(stdout: 1, stderr: 1)
165
100
 
166
- # BIEN
167
- service.TaskTemplate["ContainerSpec"]
101
+ # Health check
102
+ DockerSwarm::System.up # => "OK" si daemon responde
168
103
  ```
169
- **Razón:** Docker API usa PascalCase. La gema mantiene fidelidad. Con indifferent access, podés usar strings o symbols, pero siempre PascalCase.
170
104
 
171
- ### Reemplazar Spec en vez de mergear
105
+ ### Jerarquía de errores
106
+
107
+ Todas heredan de `DockerSwarm::Error`. Tres formas de acceso equivalentes: `DockerSwarm::NotFound`, `DockerSwarm::Error::NotFound`, `DockerSwarm::Errors::NotFound`.
108
+
109
+ | Excepción | Status | Cuándo |
110
+ |---|---|---|
111
+ | `BadRequest` | 400 | Payload malformado |
112
+ | `Unauthorized` | 401 | TLS sin credenciales |
113
+ | `Forbidden` | 403 | Permisos insuficientes (ej: swarm op en worker) |
114
+ | `NotFound` | 404 | Recurso inexistente (capturado por `find`/`destroy`) |
115
+ | `NotAcceptable` | 406 | Headers Accept incompatibles (raro) |
116
+ | `RequestTimeout` | 408 | Daemon tardó (subí `read_timeout`) |
117
+ | `Conflict` | 409 | Nombre duplicado o `Version.Index` stale |
118
+ | `UnprocessableEntity` | 422 | Payload semánticamente inválido |
119
+ | `TooManyRequests` | 429 | Rate limiting |
120
+ | `InternalServerError` | 500 | Bug Docker o update sin `version` query param |
121
+ | `BadGateway` | 502 | Proxy entre cliente y daemon falló |
122
+ | `ServiceUnavailable` | 503 | Daemon reiniciando |
123
+ | `GatewayTimeout` | 504 | Proxy timeout |
124
+ | `Communication` | — | Socket caído / inalcanzable. `cause` mantiene el `Excon::Error` original |
125
+
126
+ ## Gotchas / breaking
127
+
128
+ - **PascalCase fiel.** Los atributos NO se transforman: `service.Spec`, `service.TaskTemplate`, `service.Version` — no `snake_case`. Indifferent access soportado: `service.Spec[:Name]` ≡ `service.Spec["Name"]`.
129
+ - **`Version.Index` obligatorio en updates** (Service/Node/Network). La gema lo extrae automático de `self.Version["Index"]`. Si construís el request por afuera (`DockerSwarm.request`), tenés que pasarlo vos.
130
+ - **Retries automáticos sólo en métodos seguros** (GET/HEAD/PUT/DELETE/OPTIONS). POST/PATCH **no** reintentan para evitar duplicados — si el socket se cae durante un `create`, el caller decide qué hacer. Ver §3.5 de `docs/behavior/behavior.md`.
131
+ - **`Spec` se mergea con `deep_merge` en updates**, no se reemplaza. Pasale sólo los campos que cambian: `service.update(Mode: {...})`, no `service.update(Spec: {...completo})`.
132
+ - **`assign_attributes` muta antes de validar.** Si `update` falla por `valid?` o por el API, la instancia local quedó mutada. Hacé `reload` si necesitás estado limpio.
133
+ - **`Container.create` no existe** en la gema (gap intencional F1). Si necesitás crear containers standalone, usá `DockerSwarm.request(method: :post, path: "containers/create", ...)` directo.
134
+ - **Pull con registry privado no soportado.** `Image.create` no inyecta header `X-Registry-Auth`. Para registries privados, fallback a `DockerSwarm.request` con headers manuales.
135
+ - **`destroy` es graceful con 404** (retorna `nil`), no con 409. Si el recurso está en uso, `Conflict` se propaga.
136
+ - **Logs sensibles enmascarados** automáticamente: keys matching `password|pass|passwd|secret|token|api_key|auth|\bdata\b` → `[FILTERED]`. `\bdata\b` evita filtrar `metadata`/`database`.
137
+
138
+ ## Testing
139
+
140
+ Mockear `DockerSwarm::Api.request`:
172
141
 
173
142
  ```ruby
174
- # MAL — pierde campos existentes del Spec
175
- service.update(Spec: { Mode: { Replicated: { Replicas: 5 } } })
143
+ expect(DockerSwarm::Api).to receive(:request).with(
144
+ hash_including(action: DockerSwarm::Api::ENDPOINTS[:services][:show])
145
+ ).and_return({ "ID" => "svc-1", "Spec" => { "Name" => "web" }, "Version" => { "Index" => 1 } })
176
146
 
177
- # BIEN — mergear solo lo que cambia
178
- service.update(Mode: { Replicated: { Replicas: 5 } })
147
+ service = DockerSwarm::Service.find("svc-1")
179
148
  ```
180
- **Razón:** `payload_for_docker` extrae contenido de Spec al root. Si pasás Spec completo, `assign_attributes` hace deep_merge, pero es más limpio pasar los campos directamente.
181
149
 
182
- ### Ignorar Version.Index en updates
150
+ Para `create`: mockear **dos** llamadas (POST + show de reload). Para errores: `and_raise(DockerSwarm::NotFound)` etc.
183
151
 
184
- ```ruby
185
- # MAL — update manual sin version
186
- DockerSwarm.request(method: :post, path: "services/#{id}/update", body: payload)
187
-
188
- # BIEN — usar el modelo, maneja version automáticamente
189
- service.update(new_attrs)
190
- ```
191
- **Razón:** Docker requiere `version` query param para updates atómicos. Updatable lo extrae de `self.Version["Index"]` automáticamente.
192
-
193
- ### No capturar errores específicos
152
+ ## Integración (Rails)
194
153
 
195
154
  ```ruby
196
- # MAL
197
- begin
198
- service.destroy
199
- rescue StandardError
200
- # tragarse todo
201
- end
202
-
203
- # BIEN
204
- begin
205
- service.destroy
206
- rescue DockerSwarm::NotFound
207
- # ya fue eliminado, ok
208
- rescue DockerSwarm::Conflict => e
209
- # servicio en uso
155
+ # config/initializers/docker_swarm.rb
156
+ DockerSwarm.configure do |config|
157
+ config.socket_path = ENV.fetch("DOCKER_URL", "unix:///var/run/docker.sock")
158
+ config.logger = Rails.logger
159
+ config.log_level = Rails.logger.level
210
160
  end
211
161
  ```
212
- **Razón:** La jerarquía de errores mapea cada status HTTP. Capturar errores específicos permite manejar cada caso.
213
-
214
- ## Errores
215
162
 
216
- Los errores más comunes. Ver [Catálogo de Errores](references/errores.md) para la referencia completa.
163
+ Logs salen en formato KV (`component=docker_swarm.connection event=request_success ...`) compatible con parsers estructurados.
217
164
 
218
- | Excepción | Status | Causa típica |
219
- |-----------|--------|--------------|
220
- | `NotFound` | 404 | Recurso eliminado o ID incorrecto |
221
- | `Conflict` | 409 | Nombre duplicado o recurso en uso |
222
- | `Communication` | — | Socket caído o inalcanzable |
223
- | `ServiceUnavailable` | 503 | Docker daemon reiniciando |
165
+ ## Índice de artefactos
224
166
 
225
- Todas heredan de `DockerSwarm::Error`. Se acceden como `DockerSwarm::NotFound` (alias) o `DockerSwarm::Error::NotFound`.
167
+ - [`docs/glossary/glossary.md`](docs/glossary/glossary.md) — definición de términos (primitivas Docker + conceptos internos).
168
+ - [`docs/behavior/behavior.md`](docs/behavior/behavior.md) — secuencias load-bearing (create+reload, update+Version, retry-policy, error-mapping, etc.).
169
+ - `docs/data/` — `n/a` (gema sin DB).
170
+ - `docs/api/`, `docs/interface/`, `docs/topology/` — F2 declaradas, **no implementadas**. El contrato resumido de arriba reside **transitoriamente** acá (RFC-008 §2 coexistencia transitoria con destino pendiente).
226
171
 
227
- ## Referencias
172
+ ## Versionado del contrato
228
173
 
229
- - [API Detallada](references/api-detallada.md) Referencia completa de modelos, concerns y métodos
230
- - [Catálogo de Errores](references/errores.md) — Todas las excepciones con causa y resolución
174
+ Este SKILL.md viaja version-locked con el release de la gema (`gemspec.files` incluye `skill/**/*`). Para consumir desde otro proyecto: el agente lee `Gem.loaded_specs["docker-swarm"].gem_dir + "/skill/SKILL.md"`. Links relativos del paquete del release; no `HEAD`/branch.
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: docker-swarm
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.0
4
+ version: 0.7.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Gabriel
8
- autorequire:
9
8
  bindir: exe
10
9
  cert_chain: []
11
- date: 2026-04-08 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: activesupport
@@ -88,7 +87,12 @@ executables: []
88
87
  extensions: []
89
88
  extra_rdoc_files: []
90
89
  files:
90
+ - CHANGELOG.md
91
+ - LICENSE
91
92
  - README.md
93
+ - docs/behavior/behavior.md
94
+ - docs/config/config.md
95
+ - docs/glossary/glossary.md
92
96
  - lib/docker-swarm.rb
93
97
  - lib/docker_swarm.rb
94
98
  - lib/docker_swarm/api.rb
@@ -118,15 +122,16 @@ files:
118
122
  - lib/docker_swarm/models/volume.rb
119
123
  - lib/docker_swarm/version.rb
120
124
  - skill/SKILL.md
121
- - skill/references/api-detallada.md
122
- - skill/references/errores.md
123
- homepage: https://github.com/wispro/docker-swarm
125
+ homepage: https://github.com/gedera/docker-swarm
124
126
  licenses:
125
127
  - MIT
126
128
  metadata:
127
- homepage_uri: https://github.com/wispro/docker-swarm
128
- source_code_uri: https://github.com/wispro/docker-swarm
129
- post_install_message:
129
+ homepage_uri: https://github.com/gedera/docker-swarm
130
+ source_code_uri: https://github.com/gedera/docker-swarm
131
+ changelog_uri: https://github.com/gedera/docker-swarm/blob/v0.7.1/CHANGELOG.md
132
+ bug_tracker_uri: https://github.com/gedera/docker-swarm/issues
133
+ documentation_uri: https://github.com/gedera/docker-swarm/blob/v0.7.1/skill/SKILL.md
134
+ rubygems_mfa_required: 'true'
130
135
  rdoc_options: []
131
136
  require_paths:
132
137
  - lib
@@ -134,15 +139,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
134
139
  requirements:
135
140
  - - ">="
136
141
  - !ruby/object:Gem::Version
137
- version: 2.7.0
142
+ version: 3.2.0
138
143
  required_rubygems_version: !ruby/object:Gem::Requirement
139
144
  requirements:
140
145
  - - ">="
141
146
  - !ruby/object:Gem::Version
142
147
  version: '0'
143
148
  requirements: []
144
- rubygems_version: 3.4.19
145
- signing_key:
149
+ rubygems_version: 3.6.7
146
150
  specification_version: 4
147
151
  summary: A Ruby ORM and API client for Docker Swarm.
148
152
  test_files: []