rancher.rb 0.1.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.
@@ -0,0 +1,42 @@
1
+ require 'rancher/resource'
2
+ module Rancher
3
+ # A Collection of Resources
4
+ class Collection < Resource
5
+ include Enumerable
6
+
7
+ def initialize(data)
8
+ @data = data[:data] if data.key?(:data)
9
+ super(data)
10
+ end
11
+
12
+ def create(attrs)
13
+ attrs = attrs.meta if attrs.is_a?(Rancher::Resource)
14
+
15
+ Rancher.post get_link('self'), attrs
16
+ end
17
+
18
+ def remove!(id_or_obj)
19
+ id = id_or_obj.get_id if id_or_obj.is_a?(Rancher::Resource)
20
+ link = get_link('self') + "/#{id}"
21
+
22
+ Rancher.delete link
23
+ end
24
+
25
+ def each
26
+ return @data.enum_for(:each) unless block_given?
27
+
28
+ @data.each { |d| yield d }
29
+ end
30
+
31
+ private
32
+
33
+ def schema_field(name)
34
+ type_name = get_type
35
+ type = Rancher.types[type_name.to_sym]
36
+
37
+ type.collection_field(name) if type
38
+
39
+ type
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,84 @@
1
+ module Rancher
2
+ # Configuration options for {Client}, defaulting to values
3
+ # in {Default}
4
+ module Configurable
5
+ # @!attribute api_endpoint
6
+ # @return [String] Base URL for API requests. default: http://localhost:8080/v1/projects/1p1
7
+ # @!attribute access_key
8
+ # @return [String] Access Key from inside rancher
9
+ # @!attribute [w] secret_key
10
+ # @return [String] Secrete Key form inside rancher
11
+ # @!attribute connection_options
12
+ # @see https://github.com/lostisland/faraday
13
+ # @return [Hash] Configure connection options for Faraday
14
+ # @!attribute middleware
15
+ # @see https://Rancher.com/lostisland/faraday
16
+ # @return [Faraday::Builder or Faraday::RackBuilder] Configure middleware for Faraday
17
+ # @!attribute proxy
18
+ # @see https://Rancher.com/lostisland/faraday
19
+ # @return [String] URI for proxy server
20
+ # @!attribute user_agent
21
+ # @return [String] Configure User-Agent header for requests.
22
+
23
+ attr_accessor :access_key, :secret_key, :connection_options,
24
+ :middleware, :proxy, :user_agent, :default_media_type
25
+ attr_writer :api_endpoint
26
+
27
+ class << self
28
+ # List of configurable keys for {Rancher::Client}
29
+ # @return [Array] of option keys
30
+ def keys
31
+ @keys ||= [
32
+ :api_endpoint,
33
+ :access_key,
34
+ :secret_key,
35
+ :connection_options,
36
+ :default_media_type,
37
+ :middleware,
38
+ :proxy,
39
+ :user_agent
40
+ ]
41
+ end
42
+ end
43
+
44
+ # Set configuration options using a block
45
+ def configure
46
+ yield self
47
+ end
48
+
49
+ # Reset configuration options to default values
50
+ def reset!
51
+ Rancher::Configurable.keys.each do |key|
52
+ instance_variable_set(:"@#{key}", Rancher::Default.options[key])
53
+ end
54
+ self
55
+ end
56
+ alias_method :setup, :reset!
57
+
58
+ # Compares client options to a Hash of requested options
59
+ #
60
+ # @param opts [Hash] Options to compare with current client options
61
+ # @return [Boolean]
62
+ def same_options?(opts)
63
+ opts.hash == options.hash
64
+ end
65
+
66
+ def api_endpoint
67
+ File.join(@api_endpoint, '')
68
+ end
69
+
70
+ private
71
+
72
+ def options
73
+ Hash[Rancher::Configurable.keys.map do |key|
74
+ [key, instance_variable_get(:"@#{key}")]
75
+ end
76
+ ]
77
+ end
78
+
79
+ def fetch_access_key_and_secret(overrides = {})
80
+ opts = options.merge(overrides)
81
+ opts.values_at :access_key, :secret_key
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,192 @@
1
+ require 'sawyer'
2
+ require 'rancher/authentication'
3
+ require 'rancher/classify'
4
+ module Rancher
5
+
6
+ # Network layer for API clients.
7
+ module Connection
8
+
9
+ include Rancher::Authentication
10
+ include Rancher::Classify
11
+
12
+ # Header keys that can be passed in options hash to {#get},{#head}
13
+ CONVENIENCE_HEADERS = Set.new([:accept, :content_type])
14
+
15
+ # Make a HTTP GET request
16
+ #
17
+ # @param url [String] The path, relative to {#api_endpoint}
18
+ # @param options [Hash] Query and header params for request
19
+ # @return [Sawyer::Resource]
20
+ def get(url, options = {})
21
+ request :get, url, parse_query_and_convenience_headers(options)
22
+ end
23
+
24
+ # Make a HTTP POST request
25
+ #
26
+ # @param url [String] The path, relative to {#api_endpoint}
27
+ # @param options [Hash] Body and header params for request
28
+ # @return [Sawyer::Resource]
29
+ def post(url, options = {})
30
+ request :post, url, options
31
+ end
32
+
33
+ # Make a HTTP PUT request
34
+ #
35
+ # @param url [String] The path, relative to {#api_endpoint}
36
+ # @param options [Hash] Body and header params for request
37
+ # @return [Sawyer::Resource]
38
+ def put(url, options = {})
39
+ request :put, url, options
40
+ end
41
+
42
+ # Make a HTTP PATCH request
43
+ #
44
+ # @param url [String] The path, relative to {#api_endpoint}
45
+ # @param options [Hash] Body and header params for request
46
+ # @return [Sawyer::Resource]
47
+ def patch(url, options = {})
48
+ request :patch, url, options
49
+ end
50
+
51
+ # Make a HTTP DELETE request
52
+ #
53
+ # @param url [String] The path, relative to {#api_endpoint}
54
+ # @param options [Hash] Query and header params for request
55
+ # @return [Sawyer::Resource]
56
+ def delete(url, options = {})
57
+ request :delete, url, options
58
+ end
59
+
60
+ # Make a HTTP HEAD request
61
+ #
62
+ # @param url [String] The path, relative to {#api_endpoint}
63
+ # @param options [Hash] Query and header params for request
64
+ # @return [Sawyer::Resource]
65
+ def head(url, options = {})
66
+ request :head, url, parse_query_and_convenience_headers(options)
67
+ end
68
+
69
+ # Make one or more HTTP GET requests, optionally fetching
70
+ # the next page of results from URL in Link response header based
71
+ # on value in {#auto_paginate}.
72
+ #
73
+ # @param url [String] The path, relative to {#api_endpoint}
74
+ # @param options [Hash] Query and header params for request
75
+ # @param block [Block] Block to perform the data concatination of the
76
+ # multiple requests. The block is called with two parameters, the first
77
+ # contains the contents of the requests so far and the second parameter
78
+ # contains the latest response.
79
+ # @return [Sawyer::Resource]
80
+ def paginate(url, options = {}, &block)
81
+ opts = parse_query_and_convenience_headers(options.dup)
82
+ if @auto_paginate || @per_page
83
+ opts[:query][:per_page] ||= @per_page || (@auto_paginate ? 100 : nil)
84
+ end
85
+
86
+ data = request(:get, url, opts.dup)
87
+
88
+ if @auto_paginate
89
+ while @last_response.rels[:next] && rate_limit.remaining > 0
90
+ @last_response = @last_response.rels[:next].get(:headers => opts[:headers])
91
+ if block_given?
92
+ yield(data, @last_response)
93
+ else
94
+ data.concat(@last_response.data) if @last_response.data.is_a?(Array)
95
+ end
96
+ end
97
+
98
+ end
99
+
100
+ data
101
+ end
102
+
103
+ # Hypermedia agent for the Rancher API
104
+ #
105
+ # @return [Sawyer::Agent]
106
+ def agent
107
+ @agent ||= Sawyer::Agent.new(endpoint, sawyer_options) do |http|
108
+ http.headers[:accept] = default_media_type
109
+ http.headers[:content_type] = "application/json"
110
+ http.headers[:user_agent] = user_agent
111
+ if basic_authenticated?
112
+ http.basic_auth(@access_key, @secret_key)
113
+ end
114
+ end
115
+ end
116
+
117
+ # Fetch the root resource for the API
118
+ #
119
+ # @return [Sawyer::Resource]
120
+ def root
121
+ get ""
122
+ end
123
+
124
+ # Response for last HTTP request
125
+ #
126
+ # @return [Sawyer::Response]
127
+ def last_response
128
+ @last_response if defined? @last_response
129
+ end
130
+
131
+ protected
132
+
133
+ def endpoint
134
+ api_endpoint
135
+ end
136
+
137
+ private
138
+
139
+ def reset_agent
140
+ @agent = nil
141
+ end
142
+
143
+ def request(method, path, data, options = {})
144
+ if data.is_a?(Hash)
145
+ options[:query] = data.delete(:query) || {}
146
+ options[:headers] = data.delete(:headers) || {}
147
+ if accept = data.delete(:accept)
148
+ options[:headers][:accept] = accept
149
+ end
150
+ end
151
+
152
+ @last_response = response = agent.call(method, URI::Parser.new.escape(path.to_s), data, options)
153
+ classify response.data
154
+ end
155
+
156
+ # Executes the request, checking if it was successful
157
+ #
158
+ # @return [Boolean] True on success, false otherwise
159
+ def boolean_from_response(method, path, options = {})
160
+ request(method, path, options)
161
+ @last_response.status == 204
162
+ rescue Rancher::NotFound
163
+ false
164
+ end
165
+
166
+
167
+ def sawyer_options
168
+ opts = {}
169
+ conn_opts = @connection_options
170
+ conn_opts[:builder] = @middleware if @middleware
171
+ conn_opts[:proxy] = @proxy if @proxy
172
+ opts[:faraday] = Faraday.new(conn_opts)
173
+
174
+ opts
175
+ end
176
+
177
+ def parse_query_and_convenience_headers(options)
178
+ headers = options.delete(:headers) { Hash.new }
179
+ CONVENIENCE_HEADERS.each do |h|
180
+ if header = options.delete(h)
181
+ headers[h] = header
182
+ end
183
+ end
184
+ query = options.delete(:query)
185
+ opts = {:query => options}
186
+ opts[:query].merge!(query) if query && query.is_a?(Hash)
187
+ opts[:headers] = headers unless headers.empty?
188
+
189
+ opts
190
+ end
191
+ end
192
+ end
@@ -0,0 +1,109 @@
1
+ require 'rancher/middleware/follow_redirects'
2
+ require 'rancher/response/raise_error'
3
+ require 'rancher/version'
4
+
5
+ module Rancher
6
+ # Default configuration options for {Client}
7
+ module Default
8
+ # Default API endpoint
9
+ API_ENDPOINT = 'http://localhost:8080/v1/projects/1a5'.freeze
10
+
11
+ # Default User Agent header string
12
+ USER_AGENT = "Rancher Ruby Gem #{Rancher::VERSION}".freeze
13
+
14
+ # Default media type
15
+ MEDIA_TYPE = 'application/json'.freeze
16
+
17
+ # In Faraday 0.9, Faraday::Builder was renamed to Faraday::RackBuilder
18
+ RACK_BUILDER_CLASS = defined?(Faraday::RackBuilder) ? Faraday::RackBuilder : Faraday::Builder
19
+
20
+ # Default Faraday middleware stack
21
+ MIDDLEWARE = RACK_BUILDER_CLASS.new do |builder|
22
+ builder.use Rancher::Middleware::FollowRedirects
23
+ builder.use Rancher::Response::RaiseError
24
+ builder.adapter Faraday.default_adapter
25
+ end
26
+
27
+ class << self
28
+ # Configuration options
29
+ # @return [Hash]
30
+ def options
31
+ Hash[Rancher::Configurable.keys.map { |key| [key, send(key)] }]
32
+ end
33
+
34
+ # Default API endpoint from ENV or {API_ENDPOINT}
35
+ # @return [String]
36
+ def api_endpoint
37
+ ENV['RANCHER_API_ENDPOINT'] || API_ENDPOINT
38
+ end
39
+
40
+ # Default pagination preference from ENV
41
+ # @return [String]
42
+ def auto_paginate
43
+ ENV['RANCHER_AUTO_PAGINATE']
44
+ end
45
+
46
+ # Default OAuth app key from ENV
47
+ # @return [String]
48
+ def access_key
49
+ ENV['RANCHER_CLIENT_ID']
50
+ end
51
+
52
+ # Default OAuth app secret from ENV
53
+ # @return [String]
54
+ def secret_key
55
+ ENV['RANCHER_SECRET']
56
+ end
57
+
58
+ # Default options for Faraday::Connection
59
+ # @return [Hash]
60
+ def connection_options
61
+ {
62
+ headers: {
63
+ accept: default_media_type,
64
+ user_agent: user_agent
65
+ }
66
+ }
67
+ end
68
+
69
+ # Default media type from ENV or {MEDIA_TYPE}
70
+ # @return [String]
71
+ def default_media_type
72
+ ENV['RANCHER_DEFAULT_MEDIA_TYPE'] || MEDIA_TYPE
73
+ end
74
+
75
+ # Default middleware stack for Faraday::Connection
76
+ # from {MIDDLEWARE}
77
+ # @return [String]
78
+ def middleware
79
+ MIDDLEWARE
80
+ end
81
+
82
+ # Default pagination page size from ENV
83
+ # @return [Fixnum] Page size
84
+ def per_page
85
+ page_size = ENV['RANCHER_PER_PAGE']
86
+
87
+ page_size.to_i if page_size
88
+ end
89
+
90
+ # Default proxy server URI for Faraday connection from ENV
91
+ # @return [String]
92
+ def proxy
93
+ ENV['RANCHER_PROXY']
94
+ end
95
+
96
+ # Default User-Agent header string from ENV or {USER_AGENT}
97
+ # @return [String]
98
+ def user_agent
99
+ ENV['RANCHER_USER_AGENT'] || USER_AGENT
100
+ end
101
+
102
+ # Default web endpoint from ENV or {WEB_ENDPOINT}
103
+ # @return [String]
104
+ def web_endpoint
105
+ ENV['RANCHER_WEB_ENDPOINT'] || WEB_ENDPOINT
106
+ end
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,241 @@
1
+ module Rancher
2
+ # Custom error class for rescuing from all Rancher errors
3
+ class Error < StandardError
4
+
5
+ # Returns the appropriate Rancher::Error subclass based
6
+ # on status and response message
7
+ #
8
+ # @param [Hash] response HTTP response
9
+ # @return [Rancher::Error]
10
+ def self.from_response(response)
11
+ status = response[:status].to_i
12
+ body = response[:body].to_s
13
+ headers = response[:response_headers]
14
+
15
+ if klass = case status
16
+ when 400 then Rancher::BadRequest
17
+ when 401 then error_for_401(headers)
18
+ when 403 then error_for_403(body)
19
+ when 404 then Rancher::NotFound
20
+ when 405 then Rancher::MethodNotAllowed
21
+ when 406 then Rancher::NotAcceptable
22
+ when 409 then Rancher::Conflict
23
+ when 415 then Rancher::UnsupportedMediaType
24
+ when 422 then Rancher::UnprocessableEntity
25
+ when 400..499 then Rancher::ClientError
26
+ when 500 then Rancher::InternalServerError
27
+ when 501 then Rancher::NotImplemented
28
+ when 502 then Rancher::BadGateway
29
+ when 503 then Rancher::ServiceUnavailable
30
+ when 500..599 then Rancher::ServerError
31
+ end
32
+ klass.new(response)
33
+ end
34
+ end
35
+
36
+ def initialize(response=nil)
37
+ @response = response
38
+ super(build_error_message)
39
+ end
40
+
41
+ # Documentation URL returned by the API for some errors
42
+ #
43
+ # @return [String]
44
+ def documentation_url
45
+ data[:documentation_url] if data.is_a? Hash
46
+ end
47
+
48
+ # Returns most appropriate error for 401 HTTP status code
49
+ # @private
50
+ def self.error_for_401(headers)
51
+ if Rancher::OneTimePasswordRequired.required_header(headers)
52
+ Rancher::OneTimePasswordRequired
53
+ else
54
+ Rancher::Unauthorized
55
+ end
56
+ end
57
+
58
+ # Returns most appropriate error for 403 HTTP status code
59
+ # @private
60
+ def self.error_for_403(body)
61
+ if body =~ /rate limit exceeded/i
62
+ Rancher::TooManyRequests
63
+ elsif body =~ /login attempts exceeded/i
64
+ Rancher::TooManyLoginAttempts
65
+ elsif body =~ /abuse/i
66
+ Rancher::AbuseDetected
67
+ elsif body =~ /repository access blocked/i
68
+ Rancher::RepositoryUnavailable
69
+ else
70
+ Rancher::Forbidden
71
+ end
72
+ end
73
+
74
+ # Array of validation errors
75
+ # @return [Array<Hash>] Error info
76
+ def errors
77
+ if data && data.is_a?(Hash)
78
+ data[:errors] || []
79
+ else
80
+ []
81
+ end
82
+ end
83
+
84
+ private
85
+
86
+ def data
87
+ @data ||=
88
+ if (body = @response[:body]) && !body.empty?
89
+ if body.is_a?(String) &&
90
+ @response[:response_headers] &&
91
+ @response[:response_headers][:content_type] =~ /json/
92
+
93
+ Sawyer::Agent.serializer.decode(body)
94
+ else
95
+ body
96
+ end
97
+ else
98
+ nil
99
+ end
100
+ end
101
+
102
+ def response_message
103
+ case data
104
+ when Hash
105
+ data[:message]
106
+ when String
107
+ data
108
+ end
109
+ end
110
+
111
+ def response_error
112
+ "Error: #{data[:error]}" if data.is_a?(Hash) && data[:error]
113
+ end
114
+
115
+ def response_error_summary
116
+ return nil unless data.is_a?(Hash) && !Array(data[:errors]).empty?
117
+
118
+ summary = "\nError summary:\n"
119
+ summary << data[:errors].map do |hash|
120
+ hash.map { |k,v| " #{k}: #{v}" }
121
+ end.join("\n")
122
+
123
+ summary
124
+ end
125
+
126
+ def build_error_message
127
+ return nil if @response.nil?
128
+
129
+ message = "#{@response[:method].to_s.upcase} "
130
+ message << redact_url(@response[:url].to_s) + ": "
131
+ message << "#{@response[:status]} - "
132
+ message << "#{response_message}" unless response_message.nil?
133
+ message << "#{response_error}" unless response_error.nil?
134
+ message << "#{response_error_summary}" unless response_error_summary.nil?
135
+ message << " // See: #{documentation_url}" unless documentation_url.nil?
136
+ message
137
+ end
138
+
139
+ def redact_url(url_string)
140
+ %w[secret_key access_token].each do |token|
141
+ url_string.gsub!(/#{token}=\S+/, "#{token}=(redacted)") if url_string.include? token
142
+ end
143
+ url_string
144
+ end
145
+ end
146
+
147
+ # Raised on errors in the 400-499 range
148
+ class ClientError < Error; end
149
+
150
+ # Raised when Rancher returns a 400 HTTP status code
151
+ class BadRequest < ClientError; end
152
+
153
+ # Raised when Rancher returns a 401 HTTP status code
154
+ class Unauthorized < ClientError; end
155
+
156
+ # Raised when Rancher returns a 401 HTTP status code
157
+ # and headers include "X-Rancher-OTP"
158
+ class OneTimePasswordRequired < ClientError
159
+ #@private
160
+ OTP_DELIVERY_PATTERN = /required; (\w+)/i
161
+
162
+ #@private
163
+ def self.required_header(headers)
164
+ OTP_DELIVERY_PATTERN.match headers['X-Rancher-OTP'].to_s
165
+ end
166
+
167
+ # Delivery method for the user's OTP
168
+ #
169
+ # @return [String]
170
+ def password_delivery
171
+ @password_delivery ||= delivery_method_from_header
172
+ end
173
+
174
+ private
175
+
176
+ def delivery_method_from_header
177
+ if match = self.class.required_header(@response[:response_headers])
178
+ match[1]
179
+ end
180
+ end
181
+ end
182
+
183
+ # Raised when Rancher returns a 403 HTTP status code
184
+ class Forbidden < ClientError; end
185
+
186
+ # Raised when Rancher returns a 403 HTTP status code
187
+ # and body matches 'rate limit exceeded'
188
+ class TooManyRequests < Forbidden; end
189
+
190
+ # Raised when Rancher returns a 403 HTTP status code
191
+ # and body matches 'login attempts exceeded'
192
+ class TooManyLoginAttempts < Forbidden; end
193
+
194
+ # Raised when Rancher returns a 403 HTTP status code
195
+ # and body matches 'abuse'
196
+ class AbuseDetected < Forbidden; end
197
+
198
+ # Raised when Rancher returns a 403 HTTP status code
199
+ # and body matches 'repository access blocked'
200
+ class RepositoryUnavailable < Forbidden; end
201
+
202
+ # Raised when Rancher returns a 404 HTTP status code
203
+ class NotFound < ClientError; end
204
+
205
+ # Raised when Rancher returns a 405 HTTP status code
206
+ class MethodNotAllowed < ClientError; end
207
+
208
+ # Raised when Rancher returns a 406 HTTP status code
209
+ class NotAcceptable < ClientError; end
210
+
211
+ # Raised when Rancher returns a 409 HTTP status code
212
+ class Conflict < ClientError; end
213
+
214
+ # Raised when Rancher returns a 414 HTTP status code
215
+ class UnsupportedMediaType < ClientError; end
216
+
217
+ # Raised when Rancher returns a 422 HTTP status code
218
+ class UnprocessableEntity < ClientError; end
219
+
220
+ # Raised on errors in the 500-599 range
221
+ class ServerError < Error; end
222
+
223
+ # Raised when Rancher returns a 500 HTTP status code
224
+ class InternalServerError < ServerError; end
225
+
226
+ # Raised when Rancher returns a 501 HTTP status code
227
+ class NotImplemented < ServerError; end
228
+
229
+ # Raised when Rancher returns a 502 HTTP status code
230
+ class BadGateway < ServerError; end
231
+
232
+ # Raised when Rancher returns a 503 HTTP status code
233
+ class ServiceUnavailable < ServerError; end
234
+
235
+ # Raised when client fails to provide valid Content-Type
236
+ class MissingContentType < ArgumentError; end
237
+
238
+ # Raised when a method requires an application access_key
239
+ # and secret but none is provided
240
+ class ApplicationCredentialsRequired < StandardError; end
241
+ end