hetznercloud 2.1.0 → 2.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c90b55b1202ba500b10d67653c2c0bf7d4af7b2045f257a58d973da7d73501db
4
- data.tar.gz: f752ab64cb5d2208aed1cae8d5312aafee5314a54d27929e67f38978439d7b36
3
+ metadata.gz: 34a83a28ff685be48e3c59036aec00189356152540e96e3f0cdcd87dd927ea65
4
+ data.tar.gz: c3b9f90bd3bf142961216555382e62452e4bb0141bfba3d7ac5d09b2cbc9f93b
5
5
  SHA512:
6
- metadata.gz: 99a1bd26d8f9e2f4f0c4bfffdc3f01233cd5e3616594c15144a3c3f6ee3f268f6fd47c18c57a8073118d8630832d15ace0c89c8af355a31602eb7d78221ce6c9
7
- data.tar.gz: 9db2511d57761816e5a02051f0c9e3ab817074056c05ce33d079ad0dfb34173da239f97cf14342dd9c8986ce65770b621c2568d762ef8647d8dc1335b573ca80
6
+ metadata.gz: 54cc55c7d63d2dc949fe2112847a53184fb6155be571fcd1ec2940905536f2da2ffab94b7808bba2c3eb42775bce7a86a9b0057b1afdf98e60507f799e2e0db3
7
+ data.tar.gz: 48ac006b7ee34632ad52f177b2e89d1e260161311421c61fdd57e778791cf8d302a9276ead67ede50f2d022af7e2661af9ffc57a9c50fbabb07a4b334d83056f
data/CHANGELOG.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # Changelog
2
2
 
3
+ ## HCloud v2.2.0 (2024-08-22)
4
+
5
+ - Add support for (de-)compression of HTTP requests
6
+
3
7
  ## HCloud v2.1.0 (2024-07-28)
4
8
 
5
9
  - Add `included_traffic` attribute to `ServerType#prices` and `LoadBalancerType#prices`
data/Gemfile CHANGED
@@ -5,7 +5,12 @@ source "https://rubygems.org"
5
5
  # Specify your gem's dependencies in hcloud.gemspec
6
6
  gemspec
7
7
 
8
+ group :development do
9
+ gem "yard", require: false
10
+ end
11
+
8
12
  group :development, :test do
13
+ gem "brotli"
9
14
  gem "debug", require: false
10
15
  gem "dotenv", require: false
11
16
  gem "ffaker", require: false
data/README.md CHANGED
@@ -143,6 +143,22 @@ ssh_keys = HCloud::SSHKey.all # Will block until remaining requests have regener
143
143
  Since rate limits are per hour and per project, using multiple clients at the same time will interfere with the rate limiting mechanism.
144
144
  To prevent this, wrap client calls in a loop that retries the call after it fails with a `HCloud::RateLimitExceeded` error.
145
145
 
146
+ ### Compression
147
+
148
+ Enable compression by passing an appropriate `compression` option to `HCloud::Client.new`.
149
+ Current supported options are `nil`, `"gzip"`, and `"brotli"`.
150
+ Compression is disabled by default.
151
+
152
+ ```ruby
153
+ client = HCloud::Client.new(access_token: "my_access_token", compression: "gzip")
154
+ ```
155
+
156
+ To use Brotli compression, you need to install the `brotli` gem (at least version 0.3.0):
157
+
158
+ ```ruby
159
+ gem "brotli"
160
+ ```
161
+
146
162
  ## Testing
147
163
 
