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 +4 -4
- data/CHANGELOG.md +4 -0
- data/Gemfile +5 -0
- data/README.md +16 -0
- data/config/inflections.rb +1 -0
- data/lib/core_ext/send_wrap.rb +2 -0
- data/lib/hcloud/action_collection.rb +1 -0
- data/lib/hcloud/client.rb +7 -3
- data/lib/hcloud/collection.rb +1 -0
- data/lib/hcloud/concerns/actionable.rb +1 -0
- data/lib/hcloud/concerns/concerns.rb +1 -0
- data/lib/hcloud/concerns/creatable.rb +3 -2
- data/lib/hcloud/concerns/deletable.rb +1 -0
- data/lib/hcloud/concerns/dynamic_attributes.rb +1 -0
- data/lib/hcloud/concerns/labelable.rb +1 -0
- data/lib/hcloud/concerns/meterable.rb +1 -0
- data/lib/hcloud/concerns/queryable.rb +1 -0
- data/lib/hcloud/concerns/singleton.rb +1 -0
- data/lib/hcloud/concerns/updatable.rb +1 -0
- data/lib/hcloud/entity.rb +1 -0
- data/lib/hcloud/http.rb +16 -4
- data/lib/hcloud/resource_type.rb +4 -2
- data/lib/hcloud/version.rb +2 -1
- data/lib/http/features/block_io.rb +18 -0
- data/lib/http/features/compression.rb +86 -0
- data/lib/http/features/request/brotli_body.rb +29 -0
- data/lib/http/features/request/compressed_body.rb +55 -0
- data/lib/http/features/request/gzipped_body.rb +22 -0
- data/lib/http/features/response/brotli_inflater.rb +34 -0
- data/lib/http/features/response/gzip_inflater.rb +16 -0
- data/lib/http/mime_type/yaml.rb +3 -0
- data/lib/http/rate_limiter.rb +2 -0
- metadata +9 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 34a83a28ff685be48e3c59036aec00189356152540e96e3f0cdcd87dd927ea65
|
4
|
+
data.tar.gz: c3b9f90bd3bf142961216555382e62452e4bb0141bfba3d7ac5d09b2cbc9f93b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 54cc55c7d63d2dc949fe2112847a53184fb6155be571fcd1ec2940905536f2da2ffab94b7808bba2c3eb42775bce7a86a9b0057b1afdf98e60507f799e2e0db3
|
7
|
+
data.tar.gz: 48ac006b7ee34632ad52f177b2e89d1e260161311421c61fdd57e778791cf8d302a9276ead67ede50f2d022af7e2661af9ffc57a9c50fbabb07a4b334d83056f
|
data/CHANGELOG.md
CHANGED
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
|
data/config/inflections.rb
CHANGED
data/lib/core_ext/send_wrap.rb
CHANGED
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
|
data/lib/hcloud/collection.rb
CHANGED
@@ -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/
|
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/
|
40
|
+
# rubocop:enable Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity
|
40
41
|
end
|
41
42
|
|
42
43
|
class_methods do
|
data/lib/hcloud/entity.rb
CHANGED
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
|
-
|
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(
|
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
|
data/lib/hcloud/resource_type.rb
CHANGED
@@ -1,7 +1,8 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
# rubocop:disable Metrics/CyclomaticComplexity
|
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
|
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"))
|
data/lib/hcloud/version.rb
CHANGED
@@ -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
|
data/lib/http/mime_type/yaml.rb
CHANGED
data/lib/http/rate_limiter.rb
CHANGED
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.
|
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-
|
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
|