hcloud 0.1.2 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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