hcloud 0.1.2 → 1.0.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.
Files changed (44) hide show
  1. checksums.yaml +5 -5
  2. data/.gitignore +0 -1
  3. data/.rubocop.yml +24 -9
  4. data/.rubocop_todo.yml +18 -121
  5. data/.travis.yml +3 -3
  6. data/CHANGELOG.md +13 -0
  7. data/Gemfile +3 -0
  8. data/Gemfile.lock +143 -0
  9. data/README.md +34 -1
  10. data/Rakefile +2 -0
  11. data/bin/console +1 -0
  12. data/hcloud.gemspec +5 -3
  13. data/lib/hcloud.rb +31 -5
  14. data/lib/hcloud/abstract_resource.rb +162 -55
  15. data/lib/hcloud/action.rb +8 -10
  16. data/lib/hcloud/action_resource.rb +3 -29
  17. data/lib/hcloud/client.rb +66 -28
  18. data/lib/hcloud/datacenter.rb +7 -7
  19. data/lib/hcloud/datacenter_resource.rb +6 -31
  20. data/lib/hcloud/entry_loader.rb +181 -20
  21. data/lib/hcloud/errors.rb +2 -0
  22. data/lib/hcloud/floating_ip.rb +18 -29
  23. data/lib/hcloud/floating_ip_resource.rb +15 -30
  24. data/lib/hcloud/image.rb +12 -32
  25. data/lib/hcloud/image_resource.rb +7 -38
  26. data/lib/hcloud/iso.rb +4 -1
  27. data/lib/hcloud/iso_resource.rb +7 -28
  28. data/lib/hcloud/location.rb +3 -9
  29. data/lib/hcloud/location_resource.rb +6 -27
  30. data/lib/hcloud/network.rb +33 -0
  31. data/lib/hcloud/network_resource.rb +25 -0
  32. data/lib/hcloud/pagination.rb +2 -9
  33. data/lib/hcloud/server.rb +37 -70
  34. data/lib/hcloud/server_resource.rb +16 -36
  35. data/lib/hcloud/server_type.rb +3 -10
  36. data/lib/hcloud/server_type_resource.rb +6 -28
  37. data/lib/hcloud/ssh_key.rb +6 -17
  38. data/lib/hcloud/ssh_key_resource.rb +13 -32
  39. data/lib/hcloud/typhoeus_ext.rb +112 -0
  40. data/lib/hcloud/version.rb +3 -1
  41. data/lib/hcloud/volume.rb +32 -0
  42. data/lib/hcloud/volume_resource.rb +29 -0
  43. metadata +31 -13
  44. data/lib/hcloud/multi_reply.rb +0 -21
data/Rakefile CHANGED
@@ -1,2 +1,4 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'bundler/gem_tasks'
2
4
  task default: :spec
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
2
3
 
3
4
  require 'bundler/setup'
4
5
  require 'hcloud'
@@ -1,5 +1,6 @@
1
+ # frozen_string_literal: true
1
2
 
2
- lib = File.expand_path('../lib', __FILE__)
3
+ lib = File.expand_path('lib', __dir__)
3
4
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
5
  require 'hcloud/version'
5
6
 
@@ -19,10 +20,11 @@ Gem::Specification.new do |spec|
19
20
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
20
21
  spec.require_paths = ['lib']
21
22
 
23
+ spec.add_development_dependency 'activemodel'
22
24
  spec.add_development_dependency 'activesupport'
23
- spec.add_development_dependency 'bundler', '~> 1.15'
25
+ spec.add_development_dependency 'bundler'
24
26
  spec.add_development_dependency 'grape'
25
- spec.add_development_dependency 'rake', '~> 10.0'
27
+ spec.add_development_dependency 'rake'
26
28
  spec.add_development_dependency 'rspec'
27
29
  spec.add_development_dependency 'webmock'
28
30
  spec.add_runtime_dependency 'activesupport'
@@ -1,29 +1,55 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'hcloud/version'
2
4
  require 'active_support/core_ext/object/to_query'
5
+ require 'active_support/core_ext/hash/indifferent_access'
3
6
 
4
7
  module Hcloud
5
8
  autoload :Error, 'hcloud/errors'
6
9
  autoload :Client, 'hcloud/client'
10
+ autoload :TyphoeusExt, 'hcloud/typhoeus_ext'
7
11
  autoload :AbstractResource, 'hcloud/abstract_resource'
8
- autoload :MultiReply, 'hcloud/multi_reply'
9
- autoload :ServerResource, 'hcloud/server_resource'
10
12
  autoload :EntryLoader, 'hcloud/entry_loader'
