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