rancher.rb 0.1.0

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