docker-swarm 0.1.0 → 0.2.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: 97102c26aff73c5426373e1cfbfb542fd2cf4e82404a3b1de4833b8e82d5d66a
4
- data.tar.gz: 3a9288b1dc9eae6d4849503de616561c94af264ec467e9450c2a4e2863e4706f
3
+ metadata.gz: b43832405fbf723559703102c4d84a42c27c677d601625de43c81ceb2db804d0
4
+ data.tar.gz: 57d9349d4ce67bc9e157c6d235e5cf4925b0af05df53ce0f29fffd2bd888fbb7
5
5
  SHA512:
6
- metadata.gz: fb9eea1d60221c643681e50ea18a4a9830967e46ef226543c62df85c26102c9fd60b26a6f77f35f12c6e5ff86ca15ec280175ca656c266587e7737414837f91c
7
- data.tar.gz: 0b223dd63b9c69a14a24c07b3be527876d9535c1114f53a507152fd2d28cbf126a387b0b72ddbba18290aa621c49369adaf83f2e80ca68f3d2dad8def9c749bf
6
+ metadata.gz: 72c9fa985935caac2386f9b782ac3728f4c5ffbd121db6f5b36a6e1291eafed9bc5500bd0841385caad69ad5430152b5d11d10261c4ad73fbda86bd9cc7053b3
7
+ data.tar.gz: e841eaa3cad32138f809a40556a2c798fb97e7c92bf40975d2076adbd6650a699eb5635880f6c50d598de2c1017045d61771ff88f7a794f8482a71bdb32b7fe3
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "docker_swarm"
@@ -13,8 +13,19 @@ module DockerSwarm
13
13
  Api::ENDPOINTS[resource_name.to_sym]
14
14
  end
15
15
 
16
+ # Key in the JSON response that contains the array of items.
17
+ # Override in subclasses if the API returns a wrapped object (e.g., Volumes).
18
+ # @return [String, nil]
19
+ def root_key
20
+ nil
21
+ end
22
+
16
23
  def all(filters = {})
17
- _fetch_all(filters).map { |data| new(data) }
24
+ response = _fetch_all(filters)
25
+ return [] if response.blank?
26
+
27
+ data = root_key && response.is_a?(Hash) ? response[root_key] : response
28
+ Array(data).map { |item| new(item) }
18
29
  end
19
30
 
20
31
  def find(id)
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "logger"
4
+
5
+ module DockerSwarm
6
+ class Configuration
7
+ attr_accessor :socket_path, :logger, :log_level
8
+
9
+ def initialize
10
+ @socket_path = "unix:///var/run/docker.sock"
11
+ @logger = Logger.new($stdout)
12
+ @log_level = Logger::INFO
13
+ end
14
+ end
15
+ end
@@ -10,29 +10,110 @@ module DockerSwarm
10
10
  end
11
11
 
12
12
  def request(options = {})