13
+
14
+ autoload :Server, 'hcloud/server'
15
+ autoload :ServerResource, 'hcloud/server_resource'
16
+
17
+ autoload :ServerType, 'hcloud/server_type'
18
+ autoload :ServerTypeResource, 'hcloud/server_type_resource'
19
+
11
20
  autoload :FloatingIP, 'hcloud/floating_ip'
12
21
  autoload :FloatingIPResource, 'hcloud/floating_ip_resource'
22
+
13
23
  autoload :SSHKey, 'hcloud/ssh_key'
14
24
  autoload :SSHKeyResource, 'hcloud/ssh_key_resource'
15
- autoload :Server, 'hcloud/server'
16
- autoload :ServerType, 'hcloud/server_type'
17
- autoload :ServerTypeResource, 'hcloud/server_type_resource'
25
+
18
26
  autoload :Datacenter, 'hcloud/datacenter'
19
27
  autoload :DatacenterResource, 'hcloud/datacenter_resource'
28
+
20
29
  autoload :Location, 'hcloud/location'
21
30
  autoload :LocationResource, 'hcloud/location_resource'
31
+
22
32
  autoload :Image, 'hcloud/image'
23
33
  autoload :ImageResource, 'hcloud/image_resource'
34
+
35
+ autoload :Network, 'hcloud/network'
36
+ autoload :NetworkResource, 'hcloud/network_resource'
37
+
38
+ autoload :Volume, 'hcloud/volume'
39
+ autoload :VolumeResource, 'hcloud/volume_resource'
40
+
24
41
  autoload :Action, 'hcloud/action'
25
42
  autoload :ActionResource, 'hcloud/action_resource'
43
+
26
44
  autoload :Iso, 'hcloud/iso'
27
45
  autoload :IsoResource, 'hcloud/iso_resource'
46
+
28
47
  autoload :Pagination, 'hcloud/pagination'
48
+
49
+ COLLECT_ARGS = proc do |method_name, bind|
50
+ query = bind.receiver.method(method_name).parameters.inject({}) do |hash, (_type, name)|
51
+ hash.merge(name => bind.local_variable_get(name))
52
+ end
53
+ query.delete_if { |_, v| v.nil? }
54
+ end
29
55
  end
@@ -1,75 +1,185 @@
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
6
43
 
7
- def initialize(client:, parent: nil, base_path: '')
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
56
+
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.keys.each 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
141
  def each
66
- all.each do |member|
142
+ run.each do |member|
67
143
  yield(member)
68
144
  end
69
145
  end
70
146
 
147
+ # this is just to keep the actual bevahior
148
+ def pagination
149
+ return :auto if client.auto_pagination
150
+
151
+ run.response.pagination
152
+ end
153
+
71
154
  protected
72
155
 
156
+ def _dup(var, value)
157
+ dup.tap do |res|
158
+ res.instance_variable_set(var, value)
159
+ res.instance_variable_set(:@run, nil)
160
+ end
161
+ end
162
+
163
+ def resource_path
164
+ self.class.resource_path || self.class.resource_url
165
+ end
166
+
167
+ def resource_url
168
+ [@base_path.to_s, self.class.resource_url.to_s].reject(&:empty?).join('/')
169
+ end
170
+
171
+ def multi_query(path, **o)
172
+ return prepare_request(path, o.merge(ep: ep)) unless client&.auto_pagination
173
+
174
+ raise Error, 'unable to run auto paginate within concurrent excecution' if @concurrent
175
+
176
+ requests = __entries__(path, **o)
177
+ return requests.flat_map(&:resource) if requests.all? { |req| req.respond_to? :resource }
178
+
179
+ client.hydra.run
180
+ requests.flat_map { |req| req.response.resource }
181
+ end
182
+
73
183
  def page_params(per_page: nil, page: nil)
74
184
  { per_page: per_page || @per_page, page: page || @page }.to_param
75
185
  end
@@ -85,27 +195,24 @@ module Hcloud
85
195
  r.compact.join('&')
86
196
  end
87
197
 
88
- def request(*args)
89
- client.request(*args)
90
- end
91
-
92
198
  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
199
+ first_page = request(path, o.merge(ep: ep(per_page: 1, page: 1))).run
200
+ total_entries = first_page.pagination.total_entries
201
+ return [first_page] if total_entries <= 1 || @limit == 1
202
+
96
203
  unless @limit.nil?
