engineyard 1.3.17 → 1.3.18

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/README.rdoc CHANGED
@@ -5,13 +5,16 @@ Command:
5
5
  ey deploy
6
6
 
7
7
  Options:
8
- -r, --branch, --tag, [--ref=REF] # Git ref to deploy. May be a branch, a tag, or a SHA.
9
- [--ignore-bad-master] # Force a deploy even if the master is in a bad state
10
- -v, [--verbose] # Be verbose
11
- -a, [--app=APP] # Name of the application to deploy
12
- -m, [--migrate=MIGRATE] # Run migrations via [MIGRATE], defaults to 'rake db:migrate'; use --no-migrate to avoid running migrations
13
- [--ignore-default-branch] # Force a deploy of the specified branch even if a default is set
14
- -e, [--environment=ENVIRONMENT] # Environment in which to deploy this application
8
+ -r, --branch, --tag, [--ref=REF] # Git ref to deploy. May be a branch, a tag, or a SHA.
9
+ [--ignore-bad-master] # Force a deploy even if the master is in a bad state
10
+ -v, [--verbose] # Be verbose
11
+ -a, [--app=APP] # Name of the application to deploy
12
+ -m, [--migrate=MIGRATE] # Run migrations via [MIGRATE], defaults to 'rake db:migrate'; use --no-migrate to avoid running migrations
13
+ [--ignore-default-branch] # Force a deploy of the specified branch even if a default is set
14
+ -e, [--environment=ENVIRONMENT] # Environment in which to deploy this application
15
+ -c, [--account=ACCOUNT] # Name of the account in which the environment can be found
16
+ [--extra-deploy-hook-options key:val] # Additional options to be made available in deploy hooks (in the 'config' hash)
17
+
15
18
 
16
19
  Description:
17
20
  This command must be run with the current directory containing the app to be deployed. If ey.yml specifies a default branch then the ref parameter can be omitted. Furthermore,
@@ -35,6 +38,7 @@ Command:
35
38
 
36
39
  Options:
37
40
  -e, [--environment=ENVIRONMENT] # Environment with the interesting logs
41
+ -c, [--account=ACCOUNT] # Name of the account in which the environment can be found
38
42
 
39
43
  Description:
40
44
  Displays Engine Yard configuration logs for all servers in the environment. If recipes were uploaded to the environment & run, their logs will also be displayed beneath the
@@ -45,6 +49,7 @@ Command:
45
49
 
46
50
  Options:
47
51
  -e, [--environment=ENVIRONMENT] # Environment to rebuild
52
+ -c, [--account=ACCOUNT] # Name of the account in which the environment can be found
48
53
 
49
54
  Description:
50
55
  Engine Yard's main configuration run occurs on all servers. Mainly used to fix failed configuration of new or existing servers, or to update servers to latest Engine Yard stack
@@ -59,6 +64,7 @@ Command:
59
64
  -v, [--verbose] # Be verbose
60
65
  -a, [--app=APP] # Name of the application to roll back
61
66
  -e, [--environment=ENVIRONMENT] # Environment in which to roll back the application
67
+ -c, [--account=ACCOUNT] # Name of the account in which the environment can be found
62
68
 
63
69
  Description:
64
70
  Uses code from previous deploy in the "/data/APP_NAME/releases" directory on remote server(s) to restart application servers.
@@ -68,6 +74,7 @@ Command:
68
74
 
69
75
  Options:
70
76
  -e, [--environment=ENVIRONMENT] # Environment in which to apply recipes
77
+ -c, [--account=ACCOUNT] # Name of the account in which the environment can be found
71
78
 
72
79
  Description:
73
80
  This is similar to 'ey rebuild' except Engine Yard's main configuration step is skipped.
@@ -77,6 +84,7 @@ Command:
77
84
 
78
85
  Options:
79
86
  -e, [--environment=ENVIRONMENT] # Environment that will receive the recipes
87
+ -c, [--account=ACCOUNT] # Name of the account in which the environment can be found
80
88
 
81
89
  Description:
82
90
  The current directory should contain a subdirectory named "cookbooks" to be uploaded.
@@ -86,6 +94,7 @@ Command:
86
94
 
87
95
  Options:
88
96
  -e, [--environment=ENVIRONMENT] # Environment for which to download the recipes