13
- normalized_path = socket_path.sub(/^unix:\/\//, "")
14
- client(normalized_path).request(options).body
13
+ start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
14
+
15
+ log_event("request_started",
16
+ level: :debug,
17
+ data: { method: options[:method], path: options[:path] })
18
+
19
+ response = client.request(options)
20
+
21
+ log_event("request_success",
22
+ data: {
23
+ method: options[:method],
24
+ path: options[:path],
25
+ status: response.status,
26
+ duration_ms: calculate_duration(start_time)
27
+ })
28
+
29
+ response.body
15
30
  rescue => e
16
- if e.is_a?(Excon::Error::Socket)
17
- raise Errors::Communication, "Docker socket error: #{e.message}"
31
+ # Excon suele envolver excepciones de middleware en Excon::Error::Socket.
32
+ # Intentamos recuperar la causa original si es una de nuestras excepciones.
33
+ actual_error = (e.respond_to?(:cause) && e.cause&.class&.name&.include?("DockerSwarm::Error")) ? e.cause : e
34
+
35
+ log_event("request_failure",
36
+ level: :error,
37
+ data: {
38
+ method: options[:method],
39
+ path: options[:path],
40
+ error: actual_error.class.name,
41
+ message: actual_error.message,
42
+ duration_ms: calculate_duration(start_time)
43
+ })
44
+
45
+ if actual_error.class.name.include?("DockerSwarm::Error")
46
+ raise actual_error
47
+ elsif actual_error.is_a?(Excon::Error::Socket)
48
+ raise ::DockerSwarm::Error::Communication, "Docker socket error: #{actual_error.message}"
18
49
  else
19
- raise e
50
+ raise actual_error
20
51
  end
21
52
  end
53
+
22
54
  private
23
55
 
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
- )
56
+ def log_event(event, level: :info, data: {})
57
+ return unless logger
58
+ # Respetar el nivel de log antes de procesar nada
59
+ return if level == :debug && logger.level > Logger::DEBUG
60
+
61
+ log_block = proc do
62
+ begin
63
+ payload = {
64
+ component: "docker_swarm.connection",
65
+ event: event,
66
+ source: "http"
67
+ }.merge(data)
68
+
69
+ payload.map do |k, v|
70
+ val = k.to_s =~ /password|token|api_key|auth|secret/i ? "[FILTERED]" : v
71
+ "#{k}=#{val}"
72
+ end.join(" ")
73
+ rescue
74
+ "component=docker_swarm.connection event=logging_error"
75
+ end
76
+ end
77
+
78
+ if level == :debug
79
+ logger.debug(&log_block)
80
+ else
81
+ logger.send(level, log_block.call)
82
+ end
83
+ end
84
+
85
+ def calculate_duration(start_time)
86
+ ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time) * 1000).round(2)
87
+ end
88
+
89
+ def common_middlewares
90
+ # Usamos los middlewares por defecto de Excon para garantizar que todas las llaves internas
91
+ # (como :retries_remaining o :instrumentor_name) se inicialicen correctamente.
92
+ Excon.defaults[:middlewares] + [
93
+ Excon::Middleware::RedirectFollower,
94
+ Middleware::RequestEncoder,
95
+ Middleware::ResponseJSONParser,
96
+ Middleware::ErrorHandler
97
+ ]
98
+ end
99
+
100
+ def client
101
+ debug_enabled = logger&.level == Logger::DEBUG
102
+
103
+ options = {
104
+ middlewares: common_middlewares,
105
+ logger: logger,
106
+ debug_request: debug_enabled,
107
+ debug_response: debug_enabled,
108
+ # Si debug_enabled es true, Excon usará su lógica interna de debug con el logger proporcionado
109
+ retry_limit: 0
110
+ }
111
+
112
+ @client ||= if socket_path.start_with?("unix://")
113
+ Excon.new("unix:///", options.merge(socket: socket_path.sub(/^unix:\/\//, "")))
114
+ else
115
+ Excon.new(socket_path, options)
116
+ end
36
117
  end
37
118
  end
38
119
  end
@@ -3,7 +3,7 @@
3
3
  module DockerSwarm
4
4
  class Error < StandardError; end
5
5
 
6
- module Errors
6
+ class Error
7
7
  class BadRequest < Error; end
8
8
  class Unauthorized < Error; end
9
9
  class Forbidden < Error; end
@@ -19,4 +19,31 @@ module DockerSwarm
19
19
  class TooManyRequests < Error; end
20
20
  class Communication < Error; end
21
21
  end
22
+
23
+ # Aliases para permitir acceso directo DockerSwarm::Conflict
24
+ BadRequest = Error::BadRequest
25
+ Unauthorized = Error::Unauthorized
26
+ Forbidden = Error::Forbidden
27
+ NotFound = Error::NotFound
28
+ NotAcceptable = Error::NotAcceptable
29
+ RequestTimeout = Error::RequestTimeout
30
+ Conflict = Error::Conflict
31
+ UnprocessableEntity = Error::UnprocessableEntity
32
+ InternalServerError = Error::InternalServerError
33
+ BadGateway = Error::BadGateway
34
+ ServiceUnavailable = Error::ServiceUnavailable
35
+ GatewayTimeout = Error::GatewayTimeout
36
+ TooManyRequests = Error::TooManyRequests
37
+ Communication = Error::Communication
38
+
39
+ # Módulo Errors para compatibilidad adicional
40
+ module Errors
41
+ def self.const_missing(name)
42
+ if ::DockerSwarm::Error.const_defined?(name)
43
+ ::DockerSwarm::Error.const_get(name)
44
+ else
45
+ super
46
+ end
47
+ end
48
+ end
22
49
  end
@@ -9,31 +9,58 @@ module DockerSwarm
9
9
  status = env[:response][:status]
10
10
  body = env[:response][:body]
11
11
 
12
+ return @stack.response_call(env) if (200..299).include?(status)
13
+
14
+ error_msg = error_message(body)
15
+ log_business_error(env, status, error_msg)
16
+
12
17
  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)
18
+ when 400 then raise ::DockerSwarm::BadRequest, error_msg
19
+ when 401 then raise ::DockerSwarm::Unauthorized, error_msg
20
+ when 403 then raise ::DockerSwarm::Forbidden, error_msg
21
+ when 404 then raise ::DockerSwarm::NotFound, error_msg
22
+ when 406 then raise ::DockerSwarm::NotAcceptable, error_msg
23
+ when 408 then raise ::DockerSwarm::RequestTimeout, error_msg
24
+ when 409 then raise ::DockerSwarm::Conflict, error_msg
25
+ when 422 then raise ::DockerSwarm::UnprocessableEntity, body
26
+ when 429 then raise ::DockerSwarm::TooManyRequests, error_msg
27
+ when 500 then raise ::DockerSwarm::InternalServerError, error_msg
28
+ when 502 then raise ::DockerSwarm::BadGateway, error_msg
29
+ when 503 then raise ::DockerSwarm::ServiceUnavailable, error_msg
30
+ when 504 then raise ::DockerSwarm::GatewayTimeout, error_msg
28
31
  else
29
- raise Errors::Error, "HTTP #{status}: #{error_message(body)}"
32
+ raise ::DockerSwarm::Error, "HTTP #{status}: #{error_msg}"
30
33
  end
31
-
32
- @stack.response_call(env)
33
34
  end
34
35
 
35
36
  private
36
37
 
38
+ def log_business_error(env, status, message)
39
+ logger = env[:logger]
40
+ return unless logger
41
+
42
+ begin
43
+ payload = {
44
+ component: "docker_swarm.middleware.error_handler",
45
+ event: "business_error",
46
+ source: "http",
47
+ status: status,
48
+ message: message,
49
+ method: env[:method],
50
+ path: env[:path]
51
+ }
52
+
53
+ kv_string = payload.map do |k, v|
54
+ val = k.to_s =~ /password|token|api_key|auth|secret/i ? "[FILTERED]" : v
55
+ "#{k}=#{val}"
56
+ end.join(" ")
57
+
58
+ logger.error(kv_string)
59
+ rescue
60
+ # Resilience: logging should not crash the app
61
+ end
62
+ end
63
+
37
64
  def error_message(body)
38
65
  if body.is_a?(Hash)
39
66
  body["message"] || body["error"] || body.to_json
@@ -5,12 +5,8 @@ module DockerSwarm
5
5
  include Concerns::Creatable
6
6
  include Concerns::Deletable
7
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) }
8
+ def self.root_key
9
+ "Volumes"
14
10
  end
