docker-swarm 0.1.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 97102c26aff73c5426373e1cfbfb542fd2cf4e82404a3b1de4833b8e82d5d66a
4
+ data.tar.gz: 3a9288b1dc9eae6d4849503de616561c94af264ec467e9450c2a4e2863e4706f
5
+ SHA512:
6
+ metadata.gz: fb9eea1d60221c643681e50ea18a4a9830967e46ef226543c62df85c26102c9fd60b26a6f77f35f12c6e5ff86ca15ec280175ca656c266587e7737414837f91c
7
+ data.tar.gz: 0b223dd63b9c69a14a24c07b3be527876d9535c1114f53a507152fd2d28cbf126a387b0b72ddbba18290aa621c49369adaf83f2e80ca68f3d2dad8def9c749bf
data/README.md ADDED
@@ -0,0 +1,63 @@
1
+ # Docker Swarm Gem
2
+
3
+ [![Gem Version](https://badge.fury.io/rb/docker-swarm.svg)](https://badge.fury.io/rb/docker-swarm)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
+
6
+ `docker-swarm` es un ORM ligero y cliente API robusto para interactuar con Docker Swarm desde Ruby. Dise帽ado para sentirse familiar a los desarrolladores de Rails, utiliza `ActiveModel` para ofrecer una interfaz limpia y potente.
7
+
8
+ ## 馃殌 Inicio R谩pido
9
+
10
+ ```ruby
11
+ require 'docker_swarm'
12
+
13
+ # Configurar (opcional, usa defaults)
14
+ DockerSwarm.configure do |config|
15
+ config.socket_path = "unix:///var/run/docker.sock"
16
+ end
17
+
18
+ # Listar servicios
19
+ services = DockerSwarm::Service.all
20
+ services.each { |s| puts "#{s.ID}: #{s.Spec['Name']}" }
21
+
22
+ # Crear un nuevo servicio
23
+ service = DockerSwarm::Service.create(
24
+ Spec: {
25
+ Name: "my-webapp",
26
+ TaskTemplate: {
27
+ ContainerSpec: { Image: "nginx:latest" }
28
+ }
29
+ }
30
+ )
31
+
32
+ # Obtener logs
33
+ puts service.logs
34
+ ```
35
+
36
+ ## 馃摉 Documentaci贸n Completa
37
+
38
+ Para profundizar en el uso de la gema, consulta las siguientes gu铆as:
39
+
40
+ 1. **[Gu铆a de Configuraci贸n](docs/configuration.md)**: C贸mo configurar el socket, logger y opciones globales.
41
+ 2. **[Uso del ORM (Modelos)](docs/models.md)**: Todo sobre el ciclo de vida de los recursos (`Service`, `Node`, `Task`, etc.).
42
+ 3. **[Cliente de API (Bajo Nivel)](docs/api.md)**: C贸mo realizar peticiones personalizadas directamente a la API de Docker.
43
+ 4. **[Manejo de Errores](docs/errors.md)**: Jerarqu铆a de excepciones y mapeo de errores de Docker.
44
+ 5. **[Pruebas y Mocking](docs/testing.md)**: Gu铆a para testear tu aplicaci贸n sin depender de un socket de Docker real.
45
+
46
+ ## 馃洜 Caracter铆sticas Clave
47
+
48
+ - **Mapeo PascalCase**: Mantiene la fidelidad con los atributos de Docker (e.g., `s.ID`, `s.Spec`) evitando transformaciones costosas.
49
+ - **ActiveModel Ready**: Soporta validaciones, serializaci贸n JSON y comportamientos est谩ndar de modelos Ruby.
50
+ - **Surgical Updates**: Actualizaciones precisas enviando solo el 铆ndice de versi贸n y el payload necesario.
51
+ - **Excon Stack**: Basado en `Excon` con middlewares para encoding de peticiones, parseo de respuestas y gesti贸n de errores.
52
+
53
+ ## 馃 Contribuir
54
+
55
+ Las contribuciones son bienvenidas. Por favor, lee `CLAUDE.md` para las gu铆as de desarrollo y aseg煤rate de que todos los tests pasen antes de enviar un PR.
56
+
57
+ ```bash
58
+ bundle exec rspec
59
+ ```
60
+
61
+ ## 馃搫 Licencia
62
+
63
+ Este proyecto est谩 bajo la licencia MIT.
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DockerSwarm
4
+ class Api
5
+ ENDPOINTS = {
6
+ swarm: { show: { method: :get, path: "swarm" } },
7
+ system: {
8
+ info: { method: :get, path: "info" },
9
+ version: { method: :get, path: "version" },
10
+ up: { method: :get, path: "_ping" },
11
+ df: { method: :get, path: "system/df" }
12
+ },
13
+ nodes: {
14
+ index: { method: :get, path: "nodes" },
15
+ show: { method: :get, path: "nodes/%<id>s" },
16
+ update: { method: :post, path: "nodes/%<id>s/update" },
17
+ destroy: { method: :delete, path: "nodes/%<id>s" }
18
+ },
19
+ tasks: {
20
+ index: { method: :get, path: "tasks" },
21
+ show: { method: :get, path: "tasks/%<id>s" },
22
+ logs: { method: :get, path: "tasks/%<id>s/logs" }
23
+ },
24
+ services: {
25
+ index: { method: :get, path: "services" },
26
+ show: { method: :get, path: "services/%<id>s" },
27
+ create: { method: :post, path: "services/create" },
28
+ update: { method: :post, path: "services/%<id>s/update" },
29
+ destroy: { method: :delete, path: "services/%<id>s" },
30
+ logs: { method: :get, path: "services/%<id>s/logs" }
31
+ },
32
+ configs: {
33
+ index: { method: :get, path: "configs" },
34
+ show: { method: :get, path: "configs/%<id>s" },
35
+ create: { method: :post, path: "configs/create" },
36
+ destroy: { method: :delete, path: "configs/%<id>s" }
37
+ },
38
+ secrets: {
39
+ index: { method: :get, path: "secrets" },
40
+ show: { method: :get, path: "secrets/%<id>s" },
41
+ create: { method: :post, path: "secrets/create" },
42
+ destroy: { method: :delete, path: "secrets/%<id>s" }
43
+ },
44
+ networks: {
45
+ index: { method: :get, path: "networks" },
46
+ show: { method: :get, path: "networks/%<id>s" },
47
+ create: { method: :post, path: "networks/create" },
48
+ update: { method: :post, path: "networks/%<id>s/update" },
49
+ destroy: { method: :delete, path: "networks/%<id>s" }
50
+ },
51
+ volumes: {
52
+ index: { method: :get, path: "volumes" },
53
+ show: { method: :get, path: "volumes/%<id>s" },
54
+ create: { method: :post, path: "volumes/create" },
55
+ destroy: { method: :delete, path: "volumes/%<id>s" }
56
+ },
57
+ containers: {
58
+ index: { method: :get, path: "containers/json" },
59
+ show: { method: :get, path: "containers/%<id>s/json" },
60
+ create: { method: :post, path: "containers/create" },
61
+ start: { method: :post, path: "containers/%<id>s/start" },
62
+ stop: { method: :post, path: "containers/%<id>s/stop" },
63
+ destroy: { method: :delete, path: "containers/%<id>s" },
64
+ logs: { method: :get, path: "containers/%<id>s/logs" }
65
+ },
66
+ images: {
67
+ index: { method: :get, path: "images/json" },
68
+ show: { method: :get, path: "images/%<id>s/json" },
69
+ create: { method: :post, path: "images/create?fromImage=%<id>s" },
70
+ destroy: { method: :delete, path: "images/%<id>s" }
71
+ }
72
+ }.freeze
73
+
74
+ def self.request(action:, arguments: {}, query_params: {}, payload: nil)
75
+ path = format(action[:path], arguments)
76
+ DockerSwarm.request(
77
+ method: action[:method],
78
+ path: path,
79
+ query: query_params,
80
+ body: payload
81
+ )
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,156 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DockerSwarm
4
+ class Base
5
+ include ActiveModel::Model
6
+
7
+ class << self
8
+ def resource_name
9
+ name.demodulize.downcase.pluralize
10
+ end
11
+
12
+ def routes
13
+ Api::ENDPOINTS[resource_name.to_sym]
14
+ end
15
+
16
+ def all(filters = {})
17
+ _fetch_all(filters).map { |data| new(data) }
18
+ end
19
+
20
+ def find(id)
21
+ data = Api.request(action: routes[:show], arguments: { id: id })
22
+ new(data)
23
+ rescue Errors::NotFound
24
+ nil
25
+ end
26
+
27
+ def where(filters)
28
+ all(filters)
29
+ end
30
+
31
+ private
32
+
33
+ def _fetch_all(filters = {})
34
+ query = {}
35
+
36
+ if filters.present?
37
+ global_params = filters.slice(:all, :force, :limit, :since, :before)
38
+ docker_filters = filters.except(:all, :force, :limit, :since, :before)
39
+
40
+ query = global_params
41
+
42
+ if docker_filters.any?
43
+ normalized = docker_filters.transform_keys { |k| k.to_s.downcase }
44
+ .transform_values { |v| Array(v) }
45
+ query[:filters] = normalized.to_json
46
+ end
47
+ end
48
+
49
+ Api.request(action: routes[:index], query_params: query)
50
+ end
51
+ end
52
+
53
+ def initialize(attributes = {})
54
+ assign_attributes(attributes) if attributes.present?
55
+ super()
56
+ end
57
+
58
+ def assign_attributes(new_attributes)
59
+ return if new_attributes.blank?
60
+
61
+ attributes_to_assign = new_attributes.deep_dup
62
+
63
+ if attributes_to_assign.is_a?(Hash)
64
+ attributes_to_assign = attributes_to_assign.with_indifferent_access
65
+ normalized_attributes = {}
66
+
67
+ attributes_to_assign.each do |key, value|
68
+ normalized_key = key.to_s == "Id" ? "ID" : key.to_s
69
+ _define_dynamic_accessor(normalized_key)
70
+
71
+ if normalized_key == "Spec" && respond_to?(:Spec) && self.Spec.is_a?(Hash) && value.is_a?(Hash)
72
+ value = self.Spec.deep_merge(value)
73
+ end
74
+
75
+ normalized_attributes[normalized_key] = value
76
+ end
77
+
78
+ super(normalized_attributes)
79
+ else
80
+ Array(attributes_to_assign).each do |item|
81
+ next unless item.is_a?(Hash)
82
+ item.each_key { |key| _define_dynamic_accessor(key) }
83
+ end
84
+ end
85
+ end
86
+
87
+ def attributes
88
+ instance_values.except("validation_context", "errors", "context_for_validation")
89
+ end
90
+
91
+ def serializable_hash(_options = nil)
92
+ attributes
93
+ end
94
+
95
+ def as_json(options = nil)
96
+ serializable_hash(options)
97
+ end
98
+
99
+ def payload_for_docker
100
+ data = attributes.except("ID", "Id", "Version", "CreatedAt", "UpdatedAt").compact
101
+ return data unless data.key?("Spec")
102
+
103
+ spec = data.delete("Spec").deep_dup
104
+ spec.merge!(data)
105
+ end
106
+
107
+ def persisted?
108
+ respond_to?(:ID) && self.ID.present?
109
+ end
110
+
111
+ def id
112
+ self.ID
113
+ end
114
+
115
+ def reload
116
+ fresh = self.class.find(id)
117
+ assign_attributes(fresh.attributes) if fresh
118
+ self
119
+ end
120
+
121
+ def method_missing(method_name, *args, &block)
122
+ method_str = method_name.to_s
123
+
124
+ if method_str.end_with?("=")
125
+ _define_dynamic_accessor(method_str.chomp("="))
126
+ instance_variable_set("@#{method_str.chomp('=')}", args.first)
127
+ elsif _valid_attribute_name?(method_str) && instance_variable_defined?("@#{method_str}")
128
+ instance_variable_get("@#{method_str}")
129
+ elsif !method_str.end_with?("?")
130
+ _define_dynamic_accessor(method_str)
131
+ nil
132
+ else
133
+ super
134
+ end
135
+ end
136
+
137
+ def respond_to_missing?(method_name, include_private = false)
138
+ method_str = method_name.to_s
139
+ method_str.end_with?("=") ||
140
+ (_valid_attribute_name?(method_str) && instance_variable_defined?("@#{method_str}")) ||
141
+ (!method_str.end_with?("?") && _valid_attribute_name?(method_str)) ||
142
+ super
143
+ end
144
+
145
+ private
146
+
147
+ def _define_dynamic_accessor(key)
148
+ return if self.class.method_defined?("#{key}=")
149
+ self.class.send(:attr_accessor, key)
150
+ end
151
+
152
+ def _valid_attribute_name?(name)
153
+ /\A[a-zA-Z_]\w*\z/.match?(name)
154
+ end
155
+ end
156
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DockerSwarm
4
+ module Concerns
5
+ module Creatable
6
+ extend ActiveSupport::Concern
7
+
8
+ class_methods do
9
+ def create(attributes = {})
10
+ resource = new(attributes)
11
+ resource.save
12
+ resource
13
+ end
14
+ end
15
+
16
+ def save
17
+ return false unless valid?
18
+ return update if persisted?
19
+
20
+ response = Api.request(
21
+ action: self.class.routes[:create],
22
+ payload: payload_for_docker
23
+ )
24
+
25
+ self.ID = response["ID"] || response["Id"] || response["Name"]
26
+ reload
27
+ true
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DockerSwarm
4
+ module Concerns
5
+ module Deletable
6
+ extend ActiveSupport::Concern
7
+
8
+ class_methods do
9
+ def destroy(id)
10
+ Api.request(action: routes[:destroy], arguments: { id: id })
11
+ true
12
+ rescue Errors::NotFound
13
+ nil
14
+ end
15
+ end
16
+
17
+ def destroy
18
+ self.class.destroy(self.ID)
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DockerSwarm
4
+ module Concerns
5
+ module Updatable
6
+ extend ActiveSupport::Concern
7
+
8
+ def update(new_attributes = {})
9
+ assign_attributes(new_attributes) if new_attributes.present?
10
+ return false unless valid?
11
+
12
+ Api.request(
13
+ action: self.class.routes[:update],
14
+ arguments: { id: self.ID },
15
+ query_params: { version: self.Version&.dig("Index") },
16
+ payload: payload_for_docker
17
+ )
18
+
19
+ true
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DockerSwarm
4
+ class Connection
5
+ attr_reader :socket_path, :logger
6
+
7
+ def initialize(socket_path, logger)
8
+ @socket_path = socket_path
9
+ @logger = logger
10
+ end
11
+
12
+ def request(options = {})
13
+ normalized_path = socket_path.sub(/^unix:\/\//, "")
14
+ client(normalized_path).request(options).body
15
+ rescue => e
16
+ if e.is_a?(Excon::Error::Socket)
17
+ raise Errors::Communication, "Docker socket error: #{e.message}"
18
+ else
19
+ raise e
20
+ end
21
+ end
22
+ private
23
+
24
+ def client(path)
25
+ @client ||= Excon.new(
26
+ "unix:///",
27
+ socket: path,
28
+ middlewares: [
29
+ Excon::Middleware::ResponseParser,
30
+ Excon::Middleware::RedirectFollower,
31
+ Middleware::RequestEncoder,
32
+ Middleware::ResponseJSONParser,
33
+ Middleware::ErrorHandler
34
+ ]
35
+ )
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DockerSwarm
4
+ class Error < StandardError; end
5
+
6
+ module Errors
7
+ class BadRequest < Error; end
8
+ class Unauthorized < Error; end
9
+ class Forbidden < Error; end
10
+ class NotFound < Error; end
11
+ class NotAcceptable < Error; end
12
+ class RequestTimeout < Error; end
13
+ class Conflict < Error; end
14
+ class UnprocessableEntity < Error; end
15
+ class InternalServerError < Error; end
16
+ class BadGateway < Error; end
17
+ class ServiceUnavailable < Error; end
18
+ class GatewayTimeout < Error; end
19
+ class TooManyRequests < Error; end
20
+ class Communication < Error; end
21
+ end
22
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DockerSwarm
4
+ module Middleware
5
+ class ErrorHandler < Excon::Middleware::Base
6
+ def response_call(env)
7
+ return @stack.response_call(env) unless env[:response]
8
+
9
+ status = env[:response][:status]
10
+ body = env[:response][:body]
11
+
12
+ case status
13
+ when 200..299
14
+ # Continuar normalmente
15
+ when 400 then raise Errors::BadRequest, error_message(body)
16
+ when 401 then raise Errors::Unauthorized, error_message(body)
17
+ when 403 then raise Errors::Forbidden, error_message(body)
18
+ when 404 then raise Errors::NotFound, error_message(body)
19
+ when 406 then raise Errors::NotAcceptable, error_message(body)
20
+ when 408 then raise Errors::RequestTimeout, error_message(body)
21
+ when 409 then raise Errors::Conflict, error_message(body)
22
+ when 422 then raise Errors::UnprocessableEntity, body
23
+ when 429 then raise Errors::TooManyRequests, error_message(body)
24
+ when 500 then raise Errors::InternalServerError, error_message(body)
25
+ when 502 then raise Errors::BadGateway, error_message(body)
26
+ when 503 then raise Errors::ServiceUnavailable, error_message(body)
27
+ when 504 then raise Errors::GatewayTimeout, error_message(body)
28
+ else
29
+ raise Errors::Error, "HTTP #{status}: #{error_message(body)}"
30
+ end
31
+
32
+ @stack.response_call(env)
33
+ end
34
+
35
+ private
36
+
37
+ def error_message(body)
38
+ if body.is_a?(Hash)
39
+ body["message"] || body["error"] || body.to_json
40
+ else
41
+ body.to_s
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DockerSwarm
4
+ module Middleware
5
+ class RequestEncoder < Excon::Middleware::Base
6
+ def request_call(env)
7
+ if env[:body] && env[:body].is_a?(Hash)
8
+ env[:body] = env[:body].to_json
9
+ env[:headers]["Content-Type"] ||= "application/json"
10
+ end
11
+ @stack.request_call(env)
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DockerSwarm
4
+ module Middleware
5
+ class ResponseJSONParser < Excon::Middleware::Base
6
+ def response_call(env)
7
+ if env[:response]
8
+ body = env[:response][:body]
9
+ headers = env[:response][:headers] || {}
10
+
11
+ if body && !body.empty? && headers["Content-Type"]&.include?("application/json")
12
+ begin
13
+ parsed = JSON.parse(body)
14
+ env[:response][:body] = parsed.is_a?(Hash) ? parsed.with_indifferent_access : parsed
15
+ rescue JSON::ParserError
16
+ # Keep original body if parsing fails
17
+ end
18
+ end
19
+ end
20
+
21
+ @stack.response_call(env)
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DockerSwarm
4
+ class Config < Base
5
+ include Concerns::Creatable
6
+ include Concerns::Deletable
7
+ end
8
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DockerSwarm
4
+ class Container < Base
5
+ include Concerns::Deletable
6
+
7
+ def start
8
+ Api.request(action: self.class.routes[:start], arguments: { id: self.ID })
9
+ end
10
+
11
+ def stop
12
+ Api.request(action: self.class.routes[:stop], arguments: { id: self.ID })
13
+ end
14
+
15
+ def logs(query_params = { stdout: 1, stderr: 1 })
16
+ Api.request(action: self.class.routes[:logs], arguments: { id: self.ID }, query_params: query_params)
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DockerSwarm
4
+ class Image < Base
5
+ include Concerns::Creatable
6
+ include Concerns::Deletable
7
+ end
8
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DockerSwarm
4
+ class Network < Base
5
+ include Concerns::Creatable
6
+ include Concerns::Updatable
7
+ include Concerns::Deletable
8
+
9
+ validate :validate_name_presence
10
+
11
+ private
12
+
13
+ def validate_name_presence
14
+ name = attributes["Name"] || (respond_to?(:Name) ? Name : nil)
15
+ errors.add(:Name, "can't be blank") if name.blank?
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DockerSwarm
4
+ class Node < Base
5
+ include Concerns::Updatable
6
+ include Concerns::Deletable
7
+ end
8
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DockerSwarm
4
+ class Secret < Base
5
+ include Concerns::Creatable
6
+ include Concerns::Deletable
7
+ end
8
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DockerSwarm
4
+ # Represents a Docker Swarm Service
5
+ # @see https://docs.docker.com/engine/api/v1.41/#tag/Service
6
+ class Service < Base
7
+ include Concerns::Creatable
8
+ include Concerns::Updatable
9
+ include Concerns::Deletable
10
+
11
+ validate :validate_name_presence
12
+
13
+ # Fetches logs for the service
14
+ # @param query_params [Hash] Query parameters for the logs endpoint (stdout, stderr, follow, etc.)
15
+ # @return [String] The raw log stream
16
+ def logs(query_params = { stdout: 1, stderr: 1 })
17
+ Api.request(action: self.class.routes[:logs], arguments: { id: self.ID }, query_params: query_params)
18
+ end
19
+
20
+ private
21
+
22
+ def validate_name_presence
23
+ name = attributes["Name"] || (respond_to?(:Name) ? Name : nil)
24
+ errors.add(:Name, "can't be blank") if name.blank?
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DockerSwarm
4
+ class Swarm < Base
5
+ def self.resource_name
6
+ "swarm"
7
+ end
8
+
9
+ def self.show
10
+ Api.request(action: routes[:show])
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DockerSwarm
4
+ class System < Base
5
+ def self.resource_name
6
+ "system"
7
+ end
8
+
9
+ def self.info
10
+ Api.request(action: routes[:info])
11
+ end
12
+
13
+ def self.version
14
+ Api.request(action: routes[:version])
15
+ end
16
+
17
+ def self.up
18
+ Api.request(action: routes[:up])
19
+ end
20
+
21
+ def self.df
22
+ Api.request(action: routes[:df])
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DockerSwarm
4
+ class Task < Base
5
+ def logs(query_params = { stdout: 1, stderr: 1 })
6
+ Api.request(action: self.class.routes[:logs], arguments: { id: self.ID }, query_params: query_params)
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DockerSwarm
4
+ class Volume < Base
5
+ include Concerns::Creatable
6
+ include Concerns::Deletable
7
+
8
+ def self.all(filters = {})
9
+ response = _fetch_all(filters)
10
+ return [] if response.blank?
11
+
12
+ data = response.is_a?(Hash) ? response["Volumes"] : response
13
+ Array(data).map { |item| new(item) }
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DockerSwarm
4
+ VERSION = "0.1.0"
5
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support"
4
+ require "active_support/core_ext"
5
+ require "active_model"
6
+ require "excon"
7
+ require "json"
8
+ require "logger"
9
+
10
+ require_relative "docker_swarm/version"
11
+ require_relative "docker_swarm/errors"
12
+ require_relative "docker_swarm/middleware/request_encoder"
13
+ require_relative "docker_swarm/middleware/response_json_parser"
14
+ require_relative "docker_swarm/middleware/error_handler"
15
+ require_relative "docker_swarm/connection"
16
+ require_relative "docker_swarm/api"
17
+ require_relative "docker_swarm/base"
18
+ require_relative "docker_swarm/concerns/creatable"
19
+ require_relative "docker_swarm/concerns/updatable"
20
+ require_relative "docker_swarm/concerns/deletable"
21
+
22
+ # Models
23
+ require_relative "docker_swarm/models/swarm"
24
+ require_relative "docker_swarm/models/system"
25
+ require_relative "docker_swarm/models/service"
26
+ require_relative "docker_swarm/models/node"
27
+ require_relative "docker_swarm/models/task"
28
+ require_relative "docker_swarm/models/container"
29
+ require_relative "docker_swarm/models/image"
30
+ require_relative "docker_swarm/models/network"
31
+ require_relative "docker_swarm/models/config"
32
+ require_relative "docker_swarm/models/secret"
33
+ require_relative "docker_swarm/models/volume"
34
+
35
+ module DockerSwarm
36
+ class << self
37
+ attr_accessor :configuration
38
+
39
+ def configure
40
+ self.configuration ||= Configuration.new
41
+ yield(configuration) if block_given?
42
+ end
43
+
44
+ def connection
45
+ configure unless configuration
46
+ @connection ||= Connection.new(configuration.socket_path, configuration.logger)
47
+ end
48
+
49
+ def request(options = {})
50
+ connection.request(options)
51
+ end
52
+ end
53
+
54
+ class Configuration
55
+ attr_accessor :socket_path, :logger
56
+
57
+ def initialize
58
+ @socket_path = "unix:///var/run/docker.sock"
59
+ @logger = defined?(Rails) ? Rails.logger : Logger.new($stdout)
60
+ end
61
+ end
62
+ end
metadata ADDED
@@ -0,0 +1,140 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: docker-swarm
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Gabriel
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2026-03-31 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activesupport
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '6.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '6.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: activemodel
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '6.0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '6.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: excon
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0.80'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0.80'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rake
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '13.0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '13.0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rspec
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '3.0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '3.0'
83
+ description: Simplifies interactions with Docker Swarm through an ActiveModel-compatible
84
+ ORM and a robust Excon-based API client.
85
+ email:
86
+ - gabriel@wispro.co
87
+ executables: []
88
+ extensions: []
89
+ extra_rdoc_files: []
90
+ files:
91
+ - README.md
92
+ - lib/docker_swarm.rb
93
+ - lib/docker_swarm/api.rb
94
+ - lib/docker_swarm/base.rb
95
+ - lib/docker_swarm/concerns/creatable.rb
96
+ - lib/docker_swarm/concerns/deletable.rb
97
+ - lib/docker_swarm/concerns/updatable.rb
98
+ - lib/docker_swarm/connection.rb
99
+ - lib/docker_swarm/errors.rb
100
+ - lib/docker_swarm/middleware/error_handler.rb
101
+ - lib/docker_swarm/middleware/request_encoder.rb
102
+ - lib/docker_swarm/middleware/response_json_parser.rb
103
+ - lib/docker_swarm/models/config.rb
104
+ - lib/docker_swarm/models/container.rb
105
+ - lib/docker_swarm/models/image.rb
106
+ - lib/docker_swarm/models/network.rb
107
+ - lib/docker_swarm/models/node.rb
108
+ - lib/docker_swarm/models/secret.rb
109
+ - lib/docker_swarm/models/service.rb
110
+ - lib/docker_swarm/models/swarm.rb
111
+ - lib/docker_swarm/models/system.rb
112
+ - lib/docker_swarm/models/task.rb
113
+ - lib/docker_swarm/models/volume.rb
114
+ - lib/docker_swarm/version.rb
115
+ homepage: https://github.com/wispro/docker-swarm
116
+ licenses:
117
+ - MIT
118
+ metadata:
119
+ homepage_uri: https://github.com/wispro/docker-swarm
120
+ source_code_uri: https://github.com/wispro/docker-swarm
121
+ post_install_message:
122
+ rdoc_options: []
123
+ require_paths:
124
+ - lib
125
+ required_ruby_version: !ruby/object:Gem::Requirement
126
+ requirements:
127
+ - - ">="
128
+ - !ruby/object:Gem::Version
129
+ version: 2.7.0
130
+ required_rubygems_version: !ruby/object:Gem::Requirement
131
+ requirements:
132
+ - - ">="
133
+ - !ruby/object:Gem::Version
134
+ version: '0'
135
+ requirements: []
136
+ rubygems_version: 3.4.19
137
+ signing_key:
138
+ specification_version: 4
139
+ summary: A Ruby ORM and API client for Docker Swarm.
140
+ test_files: []