97
+ -c, [--account=ACCOUNT] # Name of the account in which the environment can be found
89
98
 
90
99
  Description:
91
100
  The recipes will be unpacked into a directory called "cookbooks" in the current directory.
@@ -99,6 +108,7 @@ Command:
99
108
  -v, [--verbose] # Be verbose
100
109
  -a, [--app=APP] # Name of the application whose maintenance page will be removed
101
110
  -e, [--environment=ENVIRONMENT] # Environment on which to take down the maintenance page
111
+ -c, [--account=ACCOUNT] # Name of the account in which the environment can be found
102
112
 
103
113
  Remove the maintenance page for this application in the given environment.
104
114
 
@@ -109,6 +119,7 @@ Command:
109
119
  -v, [--verbose] # Be verbose
110
120
  -a, [--app=APP] # Name of the application whose maintenance page will be put up
111
121
  -e, [--environment=ENVIRONMENT] # Environment on which to put up the maintenance page
122
+ -c, [--account=ACCOUNT] # Name of the account in which the environment can be found
112
123
 
113
124
  Description:
114
125
  The maintenance page is taken from the app currently being deployed. This means that you can customize maintenance pages to tell users the reason for downtime on every
@@ -128,6 +139,7 @@ Command:
128
139
  -a, [--all] # Run command on all servers
129
140
  [--db-slaves] # Run command on the slave database servers
130
141
  -e, [--environment=ENVIRONMENT] # Environment to ssh into
142
+ -c, [--account=ACCOUNT] # Name of the account in which the environment can be found
131
143
 
132
144
  Description:
133
145
  If a command is supplied, it will be run, otherwise a session will be opened. The application master is used for environments with clusters. Option --all requires a command to
data/bin/ey CHANGED
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env ruby
2
+ $:.unshift(File.expand_path('../../lib', __FILE__))
2
3
  require 'engineyard/cli'
3
4
 
4
5
  begin
@@ -4,6 +4,8 @@ module EY
4
4
  class API
5
5
  attr_reader :token
6
6
 
7
+ USER_AGENT_STRING = "EngineYardCLI/#{VERSION}"
8
+
7
9
  def initialize(token = nil)
8
10
  @token ||= token
9
11
  @token ||= self.class.read_token
@@ -47,10 +49,11 @@ module EY
47
49
  require 'json'
48
50
 
49
51
  url = EY.config.endpoint + "api/v2#{path}"
50
- method = ((meth = opts.delete(:method)) && meth.to_s || "get").downcase.to_sym
52
+ method = (opts.delete(:method) || 'get').to_s.downcase.to_sym
51
53
  params = opts.delete(:params) || {}
52
54
  headers = opts.delete(:headers) || {}
53
55
  headers["Accept"] ||= "application/json"
56
+ headers["User-Agent"] = USER_AGENT_STRING
54
57
 
55
58
  begin
56
59
  EY.ui.debug("Request", "#{method.to_s.upcase} #{url}")
@@ -67,6 +70,8 @@ module EY
67
70
  raise RequestFailed, "Could not reach the cloud API"
68
71
  rescue RestClient::ResourceNotFound
69
72
  raise RequestFailed, "The requested resource could not be found"
73
+ rescue RestClient::BadGateway
74
+ raise RequestFailed, "AppCloud API is temporarily unavailable. Please try again soon."
70
75
  rescue RestClient::RequestFailed => e
71
76
  raise RequestFailed, "#{e.message} #{e.response}"
72
77
  rescue OpenSSL::SSL::SSLError
@@ -126,6 +126,7 @@ module EY
126
126
  EY.ui.debug("Rebuilding #{environment.name}")
127
127
  environment.rebuild
128
128
  end
129
+ map "update" => :rebuild
129
130
 
130
131
  desc "rollback [--environment ENVIRONMENT]", "Rollback to the previous deploy."
131
132
  long_desc <<-DESC
@@ -186,7 +187,7 @@ module EY
186
187
  environment = fetch_environment(options[:environment], options[:account])
187
188
  hosts = ssh_hosts(options, environment)
188
189
 
189
- raise NoCommandError.new if cmd.nil? and hosts.count != 1
190
+ raise NoCommandError.new if cmd.nil? and hosts.size != 1
190
191
 
