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