engineyard-cloud-client 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (58) hide show
  1. data/LICENSE +19 -0
  2. data/README.rdoc +7 -0
  3. data/lib/engineyard-cloud-client.rb +149 -0
  4. data/lib/engineyard-cloud-client/errors.rb +38 -0
  5. data/lib/engineyard-cloud-client/model_registry.rb +21 -0
  6. data/lib/engineyard-cloud-client/models.rb +14 -0
  7. data/lib/engineyard-cloud-client/models/account.rb +38 -0
  8. data/lib/engineyard-cloud-client/models/api_struct.rb +50 -0
  9. data/lib/engineyard-cloud-client/models/app.rb +77 -0
  10. data/lib/engineyard-cloud-client/models/app_environment.rb +85 -0
  11. data/lib/engineyard-cloud-client/models/deployment.rb +105 -0
  12. data/lib/engineyard-cloud-client/models/environment.rb +240 -0
  13. data/lib/engineyard-cloud-client/models/instance.rb +15 -0
  14. data/lib/engineyard-cloud-client/models/keypair.rb +32 -0
  15. data/lib/engineyard-cloud-client/models/log.rb +11 -0
  16. data/lib/engineyard-cloud-client/models/user.rb +11 -0
  17. data/lib/engineyard-cloud-client/resolver_result.rb +19 -0
  18. data/lib/engineyard-cloud-client/rest_client_ext.rb +11 -0
  19. data/lib/engineyard-cloud-client/ruby_ext.rb +9 -0
  20. data/lib/engineyard-cloud-client/test.rb +31 -0
  21. data/lib/engineyard-cloud-client/test/fake_awsm.rb +22 -0
  22. data/lib/engineyard-cloud-client/test/fake_awsm/config.ru +207 -0
  23. data/lib/engineyard-cloud-client/test/fake_awsm/models.rb +9 -0
  24. data/lib/engineyard-cloud-client/test/fake_awsm/models/account.rb +13 -0
  25. data/lib/engineyard-cloud-client/test/fake_awsm/models/app.rb +24 -0
  26. data/lib/engineyard-cloud-client/test/fake_awsm/models/app_environment.rb +19 -0
  27. data/lib/engineyard-cloud-client/test/fake_awsm/models/deployments.rb +15 -0
  28. data/lib/engineyard-cloud-client/test/fake_awsm/models/environment.rb +25 -0
  29. data/lib/engineyard-cloud-client/test/fake_awsm/models/instance.rb +23 -0
  30. data/lib/engineyard-cloud-client/test/fake_awsm/models/user.rb +15 -0
  31. data/lib/engineyard-cloud-client/test/fake_awsm/scenarios.rb +325 -0
  32. data/lib/engineyard-cloud-client/test/fake_awsm/views/accounts.rabl +2 -0
  33. data/lib/engineyard-cloud-client/test/fake_awsm/views/apps.rabl +10 -0
  34. data/lib/engineyard-cloud-client/test/fake_awsm/views/base_app_environment.rabl +13 -0
  35. data/lib/engineyard-cloud-client/test/fake_awsm/views/base_environment.rabl +4 -0
  36. data/lib/engineyard-cloud-client/test/fake_awsm/views/environments.rabl +11 -0
  37. data/lib/engineyard-cloud-client/test/fake_awsm/views/instances.rabl +2 -0
  38. data/lib/engineyard-cloud-client/test/fake_awsm/views/resolve_app_environments.rabl +7 -0
  39. data/lib/engineyard-cloud-client/test/fake_awsm/views/resolve_environments.rabl +7 -0
  40. data/lib/engineyard-cloud-client/test/fake_awsm/views/user.rabl +2 -0
  41. data/lib/engineyard-cloud-client/test/scenario.rb +43 -0
  42. data/lib/engineyard-cloud-client/test/ui.rb +33 -0
  43. data/lib/engineyard-cloud-client/version.rb +7 -0
  44. data/spec/engineyard-cloud-client/api_spec.rb +59 -0
  45. data/spec/engineyard-cloud-client/integration/account_spec.rb +18 -0
  46. data/spec/engineyard-cloud-client/integration/app_environment_spec.rb +38 -0
  47. data/spec/engineyard-cloud-client/integration/app_spec.rb +20 -0
  48. data/spec/engineyard-cloud-client/integration/environment_spec.rb +57 -0
  49. data/spec/engineyard-cloud-client/integration/user_spec.rb +18 -0
  50. data/spec/engineyard-cloud-client/models/api_struct_spec.rb +41 -0
  51. data/spec/engineyard-cloud-client/models/app_spec.rb +64 -0
  52. data/spec/engineyard-cloud-client/models/environment_spec.rb +300 -0
  53. data/spec/engineyard-cloud-client/models/instance_spec.rb +44 -0
  54. data/spec/engineyard-cloud-client/models/keypair_spec.rb +58 -0
  55. data/spec/spec_helper.rb +50 -0
  56. data/spec/support/helpers.rb +16 -0
  57. data/spec/support/matchers.rb +2 -0
  58. metadata +377 -0