191
192
  hosts.each do |host|
192
193
  system Escape.shell_command(['ssh', "#{environment.username}@#{host}", cmd].compact)
@@ -2,13 +2,13 @@ module EY
2
2
  class CLI
3
3
  class Recipes < EY::Thor
4
4
  desc "apply [--environment ENVIRONMENT]",
5
- "Run chef recipes uploaded by the 'recipes upload' command on the specified environment."
5
+ "Run chef recipes uploaded by '#{banner_base} recipes upload' on the specified environment."
6
6
  long_desc <<-DESC
7
7
  This is similar to '#{banner_base} rebuild' except Engine Yard's main
8
8
  configuration step is skipped.
9
9
 
10
- The cookbook uploaded by the 'recipes upload' command will be run when
11
- you run 'recipes apply'.
10
+ The cookbook uploaded by the '#{banner_base} recipes upload' command will be run when
11
+ you run '#{banner_base} recipes apply'.
12
12
  DESC
13
13
 
14
14
  method_option :environment, :type => :string, :aliases => %w(-e),
@@ -17,27 +17,54 @@ module EY
17
17
  :desc => "Name of the account in which the environment can be found"
18
18
  def apply
19
19
  environment = fetch_environment(options[:environment], options[:account])
20
- environment.run_custom_recipes
21
- EY.ui.say "Uploaded recipes started for #{environment.name}"
20
+ apply_recipes(environment)
22
21
  end
23
22
 
24
23
  desc "upload [--environment ENVIRONMENT]",
25
24
  "Upload custom chef recipes to specified environment so they can be applied."
26
25
  long_desc <<-DESC
27
- The current working directory should contain a subdirectory named "cookbooks"
28
- that is the collection of recipes to be uploaded.
26
+ Make an archive of the "cookbooks/" subdirectory in your current working
27
+ directory and upload it to AppCloud's recipe storage.
29
28
 
30
- The uploaded cookbook will be run when executing 'recipes apply'.
29
+ Alternatively, specify a .tgz of a cookbooks/ directory yourself as follows:
30
+
31
+ $ #{banner_base} recipes upload -f path/to/recipes.tgz
32
+
33
+ The uploaded cookbooks will be run when executing '#{banner_base} recipes apply'
34
+ and also automatically each time you update/rebuild your instances.
31
35
  DESC
32
36
 
33
37
  method_option :environment, :type => :string, :aliases => %w(-e),
34
38
  :desc => "Environment that will receive the recipes"
35
39
  method_option :account, :type => :string, :aliases => %w(-c),
36
40
  :desc => "Name of the account in which the environment can be found"
41
+ method_option :apply, :type => :boolean,
42
+ :desc => "Apply the recipes immediately after they are uploaded"
43
+ method_option :file, :type => :string, :aliases => %w(-f),
44
+ :desc => "Specify a gzipped tar file (.tgz) for upload instead of cookbooks/ directory"
37
45
  def upload
38
46
  environment = fetch_environment(options[:environment], options[:account])
39
- environment.upload_recipes
40
- EY.ui.say "Recipes uploaded successfully for #{environment.name}"
47
+ upload_recipes(environment, options[:file])
48
+ if options[:apply]
49
+ apply_recipes(environment)
50
+ end
51
+ end
52
+
53
+ no_tasks do
54
+ def apply_recipes(environment)
55
+ environment.run_custom_recipes
56
+ EY.ui.say "Uploaded recipes started for #{environment.name}"
57
+ end
58
+
59
+ def upload_recipes(environment, filename)
60
+ if options[:file]
61
+ environment.upload_recipes_at_path(options[:file])
62
+ EY.ui.say "Recipes file #{options[:file]} uploaded successfully for #{environment.name}"
63
+ else
64
+ environment.tar_and_upload_recipes_in_cookbooks_dir
65
+ EY.ui.say "Recipes in cookbooks/ uploaded successfully for #{environment.name}"
66
+ end
67
+ end
41
68
  end
42
69
 
43
70
  desc "download [--environment ENVIRONMENT]",
@@ -28,43 +28,33 @@ module EY
28
28
  end
29
29
 
30
30
  def error(name, message = nil)