97
- a = @limit if a > @limit
204
+ total_entries = @limit if total_entries > @limit
98
205
  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|
206
+ pages = total_entries / Client::MAX_ENTRIES_PER_PAGE
207
+ pages += 1 if (total_entries % Client::MAX_ENTRIES_PER_PAGE).positive?
208
+ pages.times.map do |page|
102
209
  per_page = Client::MAX_ENTRIES_PER_PAGE
103
- if !@limit.nil? && (r == (i + 1)) && (a % per_page != 0)
104
- per_page = a % per_page
210
+ if !@limit.nil? && (pages == (page + 1)) && (total_entries % per_page != 0)
211
+ per_page = total_entries % per_page
212
+ end
213
+ request(path, o.merge(ep: ep(per_page: per_page, page: page + 1))).tap do |req|
214
+ client.hydra.queue req
105
215
  end
106
- req = request(path, o.merge(ep: ep(per_page: per_page, page: i + 1)))
107
- client.hydra.queue req
108
- req
109
216
  end
110
217
  end
111
218
  end
@@ -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
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  autoload :Typhoeus, 'typhoeus'
2
4
  autoload :Oj, 'oj'
3
5
 
@@ -5,14 +7,43 @@ module Hcloud
5
7
  class Client
6
8
  MAX_ENTRIES_PER_PAGE = 50
7
9
 
8
- attr_reader :token, :auto_pagination, :hydra
9
- def initialize(token:, auto_pagination: false, concurrency: 20)
10
+ class << self
11
+ attr_writer :connection
12
+
13
+ def connection
14
+ return @connection if @connection.is_a? Hcloud::Client
15
+
16
+ raise ArgumentError, "client not correctly initialized, actually #{@client.inspect}"
17
+ end
18
+ end
19
+
20
+ attr_reader :token, :auto_pagination, :hydra, :user_agent
21
+ def initialize(token:, auto_pagination: false, concurrency: 20, user_agent: nil)
10
22
  @token = token
23
+ @user_agent = user_agent || "hcloud-ruby v#{VERSION}"
11
24
  @auto_pagination = auto_pagination
12
25
  @concurrency = concurrency
13
26
  @hydra = Typhoeus::Hydra.new(max_concurrency: concurrency)
14
27
  end
15
28
 
29
+ def concurrent
30
+ @concurrent = true
31
+ ret = yield
32
+ ret.each do |element|
33
+ next unless element.is_a?(AbstractResource)
34
+
35
+ element.run
36
+ end
37
+ hydra.run
38
+ ret
39
+ ensure
40
+ @concurrent = nil
41
+ end
42
+
43
+ def concurrent?
44
+ !@concurrent.nil?
45
+ end
46
+
16
47
  def authorized?
17
48
  request('server_types').run
18
49
  true
@@ -56,8 +87,34 @@ module Hcloud
56
87
  FloatingIPResource.new(client: self)
57
88
  end
58
89
 
59
- def request(path, **options)
60
- code = options.delete(:code)
90
+ def networks
91
+ NetworkResource.new(client: self)
92
+ end
93
+
94
+ def volumes
95
+ VolumeResource.new(client: self)
96
+ end
97
+
98
+ class ResourceFuture < Delegator
99
+ def initialize(request)
100
+ @request = request
101
+ end
102
+
103
+ def __getobj__
104
+ @__getobj__ ||= @request&.response&.resource
105
+ end
106
+ end
107
+
108
+ def prepare_request(url, **args, &block)
109
+ req = request(url, **args.merge(block: block))
110
+ return req.run.resource unless concurrent?
111
+
112
+ hydra.queue req
113
+ ResourceFuture.new(req)
114
+ end
115
+
116
+ def request(path, **options) # rubocop:disable Metrics/MethodLength
117
+ hcloud_attributes = TyphoeusExt.collect_attributes(options)
61
118
  if x = options.delete(:j)
62
119
  options[:body] = Oj.dump(x, mode: :compat)
63
120
  options[:method] ||= :post
@@ -74,35 +131,16 @@ module Hcloud
74
131
  {
75
132
  headers: {
76
133
  'Authorization' => "Bearer #{token}",
134
+ 'User-Agent' => user_agent,
77
135
  'Content-Type' => 'application/json'
78
136
  }
79
137
  }.merge(options)
80
138
  )
81
139
  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
140
+ response.extend(TyphoeusExt)
141
+ response.attributes = hcloud_attributes
142
+ response.context.client = self
143
+ response.check_for_error unless response.request.hydra
106
144
  end
107
145
  r
108
146
  end