15
11
  end
16
12
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DockerSwarm
4
- VERSION = "0.1.0"
4
+ VERSION = "0.2.0"
5
5
  end
data/lib/docker_swarm.rb CHANGED
@@ -7,6 +7,34 @@ require "excon"
7
7
  require "json"
8
8
  require "logger"
9
9
 
10
+ module DockerSwarm
11
+ class << self
12
+ attr_accessor :configuration
13
+
14
+ def configure
15
+ @configuration ||= Configuration.new
16
+ yield(@configuration) if block_given?
17
+
18
+ if @configuration.logger.respond_to?(:level=)
19
+ @configuration.logger.level = @configuration.log_level
20
+ end
21
+
22
+ @connection = nil
23
+ end
24
+
25
+ def connection
26
+ configure unless @configuration
27
+ @connection ||= Connection.new(@configuration.socket_path, @configuration.logger)
28
+ end
29
+
30
+ def request(options = {})
31
+ connection.request(options)
32
+ end
33
+ end
34
+ end
35
+
36
+ # Primero cargamos la configuración porque la clase DockerSwarm la usa arriba
37
+ require_relative "docker_swarm/configuration"
10
38
  require_relative "docker_swarm/version"
11
39
  require_relative "docker_swarm/errors"
12
40
  require_relative "docker_swarm/middleware/request_encoder"
@@ -31,32 +59,3 @@ require_relative "docker_swarm/models/network"
31
59
  require_relative "docker_swarm/models/config"
32
60
  require_relative "docker_swarm/models/secret"
33
61
  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 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.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Gabriel
@@ -89,12 +89,14 @@ extensions: []
89
89
  extra_rdoc_files: []
90
90
  files:
91
91
  - README.md
92
+ - lib/docker-swarm.rb
92
93
  - lib/docker_swarm.rb
93
94
  - lib/docker_swarm/api.rb
94
95
  - lib/docker_swarm/base.rb
95
96
  - lib/docker_swarm/concerns/creatable.rb
96
97
  - lib/docker_swarm/concerns/deletable.rb
97
98
  - lib/docker_swarm/concerns/updatable.rb
99
+ - lib/docker_swarm/configuration.rb
98
100
  - lib/docker_swarm/connection.rb
99
101
  - lib/docker_swarm/errors.rb
100
102
  - lib/docker_swarm/middleware/error_handler.rb