148
164
  ```ssh
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  HCloud.loader.inflector.inflect(
4
+ "block_io" => "BlockIO",
4
5
  "dns_pointer" => "DNSPointer",
5
6
  "floating_ip" => "FloatingIP",
6
7
  "floating_ip_price" => "FloatingIPPrice",
@@ -1,6 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # @!visibility private
3
4
  module CoreExt
5
+ # @!visibility private
4
6
  module SendWrap
5
7
  # Send a message to self, or all objects contained in self (for arrays)
6
8
 
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module HCloud
4
+ # @!visibility private
4
5
  class ActionCollection < Collection
5
6
  attr_reader :resource
6
7
 
data/lib/hcloud/client.rb CHANGED
@@ -3,6 +3,7 @@
3
3
  require "logger"
4
4
 
5
5
  module HCloud
6
+ # @!visibility private
6
7
  class NilConnection
7
8
  def raise_error(...)
8
9
  raise ArgumentError, "no default client configured, set HCloud::Client.connection to an instance of HCloud::Client"
@@ -14,18 +15,21 @@ module HCloud
14
15
  alias delete raise_error
15
16
  end
16
17
 
18
+ # @!visibility private
17
19
  class Client
18
20
  class_attribute :connection
19
21
 
20
22
  self.connection = NilConnection.new
21
23
 
22
- attr_reader :access_token, :endpoint, :logger, :rate_limit
24
+ attr_reader :access_token, :endpoint, :logger, :rate_limit, :timeout, :compression
23
25
 
24
- def initialize(access_token:, endpoint: "https://api.hetzner.cloud/v1", logger: Logger.new("/dev/null"), rate_limit: false)
26
+ def initialize(access_token:, endpoint: "https://api.hetzner.cloud/v1", logger: Logger.new("/dev/null"), rate_limit: false, timeout: 10, compression: nil)
25
27
  @access_token = access_token
26
28
  @endpoint = endpoint
27
29
  @logger = logger
28
30
  @rate_limit = rate_limit
31
+ @timeout = timeout
32
+ @compression = compression
29
33
  end
30
34
 
31
35
  delegate :get, :put, :post, :delete, to: :http
@@ -33,7 +37,7 @@ module HCloud
33
37
  private
34
38
 
35
39
  def http
36
- @http ||= HTTP.new(access_token, endpoint, logger, rate_limit)
40
+ @http ||= HTTP.new(access_token, endpoint, logger, rate_limit, timeout, compression)
37
41
  end
38
42
  end
39
43
  end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module HCloud
4
+ # @!visibility private
4
5
  class Collection
5
6
  include Enumerable
6
7
 
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module HCloud
4
+ # @!visibility private
4
5
  module Actionable
5
6
  extend ActiveSupport::Concern
6
7
 
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module HCloud
4
+ # @!visibility private
4
5
  module Concerns
5
6
  extend ActiveSupport::Concern
6
7
 
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module HCloud
4
+ # @!visibility private
4
5
  module Creatable
5
6
  extend ActiveSupport::Concern
6
7
 
@@ -25,7 +26,7 @@ module HCloud
25
26
  end
26
27
 
27
28
  # Convert creatable_attributes into a key-value list
28
- # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity
29
+ # rubocop:disable Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity
29
30
  def creatable_params
30
31
  # Split simple and nested attributes
31
32
  nested_attributes, simple_attributes = creatable_attributes.partition { |a| a.respond_to? :each }
@@ -36,7 +37,7 @@ module HCloud
36
37
  .merge(nested_attributes.reduce(&:merge).to_h { |k, v| [k.to_s, Array(v).filter_map { |w| send(k)&.send_wrap(w) }.first] })
37
38
  .compact_blank
38
39
  end
39
- # rubocop:enable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity
40
+ # rubocop:enable Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity
40
41
  end
41
42
 
42
43
  class_methods do
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module HCloud
4
+ # @!visibility private
4
5
  module Deletable
5
6
  extend ActiveSupport::Concern
6
7
 
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module HCloud
4
+ # @!visibility private
4
5
  module DynamicAttributes
5
6
  extend ActiveSupport::Concern
6
7
 
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module HCloud
4
+ # @!visibility private
4
5
  module Labelable
5
6
  extend ActiveSupport::Concern
6
7
 
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module HCloud
4
+ # @!visibility private
4
5
  module Meterable
5
6
  extend ActiveSupport::Concern
6
7
 
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module HCloud
4
+ # @!visibility private
4
5
  module Queryable
5
6
  extend ActiveSupport::Concern
6
7
 
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module HCloud
4
+ # @!visibility private
4
5
  module Singleton
5
6
  extend ActiveSupport::Concern
6
7
 
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module HCloud
4
+ # @!visibility private
4
5
  module Updatable
5
6
  extend ActiveSupport::Concern
6
7
 
data/lib/hcloud/entity.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module HCloud
4
+ # @!visibility private
4
5
  class Entity
5
6
  include ActiveModel::Attributes
6
7
  include ActiveModel::AttributeAssignment
data/lib/hcloud/http.rb CHANGED
@@ -3,15 +3,25 @@
3
3
  require "http"
4
4
 
5
5
  module HCloud
6
+ # @!visibility private
6
7
  class HTTP
7
- attr_reader :access_token, :endpoint, :logger, :rate_limit, :timeout
8
+ # Supported compression algorithms
9
+ COMPRESSION_ALGORITHMS = [
10
+ "gzip",
11
+ "brotli",
12
+ ].freeze
13
+
14
+ attr_reader :access_token, :endpoint, :logger, :rate_limit, :timeout, :compression
15
+
16
+ def initialize(access_token, endpoint, logger, rate_limit = false, timeout = 10, compression = nil)
17
+ raise ArgumentError, "invalid compression algorithm: #{compression}" if compression && !COMPRESSION_ALGORITHMS.include?(compression)
8
18
 
9
- def initialize(access_token, endpoint, logger, rate_limit = false, timeout = 10)
10
19
  @access_token = access_token
11
20
  @endpoint = endpoint
12
21
  @logger = logger
13
22
  @rate_limit = rate_limit
14
23
  @timeout = timeout
24
+ @compression = compression
15
25
  end
16
26
 
17
27
  def get(path, params = {})
@@ -78,10 +88,12 @@ module HCloud
78
88
 
79
89
  def http
80
90
  @http ||= ::HTTP
81
- .headers(accept: "application/json", user_agent: "#{HCloud::NAME}/#{HCloud::VERSION}")
91
+ .headers(user_agent: "#{HCloud::NAME}/#{HCloud::VERSION}")
92
+ .accept("application/json")
82
93
  .timeout(timeout)
83
- .use(logging: { logger: logger })
84
94
  .then { |h| rate_limit ? h.use(:rate_limiter) : h }
95
+ .then { |h| compression ? h.use(compression: { method: compression }) : h }
96
+ .use(logging: { logger: logger })
85
97
  .encoding("utf-8")
86
98
  .auth("Bearer #{access_token}")
87
99
  end
@@ -1,7 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # rubocop:disable Metrics/CyclomaticComplexity,Metrics/AbcSize
3
+ # rubocop:disable Metrics/CyclomaticComplexity
4
4
  module HCloud
5
+ # @!visibility private
5
6
  class ResourceType
6
7
  class_attribute :resource_class_name
7
8
 
@@ -54,6 +55,7 @@ module HCloud
54
55
  end
55
56
  # rubocop:enable Naming/MethodName
56
57
 
58
+ # @!visibility private
57
59
  class GenericType < ResourceType
58
60
  def cast(value)
59
61
  case value
@@ -78,7 +80,7 @@ module HCloud
78
80
  end
79
81
  end
80
82
  end
81
- # rubocop:enable Metrics/CyclomaticComplexity,Metrics/AbcSize
83
+ # rubocop:enable Metrics/CyclomaticComplexity
82
84
 
83
85
  ActiveModel::Type.register(:action, HCloud::ResourceType.Type("HCloud::Action"))
84
86
  ActiveModel::Type.register(:algorithm, HCloud::ResourceType.Type("HCloud::Algorithm"))
@@ -1,9 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module HCloud
4
+ # @!visibility private
4
5
  module Version
5
6
  MAJOR = 2
6
- MINOR = 1
7
+ MINOR = 2
7
8
  PATCH = 0
8
9
  PRE = nil
9
10
 
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ # @!visibility private
4
+ module HTTP
5
+ # @!visibility private
6
+ module Features
7
+ # @!visibility private
8
+ class BlockIO
9
+ def initialize(block)
10
+ @block = block
11
+ end
12
+
13
+ def write(data)
14
+ @block.call(data)
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ # @!visibility private
4
+ module HTTP
5
+ # @!visibility private
6
+ module Features
7
+ # @!visibility private
8
+ class Compression < Feature
9
+ SUPPORTED_ENCODING = {
10
+ "gzip" => "gzip",
11
+ "brotli" => "br",
12
+ }.freeze
13
+
14
+ HTTP::Options.register_feature(:compression, self)
15
+
16
+ attr_reader :method
17
+
18
+ def initialize(**)
19
+ super
20
+
21
+ @method = @opts.fetch(:method, "gzip").to_s || "gzip"
22
+
23
+ raise Error, "Only gzip and brotli methods are supported" unless SUPPORTED_ENCODING.key?(method)
24
+ end
25
+
26
+ def wrap_request(request)
27
+ return request unless method
28
+
29
+ # Set Accept-Encoding header
30
+ request.headers[Headers::ACCEPT_ENCODING] = SUPPORTED_ENCODING[method]
31
+
32
+ return request if request.body.size.zero? # rubocop:disable Style/ZeroLengthPredicate
33
+
34
+ # Delete Content-Length header, it is set automatically by HTTP::Request::Writer
35
+ request.headers.delete(Headers::CONTENT_LENGTH)
36
+
37
+ # Set Content-Encoding header
38
+ request.headers[Headers::CONTENT_ENCODING] = SUPPORTED_ENCODING[method]
39
+
40
+ HTTP::Request.new(
41
+ version: request.version,
42
+ verb: request.verb,
43
+ uri: request.uri,
44
+ headers: request.headers,
45
+ proxy: request.proxy,
46
+ body: compress(request.body),
47
+ uri_normalizer: request.uri_normalizer,
48
+ )
49
+ end
50
+
51
+ def wrap_response(response)
52
+ return response unless SUPPORTED_ENCODING.value?(response.headers.get(Headers::CONTENT_ENCODING).first)
53
+
54
+ HTTP::Response.new(
55
+ status: response.status,
56
+ version: response.version,
57
+ headers: response.headers,
58
+ proxy_headers: response.proxy_headers,
59
+ connection: response.connection,
60
+ body: decompress(response.connection),
61
+ request: response.request,
62
+ )
63
+ end
64
+
65
+ private
66
+
67
+ def compress(body)
68
+ case method
69
+ when "gzip"
70
+ Request::GzippedBody.new(body)
71
+ when "brotli"
72
+ Request::BrotliBody.new(body)
73
+ end
74
+ end
75
+
76
+ def decompress(connection)
77
+ case method
78
+ when "gzip"
79
+ HTTP::Response::Body.new(Response::GzipInflater.new(connection))
80
+ when "brotli"
81
+ HTTP::Response::Body.new(Response::BrotliInflater.new(connection))
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ begin
4
+ require "brotli"
5
+
6
+ version = Brotli::VERSION.split(".").map(&:to_i)
7
+ raise ArgumentError, "incompatible version of brotli: #{Brotli::VERSION}, needs to be at least 0.3.0" unless (version[0]).positive? || version[1] >= 3
8
+ rescue LoadError
9
+ # Ignore
10
+ end
11
+
12
+ # @!visibility private
13
+ module HTTP
14
+ # @!visibility private
15
+ module Features
16
+ # @!visibility private
17
+ module Request
18
+ # @!visibility private
19
+ class BrotliBody < CompressedBody
20
+ def compress(&block)
21
+ brotli = Brotli::Writer.new(BlockIO.new(block))
22
+ @body.each { |chunk| brotli.write(chunk) }
23
+ ensure
24
+ brotli.finish
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ # @!visibility private
4
+ module HTTP
5
+ # @!visibility private
6
+ module Features
7
+ # @!visibility private
8
+ module Request
9
+ # @!visibility private
10
+ class CompressedBody < HTTP::Request::Body
11
+ def initialize(uncompressed_body) # rubocop:disable Lint/MissingSuper
12
+ @body = uncompressed_body
13
+ @compressed = nil
14
+ end
15
+
16
+ def size
17
+ compress_all! unless @compressed
18
+ @compressed.size
19
+ end
20
+
21
+ def each(&block)
22
+ return to_enum __method__ unless block
23
+
24
+ if @compressed
25
+ compressed_each(&block)
26
+ else
27
+ compress(&block)
28
+ end
29
+
30
+ self
31
+ end
32
+
33
+ def compress(&_block)
34
+ raise NotImplementedError
35
+ end
36
+
37
+ private
38
+
39
+ def compressed_each
40
+ while (data = @compressed.read(Connection::BUFFER_SIZE))
41
+ yield data
42
+ end
43
+ ensure
44
+ @compressed.close!
45
+ end
46
+
47
+ def compress_all!
48
+ @compressed = Tempfile.new("http-compressed_body", binmode: true)
49
+ compress { |data| @compressed.write(data) }
50
+ @compressed.rewind
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "zlib"
4
+
5
+ # @!visibility private
6
+ module HTTP
7
+ # @!visibility private
8
+ module Features
9
+ # @!visibility private
10
+ module Request
11
+ # @!visibility private
12
+ class GzippedBody < CompressedBody
13
+ def compress(&block)
14
+ gzip = Zlib::GzipWriter.new(BlockIO.new(block))
15
+ @body.each { |chunk| gzip.write(chunk) }
16
+ ensure
17
+ gzip.finish
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ begin
4
+ require "brotli"
5
+
6
+ version = Brotli::VERSION.split(".").map(&:to_i)
7
+ raise ArgumentError, "incompatible version of brotli: #{Brotli::VERSION}, needs to be at least 0.3.0" unless (version[0]).positive? || version[1] >= 3
8
+ rescue LoadError
9
+ # Ignore
10
+ end
11
+
12
+ # @!visibility private
13
+ module HTTP
14
+ # @!visibility private
15
+ module Features
16
+ # @!visibility private
17
+ module Response
18
+ # @!visibility private
19
+ class BrotliInflater < HTTP::Response::Inflater
20
+ def readpartial(*args)
21
+ chunks = []
22
+
23
+ while (chunk = @connection.readpartial(*args))
24
+ chunks << chunk
25
+ end
26
+
27
+ return if chunks.empty?
28
+
29
+ Brotli.inflate(chunks.join)
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "zlib"
4
+
5
+ # @!visibility private
6
+ module HTTP
7
+ # @!visibility private
8
+ module Features
9
+ # @!visibility private
10
+ module Response
11
+ # @!visibility private
12
+ class GzipInflater < HTTP::Response::Inflater
13
+ end
14
+ end
15
+ end
16
+ end
@@ -2,8 +2,11 @@
2
2
 
3
3
  require "yaml"
4
4
 
5
+ # @!visibility private
5
6
  module HTTP
7
+ # @!visibility private
6
8
  module MimeType
9
+ # @!visibility private
7
10
  class YAML < Adapter
8
11
  def encode(obj)
9
12
  return obj.to_yaml if obj.respond_to?(:to_yaml)
@@ -1,6 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # @!visibility private
3
4
  module HTTP
5
+ # @!visibility private
4
6
  class RateLimiter < Feature
5
7
  attr_reader :limit, :remaining, :reset, :at, :rate
6
8
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hetznercloud
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.1.0
4
+ version: 2.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Florian Dejonckheere
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-07-28 00:00:00.000000000 Z
11
+ date: 2024-08-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activemodel
@@ -163,6 +163,13 @@ files:
163
163
  - lib/hcloud/resources/ssh_key.rb
164
164
  - lib/hcloud/resources/volume.rb
165
165
  - lib/hcloud/version.rb
166
+ - lib/http/features/block_io.rb
167
+ - lib/http/features/compression.rb
168
+ - lib/http/features/request/brotli_body.rb
169
+ - lib/http/features/request/compressed_body.rb
170
+ - lib/http/features/request/gzipped_body.rb
171
+ - lib/http/features/response/brotli_inflater.rb
172
+ - lib/http/features/response/gzip_inflater.rb
166
173
  - lib/http/mime_type/yaml.rb
167
174
  - lib/http/rate_limiter.rb
168
175
  homepage: https://github.com/floriandejonckheere/hcloud