hcloud 0.1.2 → 1.0.3

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.
Files changed (45) hide show
  1. checksums.yaml +5 -5
  2. data/.github/workflows/ruby.yml +32 -0
  3. data/.gitignore +0 -1
  4. data/.rubocop.yml +28 -10
  5. data/.rubocop_todo.yml +18 -121
  6. data/CHANGELOG.md +60 -2
  7. data/Gemfile +3 -0
  8. data/Gemfile.lock +149 -0
  9. data/README.md +34 -1
  10. data/Rakefile +2 -0
  11. data/bin/console +1 -0
  12. data/hcloud.gemspec +14 -9
  13. data/lib/hcloud/abstract_resource.rb +165 -60
  14. data/lib/hcloud/action.rb +8 -10
  15. data/lib/hcloud/action_resource.rb +3 -29
  16. data/lib/hcloud/client.rb +70 -29
  17. data/lib/hcloud/datacenter.rb +7 -7
  18. data/lib/hcloud/datacenter_resource.rb +6 -31
  19. data/lib/hcloud/entry_loader.rb +186 -20
  20. data/lib/hcloud/errors.rb +2 -0
  21. data/lib/hcloud/floating_ip.rb +18 -29
  22. data/lib/hcloud/floating_ip_resource.rb +15 -30
  23. data/lib/hcloud/image.rb +12 -32
  24. data/lib/hcloud/image_resource.rb +7 -38
  25. data/lib/hcloud/iso.rb +4 -1
  26. data/lib/hcloud/iso_resource.rb +7 -28
  27. data/lib/hcloud/location.rb +3 -9
  28. data/lib/hcloud/location_resource.rb +6 -27
  29. data/lib/hcloud/network.rb +33 -0
  30. data/lib/hcloud/network_resource.rb +25 -0
  31. data/lib/hcloud/pagination.rb +2 -9
  32. data/lib/hcloud/server.rb +37 -70
  33. data/lib/hcloud/server_resource.rb +17 -38
  34. data/lib/hcloud/server_type.rb +3 -10
  35. data/lib/hcloud/server_type_resource.rb +6 -28
  36. data/lib/hcloud/ssh_key.rb +6 -17
  37. data/lib/hcloud/ssh_key_resource.rb +13 -32
  38. data/lib/hcloud/typhoeus_ext.rb +110 -0
  39. data/lib/hcloud/version.rb +3 -1
  40. data/lib/hcloud/volume.rb +32 -0
  41. data/lib/hcloud/volume_resource.rb +29 -0
  42. data/lib/hcloud.rb +31 -5
  43. metadata +56 -22
  44. data/.travis.yml +0 -9
  45. data/lib/hcloud/multi_reply.rb +0 -21
@@ -1,75 +1,183 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support/core_ext/string'
4
+
1
5
  module Hcloud
2
6
  class AbstractResource
3
7
  include Enumerable
4
8
 
5
- attr_reader :client, :parent, :base_path
9
+ delegate :request, :prepare_request, to: :client
10
+
11
+ class << self
12
+ def bind_to(klass)
13
+ resource = self
14
+ %w[find find_by where all [] page limit per_page order
15
+ to_a count pagnation each].each do |method|
16
+ klass.define_singleton_method(method) do |*args, &block|
17
+ resource.new(client: Client.connection).public_send(method, *args, &block)
18
+ end
19
+ end
20
+ end
21
+
22
+ def filter_attributes(*keys)
23
+ return @filter_attributes if keys.to_a.empty?
24
+
25
+ @filter_attributes = keys
26
+ end
27
+
28
+ def resource_class
29
+ ancestors[ancestors.index(Hcloud::AbstractResource) - 1]
30
+ end
31
+
32
+ def resource_url(url = nil)
33
+ return (@resource_url = url) if url
34
+
35
+ @resource_url || resource_class.name.demodulize.gsub('Resource', '').tableize
36
+ end
37
+
38
+ def resource_path(path = nil)
39
+ return (@resource_path = path) if path
40
+
41
+ @resource_path || resource_url
42
+ end
43
+
44
+ def resource(res = nil)
45
+ return (@resource = res) if res
46
+ return @resource if @resource
47
+
48
+ auto_const = resource_class.name.demodulize.gsub('Resource', '').to_sym
49
+ return Hcloud.const_get(auto_const) if Hcloud.constants.include?(auto_const)
50
+
51
+ raise Error, "unable to lookup resource class for #{name}"
52
+ end
53
+ end
54
+
55
+ attr_reader :client
6
56
 