31
- begin
32
- orig_out, $stdout = $stdout, $stderr
33
- if message
34
- say_status name, message, :red
35
- elsif name
36
- say name, :red
37
- end
38
- ensure
39
- $stdout = orig_out
40
- end
31
+ $stdout = $stderr
32
+ say_with_status(name, message, :red)
33
+ ensure
34
+ $stdout = STDOUT
41
35
  end
42
36
 
43
37
  def warn(name, message = nil)
44
- if message
45
- say_status name, message, :yellow
46
- elsif name
47
- say name, :yellow
48
- end
38
+ say_with_status(name, message, :yellow)
49
39
  end
50
40
 
51
41
  def info(name, message = nil)
52
- if message
53
- say_status name, message, :green
54
- elsif name
55
- say name, :green
56
- end
42
+ say_with_status(name, message, :green)
57
43
  end
58
44
 
59
45
  def debug(name, message = nil)
60
- return unless ENV["DEBUG"]
46
+ if ENV["DEBUG"]
47
+ name = name.inspect unless name.nil? or name.is_a?(String)
48
+ message = message.inspect unless message.nil? or message.is_a?(String)
49
+ say_with_status(name, message, :blue)
50
+ end
51
+ end
61
52
 
53
+ def say_with_status(name, message=nil, color=nil)
62
54
  if message
63
- message = message.inspect unless message.is_a?(String)
64
- say_status name, message, :blue
55
+ say_status name, message, color
65
56
  elsif name
66
- name = name.inspect unless name.is_a?(String)
67
- say name, :cyan
57
+ say name, color
68
58
  end
69
59
  end
70
60
 
@@ -71,6 +71,7 @@ module EY
71
71
  tmp = Tempfile.new("recipes")
72
72
  tmp.write(api.request("/environments/#{id}/recipes"))
73
73
  tmp.flush
74
+ tmp.close
74
75
 
75
76
  cmd = "tar xzf '#{tmp.path}' cookbooks"
76
77
 
@@ -79,27 +80,36 @@ module EY
79
80
  end
80
81
  end
81
82
 
82
- def upload_recipes(file_to_upload = recipe_file)
83
- api.request("/environments/#{id}/recipes",
84
- :method => :post,
85
- :params => {:file => file_to_upload}
86
- )
83
+ def upload_recipes_at_path(recipes_path)
84
+ recipes_path = Pathname.new(recipes_path)
85
+ if recipes_path.exist?
86
+ upload_recipes recipes_path.open('r')
87
+ else
88
+ raise EY::Error, "Recipes file not found: #{recipes_path}"
89
+ end
87
90
  end
88
91
 
89
- def recipe_file
92
+ def tar_and_upload_recipes_in_cookbooks_dir
90
93
  require 'tempfile'
91
94
  unless File.exist?("cookbooks")
92
95
  raise EY::Error, "Could not find chef recipes. Please run from the root of your recipes repo."
93
96
  end
94
97
 
95
- tmp = Tempfile.new("recipes")
96
- cmd = "tar czf '#{tmp.path}' cookbooks/"
98
+ recipes_file = Tempfile.new("recipes")
99
+ cmd = "tar czf '#{recipes_file.path}' cookbooks/"
97
100
 
98
101
  unless system(cmd)
99
102
  raise EY::Error, "Could not archive recipes.\nCommand `#{cmd}` exited with an error."
100
103
  end
101
104
 
102
- tmp
105
+ upload_recipes(recipes_file)
106
+ end
107
+
108
+ def upload_recipes(file_to_upload)
109
+ api.request("/environments/#{id}/recipes", {
110
+ :method => :post,
111
+ :params => {:file => file_to_upload}
112
+ })
103
113
  end
104
114
 
105
115
  def resolve_branch(branch, allow_non_default_branch=false)
@@ -1,3 +1,3 @@
1
1
  module EY
2
- VERSION = '1.3.17'
2
+ VERSION = '1.3.18'
3
3
  end
@@ -53,4 +53,11 @@ describe EY::API do
53
53
  }.should raise_error(EY::Error)
54
54
  end
55
55
 
