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 CHANGED
@@ -1,7 +1,7 @@
1
1
  module EY
2
2
  require 'engineyard/ruby_ext'
3
3
 
4
- VERSION = "0.4.0"
4
+ VERSION = "0.5.0"
5
5
 
6
6
  autoload :API, 'engineyard/api'
7
7
  autoload :Collection, 'engineyard/collection'
@@ -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
- else
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
@@ -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] [BRANCH]", "Deploy [BRANCH] of the app in the current directory to [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
- def deploy(env_name = nil, branch = nil)
25
- app = api.app_for_repo!(repo)
26
- environment = fetch_environment(env_name, app)
27
- deploy_branch = environment.resolve_branch(branch, options[:force]) ||
28
- repo.current_branch ||
29
- raise(DeployArgumentError)
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.ensure_eysd_present! do |action|
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!(app, deploy_branch, options[:migrate])
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(env_name)
56
- raise exists ? EnvironmentUnlinkedError.new(env_name) : e
69
+ exists = api.environments.named(options[:environment])
70
+ raise exists ? EnvironmentUnlinkedError.new(options[:environment]) : e
57
71
  end
58
72
 
59
- desc "environments [--all]", "List cloud environments for this app, or all environments"
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]", "Rebuild environment (ensure configuration is up-to-date)"
69
- def rebuild(name = nil)
70
- env = fetch_environment(name)
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]", "Rollback to the previous deploy"
76
- def rollback(name = nil)
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
- if env.app_master
81
- EY.ui.info("Rolling back #{env.name}")
82
- if env.rollback!(app)
83
- EY.ui.info "Rollback complete"
84
- else
85
- raise EY::Error, "Rollback failed"
86
- end
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 NoAppMaster.new(env.name)
126
+ raise EY::Error, "Rollback failed"
89
127
  end
90
128
  end
91
129
 
92
- desc "ssh [ENVIRONMENT]", "Open an ssh session to the environment's application server"
93
- def ssh(name = nil)
94
- env = fetch_environment(name)
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]", "Retrieve the latest logs for an environment"
104
- def logs(name = nil)
105
- fetch_environment(name).logs.each do |log|
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 COMMAND [ARGS]", "Commands related to custom recipes"
174
+ desc "recipes", "Commands related to chef recipes."
121
175
  subcommand "recipes", EY::CLI::Recipes
122
176
 
123
- desc "version", "Print the version of the engineyard gem"
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
- desc "recipes apply [ENV]", "Apply uploaded chef recipes on ENV"
6
- def apply(name = nil)
7
- environment = fetch_environment(name)
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 [ENV]", "Upload custom chef recipes from the current directory to ENV"
13
- def upload(name = nil)
14
- environment = fetch_environment(name)
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
@@ -43,6 +43,14 @@ module EY
43
43
  end
44
44
  end
45
45
 
46
+ def say(*args)
47
+ if args.first == "Tasks:"
48
+ super "Commands:"
49
+ else
50
+ super
51
+ end
52
+ end
53
+
46
54
  def ask(message, password = false)
47
55
  begin
48
56
  require 'highline'
@@ -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
@@ -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 %|"deploy" was called incorrectly. Call as "deploy [ENVIRONMENT] [BRANCH]"\n| +
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!(&blk)
35
- app_master!.ensure_eysd_present!(&blk)
34
+ def ensure_eysd_present(&blk)
35
+ app_master!.ensure_eysd_present(&blk)
36
36
  end
37
37
 
38
- def deploy!(app, ref, migration_command=nil)
39
- app_master!.deploy!(app, ref, migration_command, config)
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!(app)
43
- app_master!.rollback!(app, config)
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