7
- def initialize(client:, parent: nil, base_path: '')
57
+ def initialize(client:, base_path: '')
8
58
  @client = client
9
- @parent = parent
10
59
  @page = 1
11
60
  @per_page = 25
12
61
  @order = []
13
62
  @base_path = base_path
14
63
  end
15
64
 
65
+ def all
66
+ where
67
+ end
68
+
69
+ def where(kwargs = {})
70
+ kwargs.each_key do |key|
71
+ keys = self.class.filter_attributes.map(&:to_s)
72
+ next if keys.include?(key.to_s)
73
+
74
+ raise ArgumentError, "unknown filter #{key}, allowed keys are #{keys}"
75
+ end
76
+
77
+ _dup :@query, @query.to_h.merge(kwargs)
78
+ end
79
+
80
+ def find(id)
81
+ prepare_request(
82
+ [self.class.resource_url, id].join('/'),
83
+ resource_path: resource_path.to_s.singularize,
84
+ resource_class: self.class.resource
85
+ )
86
+ end
87
+
88
+ def [](arg)
89
+ find_by(id: arg)
90
+ end
91
+
92
+ def find_by(**kwargs)
93
+ if id = kwargs.delete(:id)
94
+ return find(id)
95
+ end
96
+
97
+ per_page(1).where(**kwargs).first
98
+ rescue Error::NotFound
99
+ end
100
+
101
+ # def count
102
+ # per_page(1).first&.response&.pagination&.total_entries.to_i
103
+ # end
104
+
16
105
  def page(page)
17
- @page = page
18
- self
106
+ _dup :@page, page
19
107
  end
20
108
 
21
109
  def per_page(per_page)
22
- @per_page = per_page
23
- self
110
+ _dup :@per_page, per_page
24
111
  end
25
112
 
26
113
  def limit(limit)
27
- @limit = limit
28
- self
114
+ _dup :@limit, limit
29
115
  end
30
116
 
31
117
  def order(*sort)
32
- @order = sort.flat_map do |s|
33
- case s
34
- when Symbol, String then s.to_s
35
- when Hash then s.map { |k, v| "#{k}:#{v}" }
36
- else
37
- raise ArgumentError,
38
- "Unable to resolve type for given #{s.inspect} from #{sort}"
39
- end
40
- end
41
- self
118
+ _dup :@order,
119
+ begin
120
+ sort.flat_map do |s|
121
+ case s
122
+ when Symbol, String then s.to_s
123
+ when Hash then s.map { |k, v| "#{k}:#{v}" }
124
+ else
125
+ raise ArgumentError,
126
+ "Unable to resolve type for given #{s.inspect} from #{sort}"
127
+ end
128
+ end
129
+ end
42
130
  end
43
131
 
44
- def mj(path, **o, &block)
45
- if !client.nil? && client.auto_pagination
46
- requests = __entries__(path, **o)
47
- if requests.all? { |x| x.is_a? Hash }
48
- m = MultiReply.new(j: requests, pagination: :auto)
49
- m.cb = block
50
- return m
51
- end
52
- client.hydra.run
53
- j = requests.map do |x|
54
- Oj.load(x.response.body)
55
- end
56
- m = MultiReply.new(j: j, pagination: :auto)
57
- m.cb = block
58
- return m
59
- end
60
- m = MultiReply.new(j: [Oj.load(request(path, o.merge(ep: ep)).run.body)])
61
- m.cb = block
62
- m
132
+ def run
133
+ @run ||= multi_query(
134
+ resource_url,
135
+ q: @query,
136
+ resource_path: resource_path,
137
+ resource_class: self.class.resource
138
+ )
63
139
  end
