engineyard-cloud-client 0.1.2

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 (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