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
@@ -0,0 +1,105 @@
1
+ require 'engineyard-cloud-client/models'
2
+ require 'engineyard-cloud-client/errors'
3
+
4
+ module EY
5
+ class CloudClient
6
+ class Deployment < ApiStruct.new(:id, :app_environment, :created_at, :commit, :finished_at, :migrate_command, :ref, :resolved_ref, :successful, :user_name, :extra_config)
7
+ def self.api_root(app_id, environment_id)
8
+ "/apps/#{app_id}/environments/#{environment_id}/deployments"
9
+ end
10
+
11
+ def self.last(api, app_environment)
12
+ get(api, app_environment, 'last')
13
+ end
14
+
15
+ def self.get(api, app_environment, id)
16
+ uri = api_root(app_environment.app.id, app_environment.environment.id) + "/#{id}"
17
+ response = api.request(uri, :method => :get)
18
+ load_from_response api, app_environment, response
19
+ rescue EY::CloudClient::ResourceNotFound
20
+ nil
21
+ end
22
+
23
+ def self.load_from_response(api, app_environment, response)
24
+ dep = from_hash(api, {:app_environment => app_environment})
25
+ dep.update_with_response(response)
26
+ dep
27
+ end
28
+
29
+ def app
30
+ app_environment.app
31
+ end
32
+
33
+ def environment
34
+ app_environment.environment
35
+ end
36
+
37
+ def migrate
38
+ !migrate_command.nil? && !migrate_command.to_s.empty?
39
+ end
40
+ alias migrate? migrate
41
+ alias migration_command migrate_command
42
+ alias migration_command= migrate_command=
43
+
44
+ alias successful? successful
45
+
46
+ alias deployed_by user_name
47
+ alias deployed_by= user_name=
48
+
49
+ def config
50
+ @config ||= {'deployed_by' => deployed_by}.merge(extra_config)
51
+ end
52
+
53
+ def start
54
+ params = { :migrate => migrate, :ref => ref }
55
+ params[:migrate_command] = migrate_command if migrate
56
+ post_to_api(params)
57
+ end
58
+
59
+ def output
60
+ @output ||= StringIO.new
61
+ end
62
+
63
+ def out
64
+ output
65
+ end
66
+
67
+ def err
68
+ output
69
+ end
70
+
71
+ def finished
72
+ output.rewind
73
+ put_to_api({:successful => successful, :output => output.read})
74
+ end
75
+
76
+ def finished?
77
+ !finished_at.nil?
78
+ end
79
+
80
+ def update_with_response(response)
81
+ response['deployment'].each do |key,val|
82
+ send("#{key}=", val) if respond_to?("#{key}=")
83
+ end
84
+ end
85
+
86
+ private
87
+
88
+ def post_to_api(params)
89
+ update_with_response api.request(collection_uri, :method => :post, :params => {:deployment => params})
90
+ end
91
+
92
+ def put_to_api(params)
93
+ update_with_response api.request(member_uri("/finished"), :method => :put, :params => {:deployment => params})
94
+ end
95
+
96
+ def collection_uri
97
+ self.class.api_root(app.id, environment.id)
98
+ end
99
+
100
+ def member_uri(path = nil)
101
+ collection_uri + "/#{id}#{path}"
102
+ end
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,240 @@
1
+ require 'engineyard-cloud-client/models'
2
+ require 'engineyard-cloud-client/errors'
3
+
4
+ module EY
5
+ class CloudClient
6
+ class Environment < ApiStruct.new(:id, :name, :framework_env, :instances_count,
7
+ :username, :app_server_stack_name,
8
+ :load_balancer_ip_address
9
+ )
10
+ attr_accessor :ignore_bad_bridge, :apps, :account
11
+
12
+ def attributes=(attrs)
13
+ account_attrs = attrs.delete('account')
14
+ apps_attrs = attrs.delete('apps')
15
+ instances_attrs = attrs.delete('instances')
16
+
17
+ super
18
+
19
+ set_account account_attrs if account_attrs
20
+ set_apps apps_attrs if apps_attrs
21
+ set_instances instances_attrs if instances_attrs
22
+ end
23
+
24
+ def add_app_environment(app_env)
25
+ @app_environments ||= []
26
+ existing_app_env = @app_environments.detect { |ae| app_env.environment == ae.environment }
27
+ unless existing_app_env
28
+ @app_environments << app_env
29
+ end
30
+ existing_app_env || app_env
31
+ end
32
+
33
+ def app_environments
34
+ @app_environments ||= []
35
+ end
36
+
37
+ def set_account(account_attrs)
38
+ @account = Account.from_hash(api, account_attrs)
39
+ @account.add_environment(self)
40
+ @account
41
+ end
42
+
43
+ # Creating an AppEnvironment will come back and call add_app_environment
44
+ # (above) to associate this model with the AppEnvironment. (that's why we
45
+ # don't save anything here.)
46
+ def set_apps(apps_attrs)
47
+ (apps_attrs || []).each do |app|
48
+ AppEnvironment.from_hash(api, {'app' => app, 'environment' => self})
49
+ end
50
+ end
51
+
52
+ def apps
53
+ app_environments.map { |app_env| app_env.app }
54
+ end
55
+
56
+ def set_instances(instances_attrs)
57
+ @instances = load_instances(instances_attrs)
58
+ end
59
+
60
+ def instances
61
+ @instances ||= request_instances
62
+ end
63
+
64
+ # Return list of all Environments linked to all current user's accounts
65
+ def self.all(api)
66
+ self.from_array(api, api.request('/environments')["environments"])
67
+ end
68
+
69
+ # Return a constrained list of environments given a set of constraints like:
70
+ #
71
+ # * app_name
72
+ # * account_name
73
+ # * environment_name
74
+ # * remotes: An array of git remote URIs
75
+ #
76
+ def self.resolve(api, constraints)
77
+ clean_constraints = constraints.reject { |k,v| v.nil? }
78
+ params = {'constraints' => clean_constraints}
79
+ response = api.request("/environments/resolve", :method => :get, :params => params)['resolver']
80
+ matches = from_array(api, response['matches'])
81
+ ResolverResult.new(api, matches, response['errors'], response['suggestions'])
82
+ end
83
+
84
+ # Usage
85
+ # Environment.create(api, {
86
+ # app: app, # requires: app.id
87
+ # name: 'myapp_production',
88
+ # region: 'us-west-1', # default: us-east-1
89
+ # app_server_stack_name: 'nginx_thin', # default: nginx_passenger3
90
+ # framework_env: 'staging' # default: production
91
+ # cluster_configuration: {
92
+ # configuration: 'single' # default: single, cluster, custom
93
+ # }
94
+ # })
95
+ #
96
+ # NOTE: Syntax above is for Ruby 1.9. In Ruby 1.8, keys must all be strings.
97
+ #
98
+ # TODO - allow any attribute to be sent through that the API might allow; e.g. region, ruby_version, stack_label
99
+ def self.create(api, attrs={})
100
+ app = attrs.delete("app")
101
+ cluster_configuration = attrs.delete('cluster_configuration')
102
+ raise EY::CloudClient::AttributeRequiredError.new("app", EY::CloudClient::App) unless app
103
+ raise EY::CloudClient::AttributeRequiredError.new("name") unless attrs["name"]
104
+
105
+ params = {"environment" => attrs.dup}
106
+ unpack_cluster_configuration(params, cluster_configuration)
107
+ response = api.request("/apps/#{app.id}/environments", :method => :post, :params => params)
108
+ self.from_hash(api, response['environment'])
109
+ end
110
+
111
+ def account_name
112
+ account && account.name
113
+ end
114
+
115
+ def ssh_username=(user)
116
+ self.username = user
117
+ end
118
+
119
+ def logs
120
+ Log.from_array(api, api.request("/environments/#{id}/logs", :method => :get)["logs"])
121
+ end
122
+
123
+ def deploy_to_instances
124
+ instances.select { |inst| inst.has_app_code? }
125
+ end
126
+
127
+ def bridge
128
+ @bridge ||= instances.detect { |inst| inst.bridge? }
129
+ end
130
+
131
+ def bridge!
132
+ if bridge.nil?
133
+ raise NoBridgeError.new(name)
134
+ elsif !ignore_bad_bridge && bridge.status != "running"
135
+ raise BadBridgeStatusError.new(bridge.status, EY::CloudClient.endpoint)
136
+ end
137
+ bridge
138
+ end
139
+
140
+ def rebuild
141
+ api.request("/environments/#{id}/update_instances", :method => :put)
142
+ end
143
+
144
+ def run_custom_recipes
145
+ api.request("/environments/#{id}/run_custom_recipes", :method => :put)
146
+ end
147
+
148
+ def download_recipes
149
+ if File.exist?('cookbooks')
150
+ raise EY::CloudClient::Error, "Could not download, cookbooks already exists"
151
+ end
152
+
153
+ require 'tempfile'
154
+ tmp = Tempfile.new("recipes")
155
+ tmp.write(api.request("/environments/#{id}/recipes"))
156
+ tmp.flush
157
+ tmp.close
158
+
159
+ cmd = "tar xzf '#{tmp.path}' cookbooks"
160
+
161
+ unless system(cmd)
162
+ raise EY::CloudClient::Error, "Could not unarchive recipes.\nCommand `#{cmd}` exited with an error."
163
+ end
164
+ end
165
+
166
+ def upload_recipes_at_path(recipes_path)
167
+ recipes_path = Pathname.new(recipes_path)
168
+ if recipes_path.exist?
169
+ upload_recipes recipes_path.open('rb')
170
+ else
171
+ raise EY::CloudClient::Error, "Recipes file not found: #{recipes_path}"
172
+ end
173
+ end
174
+
175
+ def tar_and_upload_recipes_in_cookbooks_dir
176
+ require 'tempfile'
177
+ unless File.exist?("cookbooks")
178
+ raise EY::CloudClient::Error, "Could not find chef recipes. Please run from the root of your recipes repo."
179
+ end
180
+
181
+ recipes_file = Tempfile.new("recipes")
182
+ cmd = "tar czf '#{recipes_file.path}' cookbooks/"
183
+
184
+ unless system(cmd)
185
+ raise EY::CloudClient::Error, "Could not archive recipes.\nCommand `#{cmd}` exited with an error."
186
+ end
187
+
188
+ upload_recipes(recipes_file)
189
+ end
190
+
191
+ def upload_recipes(file_to_upload)
192
+ api.request("/environments/#{id}/recipes", {
193
+ :method => :post,
194
+ :params => {:file => file_to_upload}
195
+ })
196
+ end
197
+
198
+ def shorten_name_for(app)
199
+ name.gsub(/^#{Regexp.quote(app.name)}_/, '')
200
+ end
201
+
202
+ private
203
+
204
+ def request_instances
205
+ instances_attrs = api.request("/environments/#{id}/instances")["instances"]
206
+ load_instances(instances_attrs)
207
+ end
208
+
209
+ def load_instances(instances_attrs)
210
+ Instance.from_array(api, instances_attrs, 'environment' => self)
211
+ end
212
+
213
+ def no_migrate?(deploy_options)
214
+ deploy_options.key?('migrate') && deploy_options['migrate'] == false
215
+ end
216
+
217
+ # attrs["cluster_configuration"]["cluster"] can be 'single', 'cluster', or 'custom'
218
+ # attrs["cluster_configuration"]["ip"] can be
219
+ # * 'host' (amazon public hostname)
220
+ # * 'new' (Elastic IP assigned, default)
221
+ # * or an IP id
222
+ # if 'custom' cluster, then...
223
+ def self.unpack_cluster_configuration(attrs, configuration)
224
+ if configuration
225
+ attrs["cluster_configuration"] = configuration
226
+ attrs["cluster_configuration"]["configuration"] ||= 'single'
227
+ attrs["cluster_configuration"]["ip_id"] = configuration.delete("ip") || 'new' # amazon public hostname; alternate is 'new' for Elastic IP
228
+
229
+ # if cluster_type == 'custom'
230
+ # attrs['cluster_configuration'][app_server_count] = options[:app_instances] || 2
231
+ # attrs['cluster_configuration'][db_slave_count] = options[:db_instances] || 0
232
+ # attrs['cluster_configuration'][instance_size] = options[:app_size] if options[:app_size]
233
+ # attrs['cluster_configuration'][db_instance_size] = options[:db_size] if options[:db_size]
234
+ # end
235
+ # at
236
+ end
237
+ end
238
+ end
239
+ end
240
+ end
@@ -0,0 +1,15 @@
1
+ require 'engineyard-cloud-client/errors'
2
+
3
+ module EY
4
+ class CloudClient
5
+ class Instance < ApiStruct.new(:id, :role, :name, :status, :amazon_id, :public_hostname, :environment, :bridge)
6
+ alias hostname public_hostname
7
+ alias bridge? bridge
8
+
9
+ def has_app_code?
10
+ !["db_master", "db_slave"].include?(role.to_s)
11
+ end
12
+
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,32 @@
1
+ require 'engineyard-cloud-client/models/api_struct'
2
+
3
+ module EY
4
+ class CloudClient
5
+ class Keypair < ApiStruct.new(:id, :name, :public_key)
6
+
7
+ def self.all(api)
8
+ self.from_array(api, api.request('/keypairs')["keypairs"])
9
+ end
10
+
11
+ # Create a Keypair with your SSH public key so that you can access your Instances
12
+ # via SSH
13
+ # If successful, returns new Keypair and EY Cloud will have registered your public key
14
+ # If unsuccessful, raises +EY::CloudClient::RequestFailed+
15
+ #
16
+ # Usage
17
+ # Keypair.create(api,
18
+ # name: "laptop",
19
+ # public_key: "ssh-rsa OTHERKEYPAIR"
20
+ # )
21
+ #
22
+ # NOTE: Syntax above is for Ruby 1.9. In Ruby 1.8, keys must all be strings.
23
+ def self.create(api, attrs = {})
24
+ params = attrs.dup # no default fields
25
+ raise EY::CloudClient::AttributeRequiredError.new("name") unless params["name"]
26
+ raise EY::CloudClient::AttributeRequiredError.new("public_key") unless params["public_key"]
27
+ response = api.request("/keypairs", :method => :post, :params => {"keypair" => params})
28
+ self.from_hash(api, response['keypair'])
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,11 @@
1
+ require 'engineyard-cloud-client/models'
2
+
3
+ module EY
4
+ class CloudClient
5
+ class Log < ApiStruct.new(:id, :role, :main, :custom)
6
+ def instance_name
7
+ "#{role} #{id}"
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,11 @@
1
+ require 'engineyard-cloud-client/models'
2
+
3
+ module EY
4
+ class CloudClient
5
+ class User < ApiStruct.new(:id, :name, :email)
6
+ def accounts
7
+ EY::CloudClient::Account.all(api)
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,19 @@
1
+ module EY
2
+ class CloudClient
3
+ class ResolverResult
4
+ attr_reader :api, :matches, :errors, :suggestions
5
+
6
+ def initialize(api, matches, errors, suggestions)
7
+ @api, @matches, @errors, @suggestions = api, matches, errors, suggestions
8
+ end
9
+
10
+ def one_match?() matches.size == 1 end
11
+ def no_matches?() matches.empty? end
12
+ def many_matches?() matches.size > 1 end
13
+
14
+ def one_match(&block) one_match? && block && block.call(matches.first) end
15
+ def no_matches(&block) no_matches? && block && block.call(errors, suggestions) end
16
+ def many_matches(&block) many_matches? && block && block.call(matches) end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,11 @@
1
+ require 'rest_client'
2
+
3
+ module RestClient
4
+ module AbstractResponse
5
+ private
6
+
7
+ def parse_cookie(cookie_content)
8
+ {}
9
+ end
10
+ end
11
+ end