64
140
 
65
- def each
66
- all.each do |member|
67
- yield(member)
68
- end
141
+ def each(&block)
142
+ run.each(&block)
143
+ end
144
+
145
+ # this is just to keep the actual bevahior
146
+ def pagination
147
+ return :auto if client.auto_pagination
148
+
149
+ run.response.pagination
69
150
  end
70
151
 
71
152
  protected
72
153
 
154
+ def _dup(var, value)
155
+ dup.tap do |res|
156
+ res.instance_variable_set(var, value)
157
+ res.instance_variable_set(:@run, nil)
158
+ end
159
+ end
160
+
161
+ def resource_path
162
+ self.class.resource_path || self.class.resource_url
163
+ end
164
+
165
+ def resource_url
166
+ [@base_path.to_s, self.class.resource_url.to_s].reject(&:empty?).join('/')
167
+ end
168
+
169
+ def multi_query(path, **o)
170
+ return prepare_request(path, o.merge(ep: ep)) unless client&.auto_pagination
171
+
172
+ raise Error, 'unable to run auto paginate within concurrent excecution' if @concurrent
173
+
174
+ requests = __entries__(path, **o)
175
+ return requests.flat_map(&:resource) if requests.all? { |req| req.respond_to? :resource }
176
+
177
+ client.hydra.run
178
+ requests.flat_map { |req| req.response.resource }
179
+ end
180
+
73
181
  def page_params(per_page: nil, page: nil)
74
182
  { per_page: per_page || @per_page, page: page || @page }.to_param
75
183
  end
@@ -85,28 +193,25 @@ module Hcloud
85
193
  r.compact.join('&')
86
194
  end
87
195
 
88
- def request(*args)
89
- client.request(*args)
90
- end
91
-
196
+ # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
92
197
  def __entries__(path, **o)
93
- ret = Oj.load(request(path, o.merge(ep: ep(per_page: 1, page: 1))).run.body)
94
- a = ret.dig('meta', 'pagination', 'total_entries').to_i
95
- return [ret] if a <= 1
96
- unless @limit.nil?
97
- a = @limit if a > @limit
98
- end
99
- r = a / Client::MAX_ENTRIES_PER_PAGE
100
- r += 1 if a % Client::MAX_ENTRIES_PER_PAGE > 0
101
- requests = r.times.map do |i|
198
+ first_page = request(path, o.merge(ep: ep(per_page: 1, page: 1))).run
199
+ total_entries = first_page.pagination.total_entries
200
+ return [first_page] if total_entries <= 1 || @limit == 1
201
+
202
+ total_entries = @limit if !@limit.nil? && (total_entries > @limit)
203
+ pages = total_entries / Client::MAX_ENTRIES_PER_PAGE
204
+ pages += 1 if (total_entries % Client::MAX_ENTRIES_PER_PAGE).positive?
205
+ pages.times.map do |page|
102
206
  per_page = Client::MAX_ENTRIES_PER_PAGE
103
- if !@limit.nil? && (r == (i + 1)) && (a % per_page != 0)
104
- per_page = a % per_page
207
+ if !@limit.nil? && (pages == (page + 1)) && (total_entries % per_page != 0)
208
+ per_page = total_entries % per_page
209
+ end
210
+ request(path, o.merge(ep: ep(per_page: per_page, page: page + 1))).tap do |req|
211
+ client.hydra.queue req
105
212
  end
106
- req = request(path, o.merge(ep: ep(per_page: per_page, page: i + 1)))
107
- client.hydra.queue req
108
- req
109
213
  end
110
214
  end
215
+ # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
111
216
  end
112
217
  end
data/lib/hcloud/action.rb CHANGED
@@ -1,16 +1,14 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Hcloud
2
4
  class Action
3
- Attributes = {
4
- id: nil,
5
- command: nil,
6
- status: nil,
7
- progress: nil,
8
- started: :time,
9
- finished: :time,
10
- resources: nil,
11
- error: nil
12
- }.freeze
5
+ require 'hcloud/action_resource'
13
6
 
14
7
  include EntryLoader
