engineyard 0.3.1 → 0.3.2
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/engineyard.rb +2 -2
- data/lib/engineyard/api.rb +19 -0
- data/lib/engineyard/cli.rb +24 -12
- data/lib/engineyard/cli/action/deploy.rb +112 -0
- data/lib/engineyard/cli/api.rb +14 -1
- data/lib/engineyard/error.rb +27 -0
- data/lib/engineyard/model.rb +9 -0
- data/lib/engineyard/{account → model}/api_struct.rb +8 -3
- data/lib/engineyard/{account → model}/app.rb +3 -3
- data/lib/engineyard/{account → model}/environment.rb +14 -7
- data/lib/engineyard/model/instance.rb +91 -0
- data/lib/engineyard/{account → model}/log.rb +1 -1
- data/lib/engineyard/repo.rb +3 -3
- data/spec/engineyard/{account → model}/api_struct_spec.rb +11 -7
- data/spec/engineyard/model/environment_spec.rb +45 -0
- data/spec/engineyard/model/instance_spec.rb +70 -0
- data/spec/engineyard/repo_spec.rb +17 -14
- data/spec/ey/deploy_spec.rb +95 -45
- data/spec/ey/list_environments_spec.rb +11 -1
- data/spec/ey/logs_spec.rb +18 -0
- data/spec/ey/rebuild_spec.rb +21 -4
- data/spec/ey/ssh_spec.rb +28 -3
- data/spec/spec_helper.rb +37 -12
- data/spec/support/fake_awsm.ru +139 -4
- data/spec/support/helpers.rb +19 -8
- metadata +31 -20
- data/lib/engineyard/account.rb +0 -63
- data/lib/engineyard/account/app_master.rb +0 -6
- data/lib/engineyard/account/instance.rb +0 -6
- data/lib/engineyard/action/deploy.rb +0 -138
- data/lib/engineyard/action/rebuild.rb +0 -31
- data/lib/engineyard/action/util.rb +0 -19
- data/spec/engineyard/account/environment_spec.rb +0 -20
- data/spec/engineyard/account_spec.rb +0 -18
data/lib/engineyard.rb
CHANGED
@@ -1,12 +1,12 @@
|
|
1
1
|
module EY
|
2
2
|
require 'engineyard/ruby_ext'
|
3
3
|
|
4
|
-
VERSION = "0.3.
|
4
|
+
VERSION = "0.3.2"
|
5
5
|
|
6
|
-
autoload :Account, 'engineyard/account'
|
7
6
|
autoload :API, 'engineyard/api'
|
8
7
|
autoload :Config, 'engineyard/config'
|
9
8
|
autoload :Error, 'engineyard/error'
|
9
|
+
autoload :Model, 'engineyard/model'
|
10
10
|
autoload :Repo, 'engineyard/repo'
|
11
11
|
|
12
12
|
class UI
|
data/lib/engineyard/api.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
require 'engineyard/model'
|
2
|
+
|
1
3
|
module EY
|
2
4
|
class API
|
3
5
|
attr_reader :token
|
@@ -20,9 +22,26 @@ module EY
|
|
20
22
|
self.class.request(url, opts)
|
21
23
|
end
|
22
24
|
|
25
|
+
def environments
|
26
|
+
@environments ||= EY::Model::Environment.from_array(request('/environments')["environments"], :api => self)
|
27
|
+
end
|
28
|
+
|
29
|
+
def apps
|
30
|
+
@apps ||= EY::Model::App.from_array(request('/apps')["apps"], :api => self)
|
31
|
+
end
|
32
|
+
|
33
|
+
def environment_named(name, envs = self.environments)
|
34
|
+
envs.find{|e| e.name == name }
|
35
|
+
end
|
36
|
+
|
37
|
+
def app_for_repo(repo)
|
38
|
+
apps.find{|a| repo.urls.include?(a.repository_uri) }
|
39
|
+
end
|
40
|
+
|
23
41
|
class InvalidCredentials < EY::Error; end
|
24
42
|
class RequestFailed < EY::Error; end
|
25
43
|
|
44
|
+
|
26
45
|
def self.request(path, opts={})
|
27
46
|
require 'rest_client'
|
28
47
|
require 'json'
|
data/lib/engineyard/cli.rb
CHANGED
@@ -24,8 +24,8 @@ module EY
|
|
24
24
|
method_option :install_eysd, :type => :boolean, :aliases => %(-s),
|
25
25
|
:desc => "Force remote install of eysd"
|
26
26
|
def deploy(env_name = nil, branch = nil)
|
27
|
-
require 'engineyard/action/deploy'
|
28
|
-
EY::Action::Deploy.call(env_name, branch, options)
|
27
|
+
require 'engineyard/cli/action/deploy'
|
28
|
+
EY::CLI::Action::Deploy.call(env_name, branch, options)
|
29
29
|
end
|
30
30
|
|
31
31
|
|
@@ -47,15 +47,27 @@ module EY
|
|
47
47
|
|
48
48
|
desc "rebuild [ENV]", "Rebuild environment (ensure configuration is up-to-date)"
|
49
49
|
def rebuild(name = nil)
|
50
|
-
|
51
|
-
|
50
|
+
env = if name
|
51
|
+
env = api.environment_named(name) or raise NoEnvironmentError.new(name)
|
52
|
+
end
|
53
|
+
|
54
|
+
unless env
|
55
|
+
repo = Repo.new
|
56
|
+
app = api.app_for_repo(repo) or raise NoAppError.new(repo)
|
57
|
+
env = app.one_and_only_environment or raise EnvironmentError, "Unable to determine a single environment for the current application (found #{app.environments.size} environments)"
|
58
|
+
end
|
59
|
+
|
60
|
+
EY.ui.debug("Rebuilding #{env.name}")
|
61
|
+
env.rebuild
|
52
62
|
end
|
53
63
|
|
54
64
|
desc "ssh ENV", "Open an ssh session to the environment's application server"
|
55
65
|
def ssh(name)
|
56
|
-
env =
|
57
|
-
if env
|
66
|
+
env = api.environment_named(name)
|
67
|
+
if env && env.app_master
|
58
68
|
Kernel.exec "ssh", "#{env.username}@#{env.app_master.public_hostname}", *ARGV[2..-1]
|
69
|
+
elsif env
|
70
|
+
raise NoAppMaster.new(env.name)
|
59
71
|
else
|
60
72
|
EY.ui.warn %|Could not find an environment named "#{name}"|
|
61
73
|
end
|
@@ -80,7 +92,7 @@ module EY
|
|
80
92
|
|
81
93
|
desc "upload_recipes ENV", "Upload custom chef recipes from the current directory to ENV"
|
82
94
|
def upload_recipes(name)
|
83
|
-
if
|
95
|
+
if env_named(name).upload_recipes
|
84
96
|
EY.ui.say "Recipes uploaded successfully"
|
85
97
|
else
|
86
98
|
EY.ui.error "Recipes upload failed"
|
@@ -94,8 +106,8 @@ module EY
|
|
94
106
|
map ["-v", "--version"] => :version
|
95
107
|
|
96
108
|
private
|
97
|
-
def
|
98
|
-
@
|
109
|
+
def api
|
110
|
+
@api ||= EY::CLI::API.new
|
99
111
|
end
|
100
112
|
|
101
113
|
def repo
|
@@ -103,7 +115,7 @@ module EY
|
|
103
115
|
end
|
104
116
|
|
105
117
|
def env_named(env_name)
|
106
|
-
env =
|
118
|
+
env = api.environment_named(env_name)
|
107
119
|
|
108
120
|
if env.nil?
|
109
121
|
raise EnvironmentError, "Environment '#{env_name}' can't be found\n" +
|
@@ -114,10 +126,10 @@ module EY
|
|
114
126
|
end
|
115
127
|
|
116
128
|
def app_and_envs(all_envs = false)
|
117
|
-
app =
|
129
|
+
app = api.app_for_repo(repo)
|
118
130
|
|
119
131
|
if all_envs || !app
|
120
|
-
envs =
|
132
|
+
envs = api.environments
|
121
133
|
EY.ui.warn(NoAppError.new(repo).message) unless app || all_envs
|
122
134
|
[nil, envs]
|
123
135
|
else
|
@@ -0,0 +1,112 @@
|
|
1
|
+
module EY
|
2
|
+
class CLI
|
3
|
+
module Action
|
4
|
+
class Deploy
|
5
|
+
|
6
|
+
EYSD_VERSION = "~>0.3.0"
|
7
|
+
|
8
|
+
def self.call(env_name, branch, options)
|
9
|
+
env_name ||= EY.config.default_environment
|
10
|
+
|
11
|
+
app = fetch_app
|
12
|
+
env = fetch_environment(env_name, app)
|
13
|
+
branch = fetch_branch(env.name, branch, options[:force])
|
14
|
+
|
15
|
+
running = env.app_master && env.app_master.status == "running"
|
16
|
+
raise EnvironmentError, "No running instances for environment #{env.name}\nStart one at #{EY.config.endpoint}" unless running
|
17
|
+
|
18
|
+
master = env.app_master
|
19
|
+
|
20
|
+
EY.ui.info "Connecting to the server..."
|
21
|
+
ensure_eysd_present(master, options[:install_eysd])
|
22
|
+
|
23
|
+
EY.ui.info "Running deploy for '#{env.name}' on server..."
|
24
|
+
deployed = master.deploy!(app, branch, options[:migrate], env.config)
|
25
|
+
|
26
|
+
if deployed
|
27
|
+
EY.ui.info "Deploy complete"
|
28
|
+
else
|
29
|
+
raise EY::Error, "Deploy failed"
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def self.api
|
36
|
+
@api ||= EY::CLI::API.new
|
37
|
+
end
|
38
|
+
|
39
|
+
def self.repo
|
40
|
+
@repo ||= EY::Repo.new
|
41
|
+
end
|
42
|
+
|
43
|
+
def self.fetch_app
|
44
|
+
app = api.app_for_repo(repo)
|
45
|
+
raise NoAppError.new(repo) unless app
|
46
|
+
app
|
47
|
+
end
|
48
|
+
|
49
|
+
def self.fetch_environment(env_name, app)
|
50
|
+
# if the name's not specified and there's not exactly one
|
51
|
+
# environment, we can't figure out which environment to deploy
|
52
|
+
raise DeployArgumentError if !env_name && app.environments.size != 1
|
53
|
+
|
54
|
+
env = if env_name
|
55
|
+
api.environment_named(env_name, app.environments)
|
56
|
+
else
|
57
|
+
app.environments.first
|
58
|
+
end
|
59
|
+
|
60
|
+
# the environment exists, but doesn't have this app
|
61
|
+
if !env && api.environment_named(env_name)
|
62
|
+
raise EnvironmentError, "Environment '#{env_name}' doesn't run this application\nYou can add it at #{EY.config.endpoint}"
|
63
|
+
end
|
64
|
+
|
65
|
+
if !env
|
66
|
+
raise NoEnvironmentError.new(env_name)
|
67
|
+
end
|
68
|
+
|
69
|
+
env
|
70
|
+
end
|
71
|
+
|
72
|
+
def self.fetch_branch(env_name, user_specified_branch, force)
|
73
|
+
default_branch = EY.config.default_branch(env_name)
|
74
|
+
|
75
|
+
branch = if user_specified_branch
|
76
|
+
if default_branch && (user_specified_branch != default_branch) && !force
|
77
|
+
raise BranchMismatch.new(default_branch, user_specified_branch)
|
78
|
+
end
|
79
|
+
user_specified_branch
|
80
|
+
else
|
81
|
+
default_branch || repo.current_branch
|
82
|
+
end
|
83
|
+
|
84
|
+
raise DeployArgumentError unless branch
|
85
|
+
branch
|
86
|
+
end
|
87
|
+
|
88
|
+
def self.ensure_eysd_present(instance, install_eysd)
|
89
|
+
eysd_status = instance.ey_deploy_check
|
90
|
+
case eysd_status
|
91
|
+
when :ssh_failed
|
92
|
+
raise EnvironmentError, "SSH connection to #{instance.hostname} failed"
|
93
|
+
when :eysd_missing
|
94
|
+
EY.ui.warn "Instance does not have server-side component installed"
|
95
|
+
EY.ui.info "Installing server-side component..."
|
96
|
+
instance.install_ey_deploy!
|
97
|
+
when :too_new
|
98
|
+
raise EnvironmentError, "server-side component too new; please upgrade your copy of the engineyard gem."
|
99
|
+
when :too_old
|
100
|
+
EY.ui.info "Upgrading server-side component..."
|
101
|
+
instance.upgrade_ey_deploy!
|
102
|
+
when :ok
|
103
|
+
# no action needed
|
104
|
+
else
|
105
|
+
raise EY::Error, "Internal error: Unexpected status from Instance#ey_deploy_check; got #{eysd_status.inspect}"
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
data/lib/engineyard/cli/api.rb
CHANGED
@@ -23,6 +23,10 @@ module EY
|
|
23
23
|
@token = self.class.fetch_token
|
24
24
|
end
|
25
25
|
|
26
|
+
def environment_named(env_name, envs = self.environments)
|
27
|
+
super || find_environment_by_unambiguous_substring(env_name, envs)
|
28
|
+
end
|
29
|
+
|
26
30
|
def self.fetch_token
|
27
31
|
EY.ui.warn("The engineyard gem is prerelease software. Please do not use")
|
28
32
|
EY.ui.warn("this tool to deploy to mission-critical environments, yet.")
|
@@ -37,6 +41,15 @@ module EY
|
|
37
41
|
end
|
38
42
|
end
|
39
43
|
|
44
|
+
private
|
45
|
+
def find_environment_by_unambiguous_substring(env_name, envs)
|
46
|
+
candidates = envs.find_all{|e| e.name[env_name] }
|
47
|
+
if candidates.size > 1
|
48
|
+
raise AmbiguousEnvironmentName.new(env_name, candidates.map {|e| e.name})
|
49
|
+
end
|
50
|
+
candidates.first
|
51
|
+
end
|
52
|
+
|
40
53
|
end
|
41
54
|
end
|
42
|
-
end
|
55
|
+
end
|
data/lib/engineyard/error.rb
CHANGED
@@ -1,6 +1,12 @@
|
|
1
1
|
module EY
|
2
2
|
class Error < RuntimeError; end
|
3
3
|
|
4
|
+
class NoRemotesError < EY::Error
|
5
|
+
def initialize(path)
|
6
|
+
super "fatal: No git remotes found in #{path}"
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
4
10
|
class NoAppError < Error
|
5
11
|
def initialize(repo)
|
6
12
|
@repo = repo
|
@@ -14,9 +20,30 @@ module EY
|
|
14
20
|
end
|
15
21
|
end
|
16
22
|
|
23
|
+
class NoAppMaster < EY::Error
|
24
|
+
def initialize(env_name)
|
25
|
+
@env_name = env_name
|
26
|
+
end
|
27
|
+
|
28
|
+
def message
|
29
|
+
"The environment '#{@env_name}' does not have a master instance."
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
17
33
|
class EnvironmentError < EY::Error
|
18
34
|
end
|
19
35
|
|
36
|
+
class AmbiguousEnvironmentName < EY::Error
|
37
|
+
def initialize(name, matches)
|
38
|
+
@name, @matches = name, matches
|
39
|
+
end
|
40
|
+
|
41
|
+
def message
|
42
|
+
pretty_names = @matches.map {|x| "'#{x}'"}.join(', ')
|
43
|
+
"The name '#{@name}' is ambiguous; it matches all of the following environment names: #{pretty_names}.\nPlease use a longer, unambiguous substring or the entire environment name."
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
20
47
|
class NoEnvironmentError < EY::Error
|
21
48
|
def initialize(env_name=nil)
|
22
49
|
@env_name = env_name
|
@@ -0,0 +1,9 @@
|
|
1
|
+
module EY
|
2
|
+
module Model
|
3
|
+
autoload :ApiStruct, 'engineyard/model/api_struct'
|
4
|
+
autoload :App, 'engineyard/model/app'
|
5
|
+
autoload :Environment, 'engineyard/model/environment'
|
6
|
+
autoload :Log, 'engineyard/model/log'
|
7
|
+
autoload :Instance, 'engineyard/model/instance'
|
8
|
+
end
|
9
|
+
end
|
@@ -1,13 +1,13 @@
|
|
1
1
|
module EY
|
2
|
-
|
2
|
+
module Model
|
3
3
|
class ApiStruct < Struct
|
4
4
|
def self.new(*args, &block)
|
5
5
|
super(*args) do |*block_args|
|
6
6
|
block.call(*block_args) if block
|
7
7
|
|
8
|
-
def self.from_array(array,
|
8
|
+
def self.from_array(array, common_values = {})
|
9
9
|
array.map do |values|
|
10
|
-
from_hash(values.merge(
|
10
|
+
from_hash(values.merge(common_values))
|
11
11
|
end if array
|
12
12
|
end
|
13
13
|
|
@@ -20,6 +20,11 @@ module EY
|
|
20
20
|
|
21
21
|
end
|
22
22
|
end
|
23
|
+
|
24
|
+
def api_get(uri, options = {})
|
25
|
+
api.request(uri, options.merge(:method => :get))
|
26
|
+
end
|
27
|
+
|
23
28
|
end
|
24
29
|
end
|
25
30
|
end
|
@@ -1,10 +1,10 @@
|
|
1
1
|
module EY
|
2
|
-
|
3
|
-
class App < ApiStruct.new(:name, :repository_uri, :environments, :
|
2
|
+
module Model
|
3
|
+
class App < ApiStruct.new(:name, :repository_uri, :environments, :api)
|
4
4
|
|
5
5
|
def self.from_hash(hash)
|
6
6
|
super.tap do |app|
|
7
|
-
app.environments = Environment.from_array(app.environments, app.
|
7
|
+
app.environments = Environment.from_array(app.environments, :api => app.api)
|
8
8
|
end
|
9
9
|
end
|
10
10
|
|
@@ -1,24 +1,31 @@
|
|
1
1
|
module EY
|
2
|
-
|
3
|
-
class Environment < ApiStruct.new(:id, :name, :instances_count, :apps, :app_master, :username, :
|
2
|
+
module Model
|
3
|
+
class Environment < ApiStruct.new(:id, :name, :instances_count, :apps, :app_master, :username, :api)
|
4
4
|
def self.from_hash(hash)
|
5
5
|
super.tap do |env|
|
6
6
|
env.username = hash['ssh_username']
|
7
|
-
env.apps = App.from_array(env.apps, env.
|
8
|
-
env.app_master =
|
7
|
+
env.apps = App.from_array(env.apps, :api => env.api)
|
8
|
+
env.app_master = Instance.from_hash(env.app_master.merge(:environment => env)) if env.app_master
|
9
9
|
end
|
10
10
|
end
|
11
11
|
|
12
12
|
def logs
|
13
|
-
|
13
|
+
Log.from_array(api_get("/environments/#{id}/logs")["logs"])
|
14
14
|
end
|
15
15
|
|
16
16
|
def instances
|
17
|
-
|
17
|
+
Instance.from_array(api_get("/environments/#{id}/instances")["instances"], :environment => self)
|
18
18
|
end
|
19
19
|
|
20
20
|
def rebuild
|
21
|
-
|
21
|
+
api.request("/environments/#{id}/rebuild", :method => :put)
|
22
|
+
end
|
23
|
+
|
24
|
+
def upload_recipes(file_to_upload = recipe_file)
|
25
|
+
api.request("/environments/#{id}/recipes",
|
26
|
+
:method => :post,
|
27
|
+
:params => {:file => file_to_upload}
|
28
|
+
)
|
22
29
|
end
|
23
30
|
|
24
31
|
def recipe_file
|
@@ -0,0 +1,91 @@
|
|
1
|
+
require 'escape'
|
2
|
+
|
3
|
+
module EY
|
4
|
+
module Model
|
5
|
+
class Instance < ApiStruct.new(:id, :role, :status, :amazon_id, :public_hostname, :environment)
|
6
|
+
EYSD_VERSION = "~>0.3.2"
|
7
|
+
CHECK_SCRIPT = <<-SCRIPT
|
8
|
+
require "rubygems"
|
9
|
+
requirement = Gem::Requirement.new("#{EYSD_VERSION}")
|
10
|
+
required_version = requirement.requirements.last.last # thanks thanks rubygems rubygems
|
11
|
+
|
12
|
+
# this will be a ["name-version", Gem::Specification] two-element array if present, nil otherwise
|
13
|
+
ey_deploy_geminfo = Gem.source_index.find{ |(name,_)| name =~ /^ey-deploy-\\\d/ }
|
14
|
+
exit(104) unless ey_deploy_geminfo
|
15
|
+
|
16
|
+
current_version = ey_deploy_geminfo.last.version
|
17
|
+
exit(0) if requirement.satisfied_by?(current_version)
|
18
|
+
exit(70) if required_version > current_version
|
19
|
+
exit(17) # required_version < current_version
|
20
|
+
SCRIPT
|
21
|
+
EXIT_STATUS = Hash.new { |h,k| raise EY::Error, "ey-deploy version checker exited with unknown status code #{k}" }
|
22
|
+
EXIT_STATUS.merge!({
|
23
|
+
255 => :ssh_failed,
|
24
|
+
104 => :eysd_missing,
|
25
|
+
70 => :too_old,
|
26
|
+
17 => :too_new,
|
27
|
+
0 => :ok,
|
28
|
+
})
|
29
|
+
|
30
|
+
alias :hostname :public_hostname
|
31
|
+
|
32
|
+
def deploy!(app, ref, migration_command=nil, extra_configuration=nil)
|
33
|
+
deploy_cmd = [eysd_path, 'deploy', '--app', app.name, '--branch', ref]
|
34
|
+
|
35
|
+
if extra_configuration
|
36
|
+
deploy_cmd << '--config' << extra_configuration.to_json
|
37
|
+
end
|
38
|
+
|
39
|
+
if migration_command
|
40
|
+
deploy_cmd << "--migrate" << migration_command
|
41
|
+
end
|
42
|
+
|
43
|
+
ssh Escape.shell_command(deploy_cmd)
|
44
|
+
end
|
45
|
+
|
46
|
+
def ey_deploy_check
|
47
|
+
require 'base64'
|
48
|
+
encoded_script = Base64.encode64(CHECK_SCRIPT).gsub(/\n/, '')
|
49
|
+
ssh "#{ruby_path} -r base64 -e \"eval Base64.decode64(ARGV[0])\" #{encoded_script}", false
|
50
|
+
EXIT_STATUS[$?.exitstatus]
|
51
|
+
end
|
52
|
+
|
53
|
+
def install_ey_deploy!
|
54
|
+
ssh(Escape.shell_command(['sudo', gem_path, 'install', 'ey-deploy', '-v', EYSD_VERSION]))
|
55
|
+
end
|
56
|
+
|
57
|
+
def upgrade_ey_deploy!
|
58
|
+
ssh "sudo #{gem_path} uninstall -a -x ey-deploy"
|
59
|
+
install_ey_deploy!
|
60
|
+
end
|
61
|
+
|
62
|
+
private
|
63
|
+
|
64
|
+
def ssh(remote_command, output = true)
|
65
|
+
user = environment.username
|
66
|
+
|
67
|
+
cmd = Escape.shell_command(%w[ssh -o StrictHostKeyChecking=no -q] << "#{user}@#{hostname}" << remote_command)
|
68
|
+
cmd << " > /dev/null" unless output
|
69
|
+
output ? puts(cmd) : EY.ui.debug(cmd)
|
70
|
+
unless ENV["NO_SSH"]
|
71
|
+
system cmd
|
72
|
+
else
|
73
|
+
true
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def eysd_path
|
78
|
+
"/usr/local/ey_resin/ruby/bin/eysd"
|
79
|
+
end
|
80
|
+
|
81
|
+
def gem_path
|
82
|
+
"/usr/local/ey_resin/ruby/bin/gem"
|
83
|
+
end
|
84
|
+
|
85
|
+
def ruby_path
|
86
|
+
"/usr/local/ey_resin/ruby/bin/ruby"
|
87
|
+
end
|
88
|
+
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|