data/LICENSE ADDED
@@ -0,0 +1,19 @@
1
+ Copyright (c) 2010 Engine Yard, Inc
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in
11
+ all copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,7 @@
1
+ = engineyard-cloud-client
2
+
3
+ The API client for awsm. Extracted from the engineyard gem because this piece is often all a project needs for EY Cloud API usage.
4
+
5
+ See engineyard gem for a usage example. Bother someone if this is released and the documentation is still not updated.
6
+
7
+ This is currently unreleased.
@@ -0,0 +1,149 @@
1
+ module EY
2
+ class CloudClient
3
+ end
4
+ end
5
+
6
+ require 'engineyard-cloud-client/ruby_ext'
7
+ require 'engineyard-cloud-client/model_registry'
8
+ require 'engineyard-cloud-client/models'
9
+ require 'engineyard-cloud-client/rest_client_ext'
10
+ require 'engineyard-cloud-client/resolver_result'
11
+ require 'engineyard-cloud-client/version'
12
+ require 'engineyard-cloud-client/errors'
13
+ require 'multi_json'
14
+ require 'pp'
15
+
16
+ module EY
17
+ class CloudClient
18
+ attr_reader :token, :registry
19
+ attr_accessor :ui
20
+
21
+ USER_AGENT_STRING = "EngineYardCloudClient/#{EY::CloudClient::VERSION}"
22
+
23
+ def self.endpoint
24
+ @endpoint
25
+ end
26
+
27
+ def self.endpoint=(endpoint)
28
+ @endpoint = URI.parse(endpoint)
29
+ unless @endpoint.absolute?
30
+ raise BadEndpointError.new(endpoint)
31
+ end
32
+ @endpoint
33
+ end
34
+
35
+ def self.default_endpoint!
36
+ self.endpoint = "https://cloud.engineyard.com/"
37
+ end
38
+ default_endpoint!
39
+
40
+ def initialize(token, ui)
41
+ self.token = token
42
+ self.ui = ui
43
+ end
44
+
45
+ def ==(other)
46
+ other.is_a?(self.class) && other.token == token
47
+ end
48
+
49
+ def registry
50
+ @registry ||= ModelRegistry.new
51
+ end
52
+
53
+ def token=(new_token)
54
+ unless new_token
55
+ raise ArgumentError, "EY Cloud API token required"
56
+ end
57
+ @token = new_token
58
+ end
59
+
60
+ def request(url, opts={})
61
+ opts[:headers] ||= {}
62
+ opts[:headers]["X-EY-Cloud-Token"] = token
63
+ ui.debug("Token", token)
64
+ self.class.request(url, ui, opts)
65
+ end
66
+
67
+ def resolve_environments(constraints)
68
+ EY::CloudClient::Environment.resolve(self, constraints)
69
+ end
70
+
71
+ def resolve_app_environments(constraints)
72
+ EY::CloudClient::AppEnvironment.resolve(self, constraints)
73
+ end
74
+
75
+ def environments
76
+ @environments ||= EY::CloudClient::Environment.all(self)
77
+ end
78
+
79
+ def apps
80
+ @apps ||= EY::CloudClient::App.all(self)
81
+ end
82
+
83
+ # TODO: unhaxor
84
+ # This should load an api endpoint that deals directly in app_deployments
85
+ def app_environments
86
+ @app_environments ||= apps.map { |app| app.app_environments }.flatten
87
+ end
88
+
89
+ def current_user
90
+ EY::CloudClient::User.from_hash(self, request('/current_user')['user'])
91
+ end
92
+
93
+ def self.request(path, ui, opts={})
94
+ url = self.endpoint + "api/v2#{path}"
95
+ method = (opts.delete(:method) || 'get').to_s.downcase.to_sym
96
+ params = opts.delete(:params) || {}
97
+ headers = opts.delete(:headers) || {}
98
+ headers["Accept"] ||= "application/json"
99
+ headers["User-Agent"] = USER_AGENT_STRING
100
+
101
+ begin
102
+ ui.debug("Request", "#{method.to_s.upcase} #{url}")
103
+ ui.debug("Params", params.inspect)
104
+ case method
105
+ when :get, :delete, :head
106
+ unless params.empty?
107
+ url.query = RestClient::Payload::UrlEncoded.new(params).to_s
108
+ end
109
+ resp = RestClient.send(method, url.to_s, headers)
110
+ else
111
+ resp = RestClient.send(method, url.to_s, params, headers)
112
+ end
113
+ rescue RestClient::Unauthorized
114
+ raise InvalidCredentials
115
+ rescue Errno::ECONNREFUSED
116
+ raise RequestFailed, "Could not reach the cloud API"
117
+ rescue RestClient::ResourceNotFound
118
+ raise ResourceNotFound, "The requested resource could not be found"
119
+ rescue RestClient::BadGateway
120
+ raise RequestFailed, "EY Cloud API is temporarily unavailable. Please try again soon."
121
+ rescue RestClient::RequestFailed => e
122
+ raise RequestFailed, "#{e.message} #{e.response}"
123
+ rescue OpenSSL::SSL::SSLError
124
+ raise RequestFailed, "SSL is misconfigured on your cloud"
125
+ end
126
+
127
+ if resp.body.empty?
128
+ data = ''
129
+ elsif resp.headers[:content_type] =~ /application\/json/
130
+ begin
131
+ data = MultiJson.decode(resp.body)
132
+ ui.debug("Response", "\n" + data.pretty_inspect)
133
+ rescue MultiJson::DecodeError
134
+ ui.debug("Raw response", resp.body)
135
+ raise RequestFailed, "Response was not valid JSON."
136
+ end
137
+ else
138
+ data = resp.body
139
+ end
140
+
141
+ data
142
+ end
143
+
144
+ def self.authenticate(email, password, ui)
145
+ request("/authenticate", ui, { :method => "post", :params => { :email => email, :password => password }})["api_token"]
146
+ end
147
+
148
+ end # API
149
+ end # EY
@@ -0,0 +1,38 @@
1
+ module EY
2
+ class CloudClient
3
+ class Error < RuntimeError
4
+ end
5
+
6
+ class RequestFailed < Error; end
7
+ class InvalidCredentials < RequestFailed; end
8
+ class ResourceNotFound < RequestFailed; end
9
+
10
+ class BadEndpointError < Error
11
+ def initialize(endpoint)
12
+ super "#{endpoint.inspect} is not a valid endpoint URI. Endpoint must be an absolute URI."
13
+ end
14
+ end
15
+
16
+ class AttributeRequiredError < Error
17
+ def initialize(attribute_name, klass = nil)
18
+ if klass
19
+ super "Attribute '#{attribute_name}' of class #{klass} is required for this action."
20
+ else
21
+ super "Attribute '#{attribute_name}' is required for this action."
22
+ end
23
+ end
24
+ end
25
+
26
+ class NoBridgeError < Error
27
+ def initialize(env_name)
28
+ super "The environment '#{env_name}' does not have a master instance."
29
+ end
30
+ end
31
+
32
+ class BadBridgeStatusError < Error
33
+ def initialize(bridge_status, endpoint)
34
+ super %|Application master's status is not "running" (green); it is "#{bridge_status}". Go to #{endpoint} to address this problem.|
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,21 @@
1
+ module EY
2
+ class CloudClient
3
+ class ModelRegistry
4
+ def initialize
5
+ @registry = Hash.new { |h,k| h[k] = {} }
6
+ end
7
+
8
+ def find(klass, id)
9
+ if id
10
+ @registry[klass][id]
11
+ end
12
+ end
13
+
14
+ def set(klass, obj)
15
+ if obj.respond_to?(:id) && id = obj.id
16
+ @registry[klass][id] = obj
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,14 @@
1
+ module EY
2
+ class CloudClient
3
+ require 'engineyard-cloud-client/models/api_struct'
4
+ require 'engineyard-cloud-client/models/account'
5
+ require 'engineyard-cloud-client/models/app'
6
+ require 'engineyard-cloud-client/models/app_environment'
7
+ require 'engineyard-cloud-client/models/deployment'
8
+ require 'engineyard-cloud-client/models/environment'
9
+ require 'engineyard-cloud-client/models/log'
10
+ require 'engineyard-cloud-client/models/instance'
11
+ require 'engineyard-cloud-client/models/keypair'
12
+ require 'engineyard-cloud-client/models/user'
13
+ end
14
+ end
@@ -0,0 +1,38 @@
1
+ require 'engineyard-cloud-client/models/api_struct'
2
+
3
+ module EY
4
+ class CloudClient
5
+ class Account < ApiStruct.new(:id, :name)
6
+
7
+ def self.all(api)
8
+ self.from_array(api, api.request('/accounts')["accounts"])
9
+ end
10
+
11
+ def add_app(app)
12
+ @apps ||= []
13
+ existing_app = @apps.detect { |a| app.id == a.id }
14
+ unless existing_app
15
+ @apps << app
16
+ end
17
+ existing_app || app
18
+ end
19
+
20
+ def apps
21
+ @apps ||= []
22
+ end
23
+
24
+ def add_environment(environment)
25
+ @environments ||= []
26
+ existing_environment = @environments.detect { |env| environment.id == env.id }
27
+ unless existing_environment
28
+ @environments << environment
29
+ end
30
+ existing_environment || environment
31
+ end
32
+
33
+ def environments
34
+ @environments ||= []
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,50 @@
1
+ module EY
2
+ class CloudClient
3
+ class ApiStruct < Struct
4
+ def self.new(*args, &block)
5
+ args = [:api] | args
6
+ super(*args) do |*block_args|
7
+ block.call(*block_args) if block
8
+
9
+ def self.from_array(api, array, common_values = {})
10
+ if array
11
+ array.map do |values|
12
+ from_hash(api, values.merge(common_values))
13
+ end
14
+ end
15
+ end
16
+
17
+ def self.from_hash(api, attrs_or_struct)
18
+ return nil unless attrs_or_struct
19
+
20
+ if attrs_or_struct.respond_to?(:attributes=)
21
+ # already a model
22
+ obj = attrs_or_struct
23
+ elsif obj = api.registry.find(self, attrs_or_struct['id'])
24
+ obj.attributes = attrs_or_struct
25
+ else
26
+ obj = new(api, attrs_or_struct)
27
+ api.registry.set(self, obj)
28
+ end
29
+ obj
30
+ end
31
+ end
32
+ end
33
+
34
+ def initialize(api, attrs)
35
+ self.api = api
36
+ self.attributes = attrs
37
+ end
38
+
39
+ def attributes=(attrs)
40
+ attrs.each do |key, val|
41
+ setter = :"#{key}="
42
+ if respond_to?(setter)
43
+ send(setter, val)
44
+ end
45
+ end
46
+ end
47
+
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,77 @@
1
+ require 'engineyard-cloud-client/errors'
2
+ require 'engineyard-cloud-client/models'
3
+
4
+ module EY
5
+ class CloudClient
6
+ class App < ApiStruct.new(:id, :name, :repository_uri, :app_type_id)
7
+
8
+ attr_reader :app_environments, :account
9
+
10
+ # Return list of all Apps linked to all current user's accounts
11
+ def self.all(api)
12
+ self.from_array(api, api.request('/apps')["apps"])
13
+ end
14
+
15
+ # An everything-you-need helper to create an App
16
+ # If successful, returns new App
17
+ # If unsuccessful, raises +EY::CloudClient::RequestFailed+
18
+ #
19
+ # Usage
20
+ # App.create(api,
21
+ # account: account # requires: account.id
22
+ # name: "myapp",
23
+ # repository_uri: "git@github.com:mycompany/myapp.git",
24
+ # app_type_id: "rails3",
25
+ # )
26
+ #
27
+ # NOTE: Syntax above is for Ruby 1.9. In Ruby 1.8, keys must all be strings.
28
+ def self.create(api, attrs = {})
29
+ account = attrs.delete("account")
30
+ params = attrs.dup # no default fields
31
+ raise EY::CloudClient::AttributeRequiredError.new("account", EY::CloudClient::Account) unless account
32
+ raise EY::CloudClient::AttributeRequiredError.new("name") unless params["name"]
33
+ raise EY::CloudClient::AttributeRequiredError.new("repository_uri") unless params["repository_uri"]
34
+ raise EY::CloudClient::AttributeRequiredError.new("app_type_id") unless params["app_type_id"]
35
+ response = api.request("/accounts/#{account.id}/apps", :method => :post, :params => {"app" => params})
36
+ from_hash(api, response['app'])
37
+ end
38
+
39
+ def account_name
40
+ account && account.name
41
+ end
42
+
43
+ def environments
44
+ (app_environments || []).map { |app_env| app_env.environment }
45
+ end
46
+
47
+ def add_app_environment(app_env)
48
+ @app_environments ||= []
49
+ existing_app_env = @app_environments.detect { |ae| app_env.environment == ae.environment }
50
+ unless existing_app_env
51
+ @app_environments << app_env
52
+ end
53
+ existing_app_env || app_env
54
+ end
55
+
56
+ def set_account(account_attrs)
57
+ @account = Account.from_hash(api, account_attrs)
58
+ @account.add_app(self)
59
+ @account
60
+ end
61
+
62
+ def set_environments(environments_attrs)
63
+ (environments_attrs || []).each do |env|
64
+ AppEnvironment.from_hash(api, {'app' => self, 'environment' => env})
65
+ end
66
+ end
67
+
68
+ def attributes=(attrs)
69
+ account_attrs = attrs.delete('account')
70
+ environments_attrs = attrs.delete('environments')
71
+ super
72
+ set_account account_attrs if account_attrs
73
+ set_environments environments_attrs if environments_attrs
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,85 @@
1
+ require 'launchy'
2
+ require 'engineyard-cloud-client/models'
3
+ require 'engineyard-cloud-client/errors'
4
+
5
+ module EY
6
+ class CloudClient
7
+ class AppEnvironment < ApiStruct.new(:id, :app, :environment, :uri, :domain_name, :migrate_command, :migrate)
8
+
9
+ # Return a constrained list of app_environments given a set of constraints like:
10
+ #
11
+ # * app_name
12
+ # * account_name
13
+ # * environment_name
14
+ # * remotes: An array of git remote URIs
15
+ #
16
+ def self.resolve(api, constraints)
17
+ clean_constraints = constraints.reject { |k,v| v.nil? }
18
+ params = {'constraints' => clean_constraints}
19
+ response = api.request("/app_environments/resolve", :method => :get, :params => params)['resolver']
20
+ matches = from_array(api, response['matches'])
21
+ ResolverResult.new(api, matches, response['errors'], response['suggestions'])
22
+ end
23
+
24
+ def initialize(api, attrs)
25
+ super
26
+
27
+ raise ArgumentError, 'AppEnvironment created without app!' unless app
28
+ raise ArgumentError, 'AppEnvironment created without environment!' unless environment
29
+ end
30
+
31
+ def set_app(app_or_hash)
32
+ self.app = App.from_hash(api, app_or_hash)
33
+ app.add_app_environment(self)
34
+ app
35
+ end
36
+
37
+ def set_environment(env_or_hash)
38
+ self.environment = Environment.from_hash(api, env_or_hash)
39
+ environment.add_app_environment(self)
40
+ environment
41
+ end
42
+
43
+ def attributes=(attrs)
44
+ app_attrs = attrs.delete('app')
45
+ environment_attrs = attrs.delete('environment')
46
+ super
47
+ set_app app_attrs if app_attrs
48
+ set_environment environment_attrs if environment_attrs
49
+ end
50
+
51
+ def account_name
52
+ app.account_name
53
+ end
54
+
55
+ def app_name
56
+ app.name
57
+ end
58
+
59
+ def environment_name
60
+ environment.name
61
+ end
62
+
63
+ def repository_uri
64
+ app.repository_uri
65
+ end
66
+
67
+ def to_hierarchy_str
68
+ [account_name, app_name, environment_name].join('/')
69
+ end
70
+
71
+ def last_deployment
72
+ Deployment.last(api, self)
73
+ end
74
+
75
+ def new_deployment(attrs)
76
+ Deployment.from_hash(api, attrs.merge(:app_environment => self))
77
+ end
78
+
79
+ def short_environment_name
80
+ environment.name.gsub(/^#{Regexp.quote(app.name)}_/, '')
81
+ end
82
+
83
+ end
84
+ end
85
+ end