8
+
9
+ schema(
10
+ started: :time,
11
+ finished: :time
12
+ )
15
13
  end
16
14
  end
@@ -1,35 +1,9 @@
1
+ # frozen_string_literal: true
1
2
 
2
3
  module Hcloud
3
4
  class ActionResource < AbstractResource
4
- def all
5
- mj(base_path('actions')) do |j|
6
- j.flat_map { |x| x['actions'].map { |x| Action.new(x, self, client) } }
7
- end
8
- end
5
+ filter_attributes :status
9
6
 
10
- def find(id)
11
- Action.new(
12
- Oj.load(request(base_path("actions/#{id.to_i}")).run.body)['action'],
13
- self,
14
- client
15
- )
16
- end
17
-
18
- def [](arg)
19
- find(arg)
20
- rescue Error::NotFound
21
- end
22
-
23
- def where(status: nil)
24
- mj(base_path('actions'), q: { status: status }) do |j|
25
- j.flat_map { |x| x['actions'].map { |x| Action.new(x, self, client) } }
26
- end
27
- end
28
-
29
- private
30
-
31
- def base_path(ext)
32
- [@base_path, ext].reject(&:empty?).join('/')
33
- end
7
+ bind_to Action
34
8
  end
35
9
  end
data/lib/hcloud/client.rb CHANGED
@@ -1,18 +1,52 @@
1
+ # frozen_string_literal: true
2
+
1
3
  autoload :Typhoeus, 'typhoeus'
2
4
  autoload :Oj, 'oj'
3
5
 
6
+ require 'delegate'
7
+
4
8
  module Hcloud
5
9
  class Client
6
10
  MAX_ENTRIES_PER_PAGE = 50
7
11
 
8
- attr_reader :token, :auto_pagination, :hydra
9
- def initialize(token:, auto_pagination: false, concurrency: 20)
12
+ class << self
13
+ attr_writer :connection
14
+
15
+ def connection
16
+ return @connection if @connection.is_a? Hcloud::Client
17
+
18
+ raise ArgumentError, "client not correctly initialized, actually #{@client.inspect}"
19
+ end
20
+ end
21
+
22
+ attr_reader :token, :auto_pagination, :hydra, :user_agent
23
+
24
+ def initialize(token:, auto_pagination: false, concurrency: 20, user_agent: nil)
10
25
  @token = token
26
+ @user_agent = user_agent || "hcloud-ruby v#{VERSION}"
11
27
  @auto_pagination = auto_pagination
12
28
  @concurrency = concurrency
13
29
  @hydra = Typhoeus::Hydra.new(max_concurrency: concurrency)
14
30
  end
15
31
 
32
+ def concurrent
33
+ @concurrent = true
34
+ ret = yield
35
+ ret.each do |element|
36
+ next unless element.is_a?(AbstractResource)
37
+
38
+ element.run
39
+ end
40
+ hydra.run
41
+ ret
42
+ ensure
43
+ @concurrent = nil
44
+ end
45
+
46
+ def concurrent?
47
+ !@concurrent.nil?
48
+ end
49
+
16
50
  def authorized?
17
51
  request('server_types').run
18
52
  true
@@ -56,8 +90,34 @@ module Hcloud
56
90
  FloatingIPResource.new(client: self)
57
91
  end
58
92
 
59
- def request(path, **options)
60
- code = options.delete(:code)
93
+ def networks
94
+ NetworkResource.new(client: self)
95
+ end
96
+
97
+ def volumes
98
+ VolumeResource.new(client: self)
99
+ end
100
+
101
+ class ResourceFuture < Delegator
102
+ def initialize(request) # rubocop:disable Lint/MissingSuper
103
+ @request = request
104
+ end
105
+
106
+ def __getobj__
107
+ @__getobj__ ||= @request&.response&.resource
108
+ end
109
+ end
110
+
111
+ def prepare_request(url, args = {}, &block)
112
+ req = request(url, **args.merge(block: block))
113
+ return req.run.resource unless concurrent?
114
+
115
+ hydra.queue req
116
+ ResourceFuture.new(req)
117
+ end
118
+
119
+ def request(path, options = {}) # rubocop:disable Metrics/MethodLength
120
+ hcloud_attributes = TyphoeusExt.collect_attributes(options)
61
121
  if x = options.delete(:j)
