engineyard 0.4.0 → 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/engineyard.rb +1 -1
- data/lib/engineyard/api.rb +12 -1
- data/lib/engineyard/cli.rb +116 -48
- data/lib/engineyard/cli/recipes.rb +37 -7
- data/lib/engineyard/cli/ui.rb +8 -0
- data/lib/engineyard/cli/web.rb +41 -0
- data/lib/engineyard/error.rb +8 -1
- data/lib/engineyard/model/environment.rb +32 -6
- data/lib/engineyard/model/instance.rb +39 -20
- data/lib/engineyard/repo.rb +7 -3
- data/lib/engineyard/thor.rb +55 -2
- data/spec/engineyard/api_spec.rb +2 -2
- data/spec/engineyard/cli/api_spec.rb +1 -1
- data/spec/engineyard/model/environment_spec.rb +8 -6
- data/spec/ey/deploy_spec.rb +46 -81
- data/spec/ey/ey_spec.rb +2 -2
- data/spec/ey/list_environments_spec.rb +1 -1
- data/spec/ey/logs_spec.rb +11 -21
- data/spec/ey/rebuild_spec.rb +8 -33
- data/spec/ey/recipes/apply_spec.rb +8 -33
- data/spec/ey/recipes/download_spec.rb +29 -0
- data/spec/ey/recipes/upload_spec.rb +8 -25
- data/spec/ey/rollback_spec.rb +10 -40
- data/spec/ey/ssh_spec.rb +17 -34
- data/spec/ey/web/disable_spec.rb +18 -0
- data/spec/ey/web/enable_spec.rb +18 -0
- data/spec/spec_helper.rb +7 -3
- data/spec/support/fake_awsm.ru +18 -0
- data/spec/support/helpers.rb +13 -0
- data/spec/support/shared_behavior.rb +132 -0
- metadata +8 -3
data/lib/engineyard.rb
CHANGED
data/lib/engineyard/api.rb
CHANGED
@@ -38,6 +38,15 @@ module EY
|
|
38
38
|
app_for_repo(repo) || raise(NoAppError.new(repo))
|
39
39
|
end
|
40
40
|
|
41
|
+
def fetch_app(name)
|
42
|
+
apps.find{|a| a.name == name}
|
43
|
+
end
|
44
|
+
|
45
|
+
def fetch_app!(name)
|
46
|
+
return unless name
|
47
|
+
fetch_app(name) || raise(InvalidAppError.new(name))
|
48
|
+
end
|
49
|
+
|
41
50
|
class InvalidCredentials < EY::Error; end
|
42
51
|
class RequestFailed < EY::Error; end
|
43
52
|
|
@@ -75,7 +84,7 @@ module EY
|
|
75
84
|
|
76
85
|
if resp.body.empty?
|
77
86
|
data = ''
|
78
|
-
|
87
|
+
elsif resp.headers[:content_type] =~ /application\/json/
|
79
88
|
begin
|
80
89
|
data = JSON.parse(resp.body)
|
81
90
|
EY.ui.debug("Response", data)
|
@@ -83,6 +92,8 @@ module EY
|
|
83
92
|
EY.ui.debug("Raw response", resp.body)
|
84
93
|
raise RequestFailed, "Response was not valid JSON."
|
85
94
|
end
|
95
|
+
else
|
96
|
+
data = resp.body
|
86
97
|
end
|
87
98
|
|
88
99
|
data
|
data/lib/engineyard/cli.rb
CHANGED
@@ -7,44 +7,58 @@ module EY
|
|
7
7
|
autoload :API, 'engineyard/cli/api'
|
8
8
|
autoload :UI, 'engineyard/cli/ui'
|
9
9
|
autoload :Recipes, 'engineyard/cli/recipes'
|
10
|
+
autoload :Web, 'engineyard/cli/web'
|
10
11
|
|
11
12
|
include Thor::Actions
|
12
13
|
|
13
14
|
def self.start(*)
|
15
|
+
Thor::Base.shell = EY::CLI::UI
|
14
16
|
EY.ui = EY::CLI::UI.new
|
15
17
|
super
|
16
18
|
end
|
17
19
|
|
18
|
-
desc "deploy [ENVIRONMENT] [
|
20
|
+
desc "deploy [--environment ENVIRONMENT] [--ref GIT-REF]", <<-DESC
|
21
|
+
Deploy specified branch/tag/sha to specified environment.
|
22
|
+
|
23
|
+
This command must be run with the current directory containing the app to be
|
24
|
+
deployed. If ey.yml specifies a default branch then the ref parameter can be
|
25
|
+
omitted. Furthermore, if a default branch is specified but a different command
|
26
|
+
is supplied the deploy will fail unless --force is used.
|
27
|
+
|
28
|
+
Migrations are run by default with 'rake db:migrate'. A different command can be
|
29
|
+
specified via --migrate "ruby do_migrations.rb". Migrations can also be skipped
|
30
|
+
entirely by using --no-migrate.
|
31
|
+
DESC
|
19
32
|
method_option :force, :type => :boolean, :aliases => %w(-f),
|
20
|
-
:desc => "Force a deploy of the specified branch"
|
33
|
+
:desc => "Force a deploy of the specified branch even if a default is set"
|
21
34
|
method_option :migrate, :type => :string, :aliases => %w(-m),
|
22
35
|
:default => 'rake db:migrate',
|
23
36
|
:desc => "Run migrations via [MIGRATE], defaults to 'rake db:migrate'; use --no-migrate to avoid running migrations"
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
37
|
+
method_option :environment, :type => :string, :aliases => %w(-e),
|
38
|
+
:desc => "Environment in which to deploy this application"
|
39
|
+
method_option :ref, :type => :string, :aliases => %w(-r --branch --tag),
|
40
|
+
:desc => "Git ref to deploy. May be a branch, a tag, or a SHA."
|
41
|
+
method_option :app, :type => :string, :aliases => %w(-a),
|
42
|
+
:desc => "Name of the application to deploy"
|
43
|
+
def deploy
|
44
|
+
app = api.fetch_app!(options[:app]) || api.app_for_repo!(repo)
|
45
|
+
environment = fetch_environment(options[:environment], app)
|
46
|
+
deploy_ref = if options[:app]
|
47
|
+
environment.resolve_branch(options[:ref], options[:force]) ||
|
48
|
+
raise(EY::Error, "When specifying the application, you must also specify the ref to deploy\nUsage: ey deploy --app <app name> --ref <branch|tag|ref>")
|
49
|
+
else
|
50
|
+
environment.resolve_branch(options[:ref], options[:force]) ||
|
51
|
+
repo.current_branch ||
|
52
|
+
raise(DeployArgumentError)
|
53
|
+
end
|
30
54
|
|
31
55
|
EY.ui.info "Connecting to the server..."
|
32
56
|
|
33
|
-
environment
|
34
|
-
case action
|
35
|
-
when :installing
|
36
|
-
EY.ui.warn "Instance does not have server-side component installed"
|
37
|
-
EY.ui.info "Installing server-side component..."
|
38
|
-
when :upgrading
|
39
|
-
EY.ui.info "Upgrading server-side component..."
|
40
|
-
else
|
41
|
-
# nothing slow is happening, so there's nothing to say
|
42
|
-
end
|
43
|
-
end
|
57
|
+
loudly_check_eysd(environment)
|
44
58
|
|
45
59
|
EY.ui.info "Running deploy for '#{environment.name}' on server..."
|
46
60
|
|
47
|
-
if environment.deploy
|
61
|
+
if environment.deploy(app, deploy_ref, options[:migrate])
|
48
62
|
EY.ui.info "Deploy complete"
|
49
63
|
else
|
50
64
|
raise EY::Error, "Deploy failed"
|
@@ -52,11 +66,17 @@ module EY
|
|
52
66
|
|
53
67
|
rescue NoEnvironmentError => e
|
54
68
|
# Give better feedback about why we couldn't find the environment.
|
55
|
-
exists = api.environments.named(
|
56
|
-
raise exists ? EnvironmentUnlinkedError.new(
|
69
|
+
exists = api.environments.named(options[:environment])
|
70
|
+
raise exists ? EnvironmentUnlinkedError.new(options[:environment]) : e
|
57
71
|
end
|
58
72
|
|
59
|
-
desc "environments [--all]",
|
73
|
+
desc "environments [--all]", <<-DESC
|
74
|
+
List environments.
|
75
|
+
|
76
|
+
By default, environments for this app are displayed. If the -all option is
|
77
|
+
used, all environments are displayed instead.
|
78
|
+
DESC
|
79
|
+
|
60
80
|
method_option :all, :type => :boolean, :aliases => %(-a)
|
61
81
|
def environments
|
62
82
|
apps = get_apps(options[:all])
|
@@ -65,33 +85,58 @@ module EY
|
|
65
85
|
end
|
66
86
|
map "envs" => :environments
|
67
87
|
|
68
|
-
desc "rebuild [ENVIRONMENT]",
|
69
|
-
|
70
|
-
|
88
|
+
desc "rebuild [--environment ENVIRONMENT]", <<-DESC
|
89
|
+
Rebuild specified environment.
|
90
|
+
|
91
|
+
Engine Yard's main configuration run occurs on all servers. Mainly used to fix
|
92
|
+
failed configuration of new or existing servers, or to update servers to latest
|
93
|
+
Engine Yard stack (e.g. to apply an Engine Yard supplied security
|
94
|
+
patch).
|
95
|
+
|
96
|
+
Note that uploaded recipes are also run after the main configuration run has
|
97
|
+
successfully completed.
|
98
|
+
DESC
|
99
|
+
|
100
|
+
method_option :environment, :type => :string, :aliases => %w(-e),
|
101
|
+
:desc => "Environment to rebuild"
|
102
|
+
def rebuild
|
103
|
+
env = fetch_environment(options[:environment])
|
71
104
|
EY.ui.debug("Rebuilding #{env.name}")
|
72
105
|
env.rebuild
|
73
106
|
end
|
74
107
|
|
75
|
-
desc "rollback [ENVIRONMENT]",
|
76
|
-
|
77
|
-
app = api.app_for_repo!(repo)
|
78
|
-
env = fetch_environment(name)
|
108
|
+
desc "rollback [--environment ENVIRONMENT]", <<-DESC
|
109
|
+
Rollback to the previous deploy.
|
79
110
|
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
111
|
+
Uses code from previous deploy in the "/data/APP_NAME/releases" directory on
|
112
|
+
remote server(s) to restart application servers.
|
113
|
+
DESC
|
114
|
+
method_option :environment, :type => :string, :aliases => %w(-e),
|
115
|
+
:desc => "Environment in which to roll back the current application"
|
116
|
+
def rollback
|
117
|
+
app = api.app_for_repo!(repo)
|
118
|
+
env = fetch_environment(options[:environment])
|
119
|
+
|
120
|
+
loudly_check_eysd(env)
|
121
|
+
|
122
|
+
EY.ui.info("Rolling back #{env.name}")
|
123
|
+
if env.rollback(app)
|
124
|
+
EY.ui.info "Rollback complete"
|
87
125
|
else
|
88
|
-
raise
|
126
|
+
raise EY::Error, "Rollback failed"
|
89
127
|
end
|
90
128
|
end
|
91
129
|
|
92
|
-
desc "ssh [ENVIRONMENT]",
|
93
|
-
|
94
|
-
|
130
|
+
desc "ssh [--environment ENVIRONMENT]", <<-DESC
|
131
|
+
Open an ssh session.
|
132
|
+
|
133
|
+
If the environment contains just one server, a session to it will be opened. For
|
134
|
+
environments with clusters, a session will be opened to the application master.
|
135
|
+
DESC
|
136
|
+
method_option :environment, :type => :string, :aliases => %w(-e),
|
137
|
+
:desc => "Environment to ssh into"
|
138
|
+
def ssh
|
139
|
+
env = fetch_environment(options[:environment])
|
95
140
|
|
96
141
|
if env.app_master
|
97
142
|
Kernel.exec "ssh", "#{env.username}@#{env.app_master.public_hostname}"
|
@@ -100,31 +145,54 @@ module EY
|
|
100
145
|
end
|
101
146
|
end
|
102
147
|
|
103
|
-
desc "logs [ENVIRONMENT]",
|
104
|
-
|
105
|
-
|
148
|
+
desc "logs [--environment ENVIRONMENT]", <<-DESC
|
149
|
+
Retrieve the latest logs for an environment.
|
150
|
+
|
151
|
+
Displays Engine Yard configuration logs for all servers in the environment. If
|
152
|
+
recipes were uploaded to the environment & run, their logs will also be
|
153
|
+
displayed beneath the main configuration logs.
|
154
|
+
DESC
|
155
|
+
method_option :environment, :type => :string, :aliases => %w(-e),
|
156
|
+
:desc => "Environment with the interesting logs"
|
157
|
+
def logs
|
158
|
+
env = fetch_environment(options[:environment])
|
159
|
+
env.logs.each do |log|
|
106
160
|
EY.ui.info log.instance_name
|
107
161
|
|
108
162
|
if log.main
|
109
|
-
EY.ui.info "Main logs:"
|
163
|
+
EY.ui.info "Main logs for #{env.name}:"
|
110
164
|
EY.ui.say log.main
|
111
165
|
end
|
112
166
|
|
113
167
|
if log.custom
|
114
|
-
EY.ui.info "Custom logs:"
|
168
|
+
EY.ui.info "Custom logs for #{env.name}:"
|
115
169
|
EY.ui.say log.custom
|
116
170
|
end
|
117
171
|
end
|
118
172
|
end
|
119
173
|
|
120
|
-
desc "recipes
|
174
|
+
desc "recipes", "Commands related to chef recipes."
|
121
175
|
subcommand "recipes", EY::CLI::Recipes
|
122
176
|
|
123
|
-
desc "
|
177
|
+
desc "web", "Commands related to maintenance pages."
|
178
|
+
subcommand "web", EY::CLI::Web
|
179
|
+
|
180
|
+
desc "version", "Print version number."
|
124
181
|
def version
|
125
182
|
EY.ui.say %{engineyard version #{EY::VERSION}}
|
126
183
|
end
|
127
184
|
map ["-v", "--version"] => :version
|
128
185
|
|
186
|
+
desc "help [COMMAND]", "Describe all commands or one specific command."
|
187
|
+
def help(*cmds)
|
188
|
+
if cmds.empty?
|
189
|
+
super
|
190
|
+
EY.ui.say "See '#{self.class.send(:banner_base)} help [COMMAND]' for more information on a specific command."
|
191
|
+
elsif klass = EY::Thor.subcommands[cmds.first]
|
192
|
+
klass.new.help(*cmds[1..-1])
|
193
|
+
else
|
194
|
+
super
|
195
|
+
end
|
196
|
+
end
|
129
197
|
end # CLI
|
130
198
|
end # EY
|
@@ -1,22 +1,52 @@
|
|
1
1
|
module EY
|
2
2
|
class CLI
|
3
3
|
class Recipes < EY::Thor
|
4
|
+
desc "recipes apply [ENVIRONMENT]", <<-DESC
|
5
|
+
Run uploaded chef recipes on specified environment.
|
4
6
|
|
5
|
-
|
6
|
-
|
7
|
-
|
7
|
+
This is similar to '#{banner_base} rebuild' except Engine Yard's main
|
8
|
+
configuration step is skipped.
|
9
|
+
DESC
|
10
|
+
|
11
|
+
method_option :environment, :type => :string, :aliases => %w(-e),
|
12
|
+
:desc => "Environment in which to apply recipes"
|
13
|
+
def apply
|
14
|
+
environment = fetch_environment(options[:environment])
|
8
15
|
environment.run_custom_recipes
|
9
16
|
EY.ui.say "Uploaded recipes started for #{environment.name}"
|
10
17
|
end
|
11
18
|
|
12
|
-
desc "recipes upload [
|
13
|
-
|
14
|
-
|
19
|
+
desc "recipes upload [ENVIRONMENT]", <<-DESC
|
20
|
+
Upload custom chef recipes to specified environment.
|
21
|
+
|
22
|
+
The current directory should contain a subdirectory named "cookbooks" to be
|
23
|
+
uploaded.
|
24
|
+
DESC
|
25
|
+
|
26
|
+
method_option :environment, :type => :string, :aliases => %w(-e),
|
27
|
+
:desc => "Environment that will receive the recipes"
|
28
|
+
def upload
|
29
|
+
environment = fetch_environment(options[:environment])
|
15
30
|
environment.upload_recipes
|
16
31
|
EY.ui.say "Recipes uploaded successfully for #{environment.name}"
|
17
32
|
end
|
18
33
|
|
34
|
+
desc "recipes download [--environment ENVIRONMENT]", <<-DESC
|
35
|
+
Download custom chef recipes from ENVIRONMENT into the current directory.
|
36
|
+
|
37
|
+
The recipes will be unpacked into a directory called "cookbooks" in the
|
38
|
+
current directory.
|
39
|
+
|
40
|
+
If the cookbooks directory already exists, an error will be raised.
|
41
|
+
DESC
|
42
|
+
method_option :environment, :type => :string, :aliases => %w(-e),
|
43
|
+
:desc => "Environment for which to download the recipes"
|
44
|
+
def download
|
45
|
+
environment = fetch_environment(options[:environment])
|
46
|
+
environment.download_recipes
|
47
|
+
EY.ui.say "Recipes downloaded successfully for #{environment.name}"
|
48
|
+
end
|
49
|
+
|
19
50
|
end
|
20
51
|
end
|
21
|
-
|
22
52
|
end
|
data/lib/engineyard/cli/ui.rb
CHANGED
@@ -0,0 +1,41 @@
|
|
1
|
+
module EY
|
2
|
+
class CLI
|
3
|
+
class Web < EY::Thor
|
4
|
+
desc "web enable [ENVIRONMENT]", <<-HELP
|
5
|
+
Take down the maintenance page for the current application in the specified environment.
|
6
|
+
HELP
|
7
|
+
method_option :environment, :type => :string, :aliases => %w(-e),
|
8
|
+
:desc => "Environment on which to put up the maintenance page"
|
9
|
+
def enable
|
10
|
+
app = api.app_for_repo!(repo)
|
11
|
+
environment = fetch_environment(options[:environment], app)
|
12
|
+
loudly_check_eysd(environment)
|
13
|
+
EY.ui.info "Taking down maintenance page for #{environment.name}"
|
14
|
+
environment.take_down_maintenance_page(app)
|
15
|
+
end
|
16
|
+
|
17
|
+
desc "web disable [ENVIRONMENT]", <<-HELP
|
18
|
+
Put up the maintenance page for the current application in the specified environment.
|
19
|
+
|
20
|
+
The maintenance page is taken from the app currently being deployed. This means
|
21
|
+
that you can customize maintenance pages to tell users the reason for downtime
|
22
|
+
on every particular deploy.
|
23
|
+
|
24
|
+
Maintenance pages searched for in order of decreasing priority:
|
25
|
+
* public/maintenance.html.custom
|
26
|
+
* public/maintenance.html.tmp
|
27
|
+
* public/maintenance.html
|
28
|
+
* public/system/maintenance.html.default
|
29
|
+
HELP
|
30
|
+
method_option :environment, :type => :string, :aliases => %w(-e),
|
31
|
+
:desc => "Environment on which to take down the maintenance page"
|
32
|
+
def disable
|
33
|
+
app = api.app_for_repo!(repo)
|
34
|
+
environment = fetch_environment(options[:environment], app)
|
35
|
+
loudly_check_eysd(environment)
|
36
|
+
EY.ui.info "Putting up maintenance page for #{environment.name}"
|
37
|
+
environment.put_up_maintenance_page(app)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
data/lib/engineyard/error.rb
CHANGED
@@ -16,6 +16,13 @@ module EY
|
|
16
16
|
end
|
17
17
|
end
|
18
18
|
|
19
|
+
class InvalidAppError < Error
|
20
|
+
def initialize(name)
|
21
|
+
error = %|There is no app configured with the name "#{name}"|
|
22
|
+
super error
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
19
26
|
class NoAppMaster < EY::Error
|
20
27
|
def initialize(env_name)
|
21
28
|
super "The environment '#{env_name}' does not have a master instance."
|
@@ -67,7 +74,7 @@ module EY
|
|
67
74
|
|
68
75
|
class DeployArgumentError < EY::Error
|
69
76
|
def initialize
|
70
|
-
super
|
77
|
+
super %("deploy" was called incorrectly. Call as "deploy [--environment <env>] [--ref <branch|tag|ref>]"\n) +
|
71
78
|
%|You can set default environments and branches in ey.yml|
|
72
79
|
end
|
73
80
|
end
|
@@ -31,16 +31,24 @@ module EY
|
|
31
31
|
master
|
32
32
|
end
|
33
33
|
|
34
|
-
def ensure_eysd_present
|
35
|
-
app_master!.ensure_eysd_present
|
34
|
+
def ensure_eysd_present(&blk)
|
35
|
+
app_master!.ensure_eysd_present(&blk)
|
36
36
|
end
|
37
37
|
|
38
|
-
def deploy
|
39
|
-
app_master!.deploy
|
38
|
+
def deploy(app, ref, migration_command=nil)
|
39
|
+
app_master!.deploy(app, ref, migration_command, config)
|
40
40
|
end
|
41
41
|
|
42
|
-
def rollback
|
43
|
-
app_master!.rollback
|
42
|
+
def rollback(app)
|
43
|
+
app_master!.rollback(app, config)
|
44
|
+
end
|
45
|
+
|
46
|
+
def take_down_maintenance_page(app)
|
47
|
+
app_master!.take_down_maintenance_page(app)
|
48
|
+
end
|
49
|
+
|
50
|
+
def put_up_maintenance_page(app)
|
51
|
+
app_master!.put_up_maintenance_page(app)
|
44
52
|
end
|
45
53
|
|
46
54
|
def rebuild
|
@@ -51,6 +59,23 @@ module EY
|
|
51
59
|
api.request("/environments/#{id}/run_custom_recipes", :method => :put)
|
52
60
|
end
|
53
61
|
|
62
|
+
def download_recipes
|
63
|
+
if File.exist?('cookbooks')
|
64
|
+
raise EY::Error, "Could not download, cookbooks already exists"
|
65
|
+
end
|
66
|
+
|
67
|
+
require 'tempfile'
|
68
|
+
tmp = Tempfile.new("recipes")
|
69
|
+
tmp.write(api.request("/environments/#{id}/recipes"))
|
70
|
+
tmp.flush
|
71
|
+
|
72
|
+
cmd = "tar xzf '#{tmp.path}' cookbooks"
|
73
|
+
|
74
|
+
unless system(cmd)
|
75
|
+
raise EY::Error, "Could not unarchive recipes.\nCommand `#{cmd}` exited with an error."
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
54
79
|
def upload_recipes(file_to_upload = recipe_file)
|
55
80
|
api.request("/environments/#{id}/recipes",
|
56
81
|
:method => :post,
|
@@ -95,4 +120,5 @@ module EY
|
|
95
120
|
end
|
96
121
|
end
|
97
122
|
end
|
123
|
+
|
98
124
|
end
|