56
+ it "raises RequestFailed with a friendly error when cloud is under maintenance" do
57
+ FakeWeb.register_uri(:post, "https://cloud.engineyard.com/api/v2/authenticate", :status => 502, :content_type => 'text/html')
58
+
59
+ lambda {
60
+ EY::API.fetch_token("a@b.com", "foo")
61
+ }.should raise_error(EY::API::RequestFailed, /AppCloud API is temporarily unavailable/)
62
+ end
56
63
  end
@@ -17,12 +17,52 @@ describe "ey recipes upload" do
17
17
  end
18
18
 
19
19
  def verify_ran(scenario)
20
- @out.should =~ /Recipes uploaded successfully for #{scenario[:environment]}/
20
+ @out.should =~ %r|Recipes in cookbooks/ uploaded successfully for #{scenario[:environment]}|
21
21
  end
22
22
 
23
23
  it_should_behave_like "it takes an environment name and an account name"
24
24
  end
25
25
 
26
+ describe "ey recipes upload -f recipes.tgz" do
27
+ given "integration"
28
+
29
+ define_git_repo('+recipes') do |git_dir|
30
+ link_recipes_tgz(git_dir)
31
+ end
32
+ use_git_repo('+recipes')
33
+
34
+ def command_to_run(opts)
35
+ cmd = %w[recipes upload]
36
+ cmd << "--environment" << opts[:environment] if opts[:environment]
37
+ cmd << "--account" << opts[:account] if opts[:account]
38
+ cmd << "-f" << "recipes.tgz"
39
+ cmd
40
+ end
41
+
42
+ def verify_ran(scenario)
43
+ @out.should =~ %r|Recipes file recipes.tgz uploaded successfully for #{scenario[:environment]}|
44
+ end
45
+
46
+ it_should_behave_like "it takes an environment name and an account name"
47
+ end
48
+
49
+ describe "ey recipes upload -f with a missing filenamen" do
50
+ given "integration"
51
+ def command_to_run(opts)
52
+ cmd = %w[recipes upload]
53
+ cmd << "--environment" << opts[:environment] if opts[:environment]
54
+ cmd << "--account" << opts[:account] if opts[:account]
55
+ cmd << "-f" << "recipes.tgz"
56
+ cmd
57
+ end
58
+
59
+ it "errors with file not found" do
60
+ api_scenario "one app, one environment"
61
+ ey(%w[recipes upload --environment giblets -f recipes.tgz], :expect_failure => true)
62
+ @err.should match(/Recipes file not found: recipes.tgz/i)
63
+ end
64
+ end
65
+
26
66
  describe "ey recipes upload with an ambiguous git repo" do
27
67
  given "integration"
28
68
  def command_to_run(_) %w[recipes upload] end
@@ -48,7 +88,16 @@ describe "ey recipes upload from a separate cookbooks directory" do
48
88
  api_scenario "one app, one environment"
49
89
 
50
90
  ey %w[recipes upload -e giblets]
51
- @out.should =~ /Recipes uploaded successfully/
91
+ @out.should =~ %r|Recipes in cookbooks/ uploaded successfully|
92
+ @out.should_not =~ /Uploaded recipes started for giblets/
93
+ end
94
+
95
+ it "applies the recipes with --apply" do
96
+ api_scenario "one app, one environment"
97
+
98
+ ey %w[recipes upload -e giblets --apply]
99
+ @out.should =~ %r|Recipes in cookbooks/ uploaded successfully|
100
+ @out.should =~ /Uploaded recipes started for giblets/
52
101
  end
53
102
  end
54
103
 
@@ -70,7 +119,7 @@ describe "ey recipes upload from a separate cookbooks directory" do
70
119
  api_scenario "one app, one environment"
71
120
 
72
121
  ey %w[recipes upload -e giblets]
73
- @out.should =~ /Recipes uploaded successfully/
122
+ @out.should =~ %r|Recipes in cookbooks/ uploaded successfully|
74
123
  end
75
124
 
76
125
  end
data/spec/spec_helper.rb CHANGED
@@ -45,6 +45,7 @@ Spec::Runner.configure do |config|
45
45
  config.include Spec::Helpers
46
46
  config.include Spec::GitRepo
47
47
  config.extend Spec::Helpers::SemanticNames
48
+ config.extend Spec::Helpers::Fixtures
48
49
 
49
50
  config.before(:all) do
50
51
  FakeWeb.allow_net_connect = false