62
122
  options[:body] = Oj.dump(x, mode: :compat)
63
123
  options[:method] ||= :post
@@ -68,41 +128,22 @@ module Hcloud
68
128
  q << x.to_param
69
129
  end
70
130
  path = path.dup
71
- path << '?' + q.join('&')
131
+ path << "?#{q.join('&')}"
72
132
  r = Typhoeus::Request.new(
73
133
  "https://api.hetzner.cloud/v1/#{path}",
74
134
  {
75
135
  headers: {
76
136
  'Authorization' => "Bearer #{token}",
137
+ 'User-Agent' => user_agent,
77
138
  'Content-Type' => 'application/json'
78
139
  }
79
140
  }.merge(options)
80
141
  )
81
142
  r.on_complete do |response|
82
- case response.code
83
- when code
84
- raise Error::UnexpectedError, response.body
85
- when 401
86
- raise Error::Unauthorized
87
- when 0
88
- raise Error::ServerError, "Connection error: #{response.return_code}"
89
- when 400...600
90
- j = Oj.load(response.body)
91
- code = j.dig('error', 'code')
92
- error_class = case code
93
- when 'invalid_input' then Error::InvalidInput
94
- when 'forbidden' then Error::Forbidden
95
- when 'locked' then Error::Locked
96
- when 'not_found' then Error::NotFound
97
- when 'rate_limit_exceeded' then Error::RateLimitExceeded
98
- when 'resource_unavailable' then Error::ResourceUnavilable
99
- when 'service_error' then Error::ServiceError
100
- when 'uniqueness_error' then Error::UniquenessError
101
- else
102
- Error::ServerError
103
- end
104
- raise error_class, j.dig('error', 'message')
105
- end
143
+ response.extend(TyphoeusExt)
144
+ response.attributes = hcloud_attributes
145
+ response.context.client = self
146
+ response.check_for_error unless response.request.hydra
106
147
  end
107
148
  r
108
149
  end
@@ -1,13 +1,13 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Hcloud
2
4
  class Datacenter
3
- Attributes = {
4
- id: nil,
5
- name: nil,
6
- description: nil,
7
- location: Location,
8
- server_types: nil
9
- }.freeze
5
+ require 'hcloud/datacenter_resource'
10
6
 
11
7
  include EntryLoader
8
+
9
+ schema(
10
+ location: Location
11
+ )
12
12
  end
13
13
  end
@@ -1,44 +1,19 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Hcloud
2
4
  class DatacenterResource < AbstractResource
3
- include Enumerable
5
+ filter_attributes :name
4
6
 
5
- def all
6
- j = Oj.load(request('datacenters').run.body)
7
- dc = j['datacenters'].map { |x| Datacenter.new(x, self, client) }
8
- dc.reject { |x| x.id == j['recommendation'] }.unshift(
9
- dc.find { |x| x.id == j['recommendation'] }
10
- )
11
- end
7
+ bind_to Datacenter
12
8
 
13
9
  def recommended
14
10
  all.first
15
11
  end
16
12
 
17
- def find(id)
18
- Datacenter.new(
19
- Oj.load(request("datacenters/#{id}").run.body)['datacenter'],
20
- self,
21
- client
22
- )
23
- end
24
-
25
- def find_by(name:)
26
- x = Oj.load(request('datacenters', q: { name: name }).run.body)['datacenters']
27
- return nil if x.none?
28
- x.each do |s|
29
- return Datacenter.new(s, self, client)
30
- end
31
- end
32
-
33
13
  def [](arg)
34
14
  case arg
35
- when Integer
36
- begin
37
- find(arg)
38
- rescue Error::NotFound
39
- end
40
- when String
41
- find_by(name: arg)
15
+ when Integer then find_by(id: arg)
16
+ when String then find_by(name: arg)
42
17
  end
43
18
  end
44
19
  end