engineyard 1.3.17 → 1.3.18

Sign up to get free protection for your applications and to get access to all the features.
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