engineyard-cloud-client 0.1.2
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +19 -0
- data/README.rdoc +7 -0
- data/lib/engineyard-cloud-client.rb +149 -0
- data/lib/engineyard-cloud-client/errors.rb +38 -0
- data/lib/engineyard-cloud-client/model_registry.rb +21 -0
- data/lib/engineyard-cloud-client/models.rb +14 -0
- data/lib/engineyard-cloud-client/models/account.rb +38 -0
- data/lib/engineyard-cloud-client/models/api_struct.rb +50 -0
- data/lib/engineyard-cloud-client/models/app.rb +77 -0
- data/lib/engineyard-cloud-client/models/app_environment.rb +85 -0
- data/lib/engineyard-cloud-client/models/deployment.rb +105 -0
- data/lib/engineyard-cloud-client/models/environment.rb +240 -0
- data/lib/engineyard-cloud-client/models/instance.rb +15 -0
- data/lib/engineyard-cloud-client/models/keypair.rb +32 -0
- data/lib/engineyard-cloud-client/models/log.rb +11 -0
- data/lib/engineyard-cloud-client/models/user.rb +11 -0
- data/lib/engineyard-cloud-client/resolver_result.rb +19 -0
- data/lib/engineyard-cloud-client/rest_client_ext.rb +11 -0
- data/lib/engineyard-cloud-client/ruby_ext.rb +9 -0
- data/lib/engineyard-cloud-client/test.rb +31 -0
- data/lib/engineyard-cloud-client/test/fake_awsm.rb +22 -0
- data/lib/engineyard-cloud-client/test/fake_awsm/config.ru +207 -0
- data/lib/engineyard-cloud-client/test/fake_awsm/models.rb +9 -0
- data/lib/engineyard-cloud-client/test/fake_awsm/models/account.rb +13 -0
- data/lib/engineyard-cloud-client/test/fake_awsm/models/app.rb +24 -0
- data/lib/engineyard-cloud-client/test/fake_awsm/models/app_environment.rb +19 -0
- data/lib/engineyard-cloud-client/test/fake_awsm/models/deployments.rb +15 -0
- data/lib/engineyard-cloud-client/test/fake_awsm/models/environment.rb +25 -0
- data/lib/engineyard-cloud-client/test/fake_awsm/models/instance.rb +23 -0
- data/lib/engineyard-cloud-client/test/fake_awsm/models/user.rb +15 -0
- data/lib/engineyard-cloud-client/test/fake_awsm/scenarios.rb +325 -0
- data/lib/engineyard-cloud-client/test/fake_awsm/views/accounts.rabl +2 -0
- data/lib/engineyard-cloud-client/test/fake_awsm/views/apps.rabl +10 -0
- data/lib/engineyard-cloud-client/test/fake_awsm/views/base_app_environment.rabl +13 -0
- data/lib/engineyard-cloud-client/test/fake_awsm/views/base_environment.rabl +4 -0
- data/lib/engineyard-cloud-client/test/fake_awsm/views/environments.rabl +11 -0
- data/lib/engineyard-cloud-client/test/fake_awsm/views/instances.rabl +2 -0
- data/lib/engineyard-cloud-client/test/fake_awsm/views/resolve_app_environments.rabl +7 -0
- data/lib/engineyard-cloud-client/test/fake_awsm/views/resolve_environments.rabl +7 -0
- data/lib/engineyard-cloud-client/test/fake_awsm/views/user.rabl +2 -0
- data/lib/engineyard-cloud-client/test/scenario.rb +43 -0
- data/lib/engineyard-cloud-client/test/ui.rb +33 -0
- data/lib/engineyard-cloud-client/version.rb +7 -0
- data/spec/engineyard-cloud-client/api_spec.rb +59 -0
- data/spec/engineyard-cloud-client/integration/account_spec.rb +18 -0
- data/spec/engineyard-cloud-client/integration/app_environment_spec.rb +38 -0
- data/spec/engineyard-cloud-client/integration/app_spec.rb +20 -0
- data/spec/engineyard-cloud-client/integration/environment_spec.rb +57 -0
- data/spec/engineyard-cloud-client/integration/user_spec.rb +18 -0
- data/spec/engineyard-cloud-client/models/api_struct_spec.rb +41 -0
- data/spec/engineyard-cloud-client/models/app_spec.rb +64 -0
- data/spec/engineyard-cloud-client/models/environment_spec.rb +300 -0
- data/spec/engineyard-cloud-client/models/instance_spec.rb +44 -0
- data/spec/engineyard-cloud-client/models/keypair_spec.rb +58 -0
- data/spec/spec_helper.rb +50 -0
- data/spec/support/helpers.rb +16 -0
- data/spec/support/matchers.rb +2 -0
- 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,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
|