engineyard 0.4.0 → 0.5.0

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