engineyard 0.3.3 → 0.4.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.3.3"
4
+ VERSION = "0.4.0"
5
5
 
6
6
  autoload :API, 'engineyard/api'
7
7
  autoload :Collection, 'engineyard/collection'
@@ -34,6 +34,10 @@ module EY
34
34
  apps.find{|a| repo.urls.include?(a.repository_uri) }
35
35
  end
36
36
 
37
+ def app_for_repo!(repo)
38
+ app_for_repo(repo) || raise(NoAppError.new(repo))
39
+ end
40
+
37
41
  class InvalidCredentials < EY::Error; end
38
42
  class RequestFailed < EY::Error; end
39
43
 
@@ -69,8 +73,8 @@ module EY
69
73
  raise RequestFailed, "SSL is misconfigured on your cloud"
70
74
  end
71
75
 
72
- if resp.code == 204
73
- data = nil
76
+ if resp.body.empty?
77
+ data = ''
74
78
  else
75
79
  begin
76
80
  data = JSON.parse(resp.body)
@@ -21,11 +21,39 @@ module EY
21
21
  method_option :migrate, :type => :string, :aliases => %w(-m),
22
22
  :default => 'rake db:migrate',
23
23
  :desc => "Run migrations via [MIGRATE], defaults to 'rake db:migrate'; use --no-migrate to avoid running migrations"
24
- method_option :install_eysd, :type => :boolean, :aliases => %(-s),
25
- :desc => "Force remote install of eysd"
26
24
  def deploy(env_name = nil, branch = nil)
27
- require 'engineyard/cli/action/deploy'
28
- EY::CLI::Action::Deploy.call(env_name, branch, options)
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)
30
+
31
+ EY.ui.info "Connecting to the server..."
32
+
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
44
+
45
+ EY.ui.info "Running deploy for '#{environment.name}' on server..."
46
+
47
+ if environment.deploy!(app, deploy_branch, options[:migrate])
48
+ EY.ui.info "Deploy complete"
49
+ else
50
+ raise EY::Error, "Deploy failed"
51
+ end
52
+
53
+ rescue NoEnvironmentError => e
54
+ # 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
29
57
  end
30
58
 
31
59
  desc "environments [--all]", "List cloud environments for this app, or all environments"
@@ -37,14 +65,31 @@ module EY
37
65
  end
38
66
  map "envs" => :environments
39
67
 
40
- desc "rebuild [ENV]", "Rebuild environment (ensure configuration is up-to-date)"
68
+ desc "rebuild [ENVIRONMENT]", "Rebuild environment (ensure configuration is up-to-date)"
41
69
  def rebuild(name = nil)
42
70
  env = fetch_environment(name)
43
71
  EY.ui.debug("Rebuilding #{env.name}")
44
72
  env.rebuild
45
73
  end
46
74
 
47
- desc "ssh [ENV]", "Open an ssh session to the environment's application server"
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)
79
+
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
87
+ else
88
+ raise NoAppMaster.new(env.name)
89
+ end
90
+ end
91
+
92
+ desc "ssh [ENVIRONMENT]", "Open an ssh session to the environment's application server"
48
93
  def ssh(name = nil)
49
94
  env = fetch_environment(name)
50
95
 
@@ -55,7 +100,7 @@ module EY
55
100
  end
56
101
  end
57
102
 
58
- desc "logs [ENV]", "Retrieve the latest logs for an environment"
103
+ desc "logs [ENVIRONMENT]", "Retrieve the latest logs for an environment"
59
104
  def logs(name = nil)
60
105
  fetch_environment(name).logs.each do |log|
61
106
  EY.ui.info log.instance_name
@@ -23,10 +23,6 @@ module EY
23
23
  @token = self.class.fetch_token
24
24
  end
25
25
 
26
- def fetch_app_for_repo(repo)
27
- app_for_repo(repo) || raise(NoAppError.new(repo))
28
- end
29
-
30
26
  def self.fetch_token
31
27
  EY.ui.warn("The engineyard gem is prerelease software. Please do not use")
32
28
  EY.ui.warn("this tool to deploy to mission-critical environments, yet.")
@@ -1,14 +1,22 @@
1
1
  module EY
2
2
  class CLI
3
3
  class Recipes < EY::Thor
4
+
5
+ desc "recipes apply [ENV]", "Apply uploaded chef recipes on ENV"
6
+ def apply(name = nil)
7
+ environment = fetch_environment(name)
8
+ environment.run_custom_recipes
9
+ EY.ui.say "Uploaded recipes started for #{environment.name}"
10
+ end
11
+
4
12
  desc "recipes upload [ENV]", "Upload custom chef recipes from the current directory to ENV"
5
13
  def upload(name = nil)
6
- if fetch_environment(name).upload_recipes
7
- EY.ui.say "Recipes uploaded successfully"
8
- else
9
- EY.ui.error "Recipes upload failed"
10
- end
14
+ environment = fetch_environment(name)
15
+ environment.upload_recipes
16
+ EY.ui.say "Recipes uploaded successfully for #{environment.name}"
11
17
  end
18
+
12
19
  end
13
20
  end
21
+
14
22
  end
@@ -18,8 +18,7 @@ module EY
18
18
  end
19
19
 
20
20
  def match_one!(name_part)
21
- match_one(name_part) or raise EnvironmentError,
22
- "Environment containing '#{name_part}' can't be found\nYou can create it at #{EY.config.endpoint}"
21
+ match_one(name_part) or raise NoEnvironmentError.new(name_part)
23
22
  end
24
23
 
25
24
  private
@@ -9,76 +9,65 @@ module EY
9
9
 
10
10
  class NoAppError < Error
11
11
  def initialize(repo)
12
- @repo = repo
13
- end
14
-
15
- def message
16
12
  error = [%|There is no application configured for any of the following remotes:|]
17
- @repo.urls.each{|url| error << %|\t#{url}| }
13
+ repo.urls.each{|url| error << %|\t#{url}| }
18
14
  error << %|You can add this application at #{EY.config.endpoint}|
19
- error.join("\n")
15
+ super error.join("\n")
20
16
  end
21
17
  end
22
18
 
23
19
  class NoAppMaster < EY::Error
24
20
  def initialize(env_name)
25
- @env_name = env_name
21
+ super "The environment '#{env_name}' does not have a master instance."
26
22
  end
23
+ end
27
24
 
28
- def message
29
- "The environment '#{@env_name}' does not have a master instance."
25
+ class BadAppMasterStatus < EY::Error
26
+ def initialize(master_status)
27
+ super "Application master's status is not \"running\" (green); it is \"#{master_status}\"."
30
28
  end
31
29
  end
32
30
 
33
31
  class EnvironmentError < EY::Error
34
32
  end
35
33
 
36
- class AmbiguousEnvironmentName < EY::Error
34
+ class AmbiguousEnvironmentName < EY::EnvironmentError
37
35
  def initialize(name, matches)
38
- @name, @matches = name, matches
39
- end
40
-
41
- def message
42
- pretty_names = @matches.map {|x| "'#{x}'"}.join(', ')
43
- "The name '#{@name}' is ambiguous; it matches all of the following environment names: #{pretty_names}.\nPlease use a longer, unambiguous substring or the entire environment name."
36
+ pretty_names = matches.map {|x| "'#{x}'"}.join(', ')
37
+ super "The name '#{name}' is ambiguous; it matches all of the following environment names: #{pretty_names}.\n" +
38
+ "Please use a longer, unambiguous substring or the entire environment name."
44
39
  end
45
40
  end
46
41
 
47
- class NoSingleEnvironmentError < EY::Error
42
+ class NoSingleEnvironmentError < EY::EnvironmentError
48
43
  def initialize(app)
49
- @envs = app.environments
50
- end
51
-
52
- def message
53
- "Unable to determine a single environment for the current application (found #{@envs.size} environments)"
44
+ size = app.environments.size
45
+ super "Unable to determine a single environment for the current application (found #{size} environments)"
54
46
  end
55
47
  end
56
48
 
57
- class NoEnvironmentError < EY::Error
49
+ class NoEnvironmentError < EY::EnvironmentError
58
50
  def initialize(env_name=nil)
59
- @env_name = env_name
51
+ super "No environment named '#{env_name}'\nYou can create one at #{EY.config.endpoint}"
60
52
  end
53
+ end
61
54
 
62
- def message
63
- "No environment named '#{@env_name}'\nYou can create one at #{EY.config.endpoint}"
55
+ class EnvironmentUnlinkedError < EY::Error
56
+ def initialize(env_name)
57
+ super "Environment '#{env_name}' exists but does not run this application."
64
58
  end
65
59
  end
66
60
 
67
61
  class BranchMismatch < EY::Error
68
62
  def initialize(default_branch, branch)
69
- super(nil)
70
- @default_branch, @branch = default_branch, branch
71
- end
72
-
73
- def message
74
- %|Your deploy branch is set to "#{@default_branch}".\n| +
75
- %|If you want to deploy branch "#{@branch}", use --force.|
63
+ super %|Your deploy branch is set to "#{default_branch}".\n| +
64
+ %|If you want to deploy branch "#{branch}", use --force.|
76
65
  end
77
66
  end
78
67
 
79
68
  class DeployArgumentError < EY::Error
80
- def message
81
- %|"deploy" was called incorrectly. Call as "deploy [ENVIRONMENT] [BRANCH]"\n| +
69
+ def initialize
70
+ super %|"deploy" was called incorrectly. Call as "deploy [ENVIRONMENT] [BRANCH]"\n| +
82
71
  %|You can set default environments and branches in ey.yml|
83
72
  end
84
73
  end
@@ -21,10 +21,36 @@ module EY
21
21
  Instance.from_array(api_get("/environments/#{id}/instances")["instances"], :environment => self)
22
22
  end
23
23
 
24
+ def app_master!
25
+ master = app_master
26
+ if master.nil?
27
+ raise NoAppMaster.new(name)
28
+ elsif master.status != "running"
29
+ raise BadAppMasterStatus.new(master.status)
30
+ end
31
+ master
32
+ end
33
+
34
+ def ensure_eysd_present!(&blk)
35
+ app_master!.ensure_eysd_present!(&blk)
36
+ end
37
+
38
+ def deploy!(app, ref, migration_command=nil)
39
+ app_master!.deploy!(app, ref, migration_command, config)
40
+ end
41
+
42
+ def rollback!(app)
43
+ app_master!.rollback!(app, config)
44
+ end
45
+
24
46
  def rebuild
25
47
  api.request("/environments/#{id}/rebuild", :method => :put)
26
48
  end
27
49
 
50
+ def run_custom_recipes
51
+ api.request("/environments/#{id}/run_custom_recipes", :method => :put)
52
+ end
53
+
28
54
  def upload_recipes(file_to_upload = recipe_file)
29
55
  api.request("/environments/#{id}/recipes",
30
56
  :method => :post,
@@ -39,7 +65,7 @@ module EY
39
65
  end
40
66
 
41
67
  tmp = Tempfile.new("recipes")
42
- cmd = "git archive --format=tar HEAD cookbooks | gzip > #{tmp.path}"
68
+ cmd = "tar czf '#{tmp.path}' cookbooks/"
43
69
 
44
70
  unless system(cmd)
45
71
  raise EY::Error, "Could not archive recipes.\nCommand `#{cmd}` exited with an error."
@@ -48,11 +74,22 @@ module EY
48
74
  tmp
49
75
  end
50
76
 
77
+ def resolve_branch(branch, allow_non_default_branch=false)
78
+ if !allow_non_default_branch && branch && default_branch && (branch != default_branch)
79
+ raise BranchMismatch.new(default_branch, branch)
80
+ end
81
+ branch || default_branch
82
+ end
83
+
51
84
  def configuration
52
85
  EY.config.environments[self.name]
53
86
  end
54
87
  alias_method :config, :configuration
55
88
 
89
+ def default_branch
90
+ EY.config.default_branch(name)
91
+ end
92
+
56
93
  def shorten_name_for(app)
57
94
  name.gsub(/^#{Regexp.quote(app.name)}_/, '')
58
95
  end
@@ -3,7 +3,7 @@ require 'escape'
3
3
  module EY
4
4
  module Model
5
5
  class Instance < ApiStruct.new(:id, :role, :status, :amazon_id, :public_hostname, :environment)
6
- EYSD_VERSION = "~>0.3.3"
6
+ EYSD_VERSION = "~>0.4.0"
7
7
  CHECK_SCRIPT = <<-SCRIPT
8
8
  require "rubygems"
9
9
  requirement = Gem::Requirement.new("#{EYSD_VERSION}")
@@ -43,6 +43,25 @@ exit(17) # required_version < current_version
43
43
  ssh Escape.shell_command(deploy_cmd)
44
44
  end
45
45
 
46
+ def ensure_eysd_present!
47
+ case ey_deploy_check
48
+ when :ssh_failed
49
+ raise EnvironmentError, "SSH connection to #{hostname} failed"
50
+ when :eysd_missing
51
+ yield :installing if block_given?
52
+ install_ey_deploy!
53
+ when :too_new
54
+ raise EnvironmentError, "server-side component too new; please upgrade your copy of the engineyard gem."
55
+ when :too_old
56
+ yield :upgrading if block_given?
57
+ upgrade_ey_deploy!
58
+ when :ok
59
+ # no action needed
60
+ else
61
+ raise EY::Error, "Internal error: Unexpected status from Instance#ey_deploy_check; got #{eysd_status.inspect}"
62
+ end
63
+ end
64
+
46
65
  def ey_deploy_check
47
66
  require 'base64'
48
67
  encoded_script = Base64.encode64(CHECK_SCRIPT).gsub(/\n/, '')
@@ -54,6 +73,16 @@ exit(17) # required_version < current_version
54
73
  ssh(Escape.shell_command(['sudo', gem_path, 'install', 'ey-deploy', '-v', EYSD_VERSION]))
55
74
  end
56
75
 
76
+ def rollback!(app, extra_configuration=nil)
77
+ deploy_cmd = [eysd_path, 'deploy', 'rollback', '--app', app.name]
78
+
79
+ if extra_configuration
80
+ deploy_cmd << '--config' << extra_configuration.to_json
81
+ end
82
+
83
+ ssh Escape.shell_command(deploy_cmd)
84
+ end
85
+
57
86
  def upgrade_ey_deploy!
58
87
  ssh "sudo #{gem_path} uninstall -a -x ey-deploy"
59
88
  install_ey_deploy!
@@ -20,6 +20,10 @@ module EY
20
20
 
21
21
  protected
22
22
 
23
+ def self.exit_on_failure?
24
+ true
25
+ end
26
+
23
27
  def api
24
28
  @api ||= EY::CLI::API.new
25
29
  end
@@ -28,11 +32,13 @@ module EY
28
32
  @repo ||= EY::Repo.new
29
33
  end
30
34
 
31
- def fetch_environment(env_name)
32
- if env_name.nil?
33
- api.fetch_app_for_repo(repo).sole_environment!
35
+ # if an app is supplied, it is used as a constraint for implied environment lookup
36
+ def fetch_environment(env_name, app = nil)
37
+ env_name ||= EY.config.default_environment
38
+ if env_name
39
+ (app || api).environments.match_one!(env_name)
34
40
  else
35
- api.environments.match_one!(env_name)
41
+ (app || api.app_for_repo!(repo)).sole_environment!
36
42
  end
37
43
  end
38
44
 
@@ -9,9 +9,11 @@ describe "EY::Model::Environment#rebuild" do
9
9
  "api" => @api,
10
10
  })
11
11
 
12
- FakeWeb.register_uri(:put,
12
+ FakeWeb.register_uri(
13
+ :put,
13
14
  "https://cloud.engineyard.com/api/v2/environments/#{env.id}/rebuild",
14
- :body => {}.to_json)
15
+ :body => ''
16
+ )
15
17
 
16
18
  env.rebuild
17
19
 
@@ -19,6 +21,27 @@ describe "EY::Model::Environment#rebuild" do
19
21
  end
20
22
  end
21
23
 
24
+ describe "EY::Model::Environment#run_custom_recipes" do
25
+ it_should_behave_like "it has an api"
26
+
27
+ it "hits the rebuild action in the API" do
28
+ env = EY::Model::Environment.from_hash({
29
+ "id" => 46534,
30
+ "api" => @api,
31
+ })
32
+
33
+ FakeWeb.register_uri(
34
+ :put,
35
+ "https://cloud.engineyard.com/api/v2/environments/#{env.id}/run_custom_recipes",
36
+ :body => ''
37
+ )
38
+
39
+ env.run_custom_recipes
40
+
41
+ FakeWeb.should have_requested(:put, "https://cloud.engineyard.com/api/v2/environments/#{env.id}/run_custom_recipes")
42
+ end
43
+ end
44
+
22
45
  describe "EY::Model::Environment.from_array" do
23
46
  it "returns a smart collection, not just a dumb array" do
24
47
  api_data = [
@@ -57,6 +80,45 @@ describe "EY::Model::Environment#instances" do
57
80
  end
58
81
  end
59
82
 
83
+ describe "EY::Model::Environment#app_master!" do
84
+ def make_env_with_master(app_master)
85
+ if app_master
86
+ app_master = {
87
+ "id" => 44206,
88
+ "role" => "solo",
89
+ }.merge(app_master)
90
+ end
91
+
92
+ EY::Model::Environment.from_hash({
93
+ "id" => 11830,
94
+ "name" => "guinea-pigs-are-delicious",
95
+ "app_master" => app_master,
96
+ "instances" => [app_master],
97
+ })
98
+ end
99
+
100
+
101
+ it "returns the app master if it's present and running" do
102
+ env = make_env_with_master("status" => "running")
103
+ env.app_master!.should_not be_nil
104
+ env.app_master!.id.should == 44206
105
+ end
106
+
107
+ it "raises an error if the app master is in a non-running state" do
108
+ env = make_env_with_master("status" => "error")
109
+ lambda {
110
+ env.app_master!
111
+ }.should raise_error(EY::BadAppMasterStatus)
112
+ end
113
+
114
+ it "raises an error if the app master is absent" do
115
+ env = make_env_with_master(nil)
116
+ lambda {
117
+ env.app_master!
118
+ }.should raise_error(EY::NoAppMaster)
119
+ end
120
+ end
121
+
60
122
  describe "EY::Model::Environment#shorten_name_for(app)" do
61
123
  def short(environment_name, app_name)
62
124
  env = EY::Model::Environment.from_hash({:name => environment_name})
@@ -41,13 +41,13 @@ describe "ey deploy" do
41
41
  it "complains when the specified environment does not contain the app" do
42
42
  api_scenario "one app, one environment, not linked"
43
43
  ey "deploy giblets master", :expect_failure => true
44
- @err.should match(/doesn't run this application/i)
44
+ @err.should match(/does not run this application/i)
45
45
  end
46
46
 
47
- it "complains when environment is ambiguous" do
47
+ it "complains when environment is not specified and app is in >1 environment" do
48
48
  api_scenario "one app, two environments"
49
49
  ey "deploy", :expect_failure => true
50
- @err.should match(/was called incorrectly/i)
50
+ @err.should match(/single environment.*2/i)
51
51
  end
52
52
 
53
53
  it "complains when the app master is in a non-running state" do
@@ -94,48 +94,23 @@ describe "ey deploy" do
94
94
  end
95
95
 
96
96
  context "choosing something to deploy" do
97
- before(:all) do
98
- api_scenario "one app, one environment", "user@git.host/path/to/repo.git"
99
- end
97
+ define_git_repo('deploy test') do
98
+ # we'll have one commit on master
99
+ system("echo 'source :gemcutter' > Gemfile")
100
+ system("git add Gemfile")
101
+ system("git commit -m 'initial commit' >/dev/null 2>&1")
100
102
 
101
- before(:all) do
102
- @local_git_dir = File.join(
103
- Dir.tmpdir,
104
- "ey_test_git_#{Time.now.tv_sec}_#{Time.now.tv_usec}_#{$$}")
105
-
106
- Dir.mkdir(@local_git_dir)
107
-
108
- Dir.chdir(@local_git_dir) do
109
- [
110
- # initial repo setup
111
- 'git init >/dev/null 2>&1',
112
- 'git config user.email deploy@spec.test',
113
- 'git config user.name "Deploy Spec"',
114
- 'git remote add origin "user@git.host/path/to/repo.git"',
115
-
116
- # we'll have one commit on master
117
- "echo 'source :gemcutter' > Gemfile",
118
- "git add Gemfile",
119
- "git commit -m 'initial commit' >/dev/null 2>&1",
120
-
121
- # and a tag
122
- "git tag -a -m 'version one' v1",
123
-
124
- # and we need a non-master branch
125
- "git checkout -b current-branch >/dev/null 2>&1",
126
- ].each do |cmd|
127
- system("#{cmd}") or raise "#{cmd} failed"
128
- end
129
- end
130
- end
103
+ # and a tag
104
+ system("git tag -a -m 'version one' v1")
131
105
 
132
- before(:each) do
133
- @original_dir = Dir.getwd
134
- Dir.chdir(@local_git_dir)
106
+ # and we need a non-master branch
107
+ system("git checkout -b current-branch >/dev/null 2>&1")
135
108
  end
136
109
 
137
- after(:each) do
138
- Dir.chdir(@original_dir)
110
+ use_git_repo('deploy test')
111
+
112
+ before(:all) do
113
+ api_scenario "one app, one environment", "user@git.host:path/to/repo.git"
139
114
  end
140
115
 
141
116
  context "without a configured default branch" do
@@ -156,13 +131,12 @@ describe "ey deploy" do
156
131
  end
157
132
 
158
133
  context "when there is extra configuration" do
159
- before(:all) do
160
- write_yaml({"environments" => {"giblets" => {"bert" => "ernie"}}},
161
- File.join(@local_git_dir, "ey.yml"))
134
+ before(:each) do
135
+ write_yaml({"environments" => {"giblets" => {"bert" => "ernie"}}})
162
136
  end
163
137
 
164
- after(:all) do
165
- File.unlink(File.join(@local_git_dir, "ey.yml"))
138
+ after(:each) do
139
+ File.unlink("ey.yml")
166
140
  end
167
141
 
168
142
  it "gets passed along to eysd" do
@@ -172,13 +146,12 @@ describe "ey deploy" do
172
146
  end
173
147
 
174
148
  context "with a configured default branch" do
175
- before(:all) do
176
- write_yaml({"environments" => {"giblets" => {"branch" => "master"}}},
177
- File.join(@local_git_dir, "ey.yml"))
149
+ before(:each) do
150
+ write_yaml({"environments" => {"giblets" => {"branch" => "master"}}})
178
151
  end
179
152
 
180
- after(:all) do
181
- File.unlink(File.join(@local_git_dir, "ey.yml"))
153
+ after(:each) do
154
+ File.unlink "ey.yml"
182
155
  end
183
156
 
184
157
  it "deploys the default branch by default" do
data/spec/ey/ey_spec.rb CHANGED
@@ -9,7 +9,7 @@ describe "ey" do
9
9
 
10
10
  context "run with an argument that is not a command" do
11
11
  it "tells the user that is not a command" do
12
- ey "foobarbaz", :hide_err => true
12
+ ey "foobarbaz", :expect_failure => true
13
13
  @err.should include("Could not find task")
14
14
  end
15
15
  end
@@ -0,0 +1,42 @@
1
+ require 'spec_helper'
2
+
3
+ describe "ey recipes apply" do
4
+ it_should_behave_like "an integration test"
5
+
6
+ before(:all) do
7
+ api_scenario "one app, one environment"
8
+ end
9
+
10
+ it "works when the environment name is valid" do
11
+ ey "recipes apply giblets", :debug => true
12
+ @out.should =~ /Uploaded recipes started for giblets/i
13
+ end
14
+
15
+ it "runs recipes for the current environment by default" do
16
+ ey "recipes apply", :debug => true
17
+ @out.should =~ /Uploaded recipes started for giblets/i
18
+ end
19
+
20
+ it "fails when the environment name is bogus" do
21
+ ey "recipes apply typo", :expect_failure => true
22
+ @err.should match(/'typo'/)
23
+ end
24
+ end
25
+
26
+ describe "ey recipes apply ENV" do
27
+ it_should_behave_like "an integration test"
28
+
29
+ before(:all) do
30
+ api_scenario "one app, many similarly-named environments"
31
+ end
32
+
33
+ it "works when given an unambiguous substring" do
34
+ ey "recipes apply prod", :debug => true
35
+ @out.should =~ /Uploaded recipes started for railsapp_production/
36
+ end
37
+
38
+ it "complains when given an ambiguous substring" do
39
+ ey "recipes apply staging", :hide_err => true, :expect_failure => true
40
+ @err.should =~ /'staging' is ambiguous/
41
+ end
42
+ end
@@ -0,0 +1,40 @@
1
+ require 'spec_helper'
2
+
3
+ describe "ey recipes upload" do
4
+ it_should_behave_like "an integration test"
5
+
6
+ define_git_repo('+cookbooks') do |git_dir|
7
+ git_dir.join("cookbooks").mkdir
8
+ File.open(git_dir.join("cookbooks/file"), "w"){|f| f << "boo" }
9
+ end
10
+
11
+ use_git_repo('+cookbooks')
12
+
13
+ it "posts the recipes to the correct url" do
14
+ api_scenario "one app, one environment"
15
+ ey "recipes upload giblets", :debug => true
16
+
17
+ @out.should =~ /Recipes uploaded successfully for giblets/i
18
+ end
19
+
20
+ it "errors correctly on bogus env name" do
21
+ api_scenario "one app, one environment"
22
+ ey "recipes upload bogusenv", :expect_failure => true
23
+
24
+ @err.should =~ /No environment named 'bogusenv'/
25
+ end
26
+
27
+ it "can infer the environment from the current application" do
28
+ api_scenario "one app, one environment"
29
+
30
+ ey "recipes upload", :debug => true
31
+ @out.should =~ /Recipes uploaded successfully for giblets/i
32
+ end
33
+
34
+ it "complains when it can't infer the environment from the current application" do
35
+ api_scenario "one app, one environment, not linked"
36
+
37
+ ey "recipes upload", :debug => true, :expect_failure => true
38
+ @err.should =~ /single environment/i
39
+ end
40
+ end
@@ -0,0 +1,50 @@
1
+ require 'spec_helper'
2
+
3
+ describe "ey rollback" do
4
+ it_should_behave_like "an integration test"
5
+
6
+ before(:all) do
7
+ api_scenario "one app, one environment"
8
+ end
9
+
10
+ it "works when the environment name is valid" do
11
+ ey "rollback giblets", :debug => true
12
+ @out.should match(/rolling back giblets/i)
13
+ @err.should be_empty
14
+ @ssh_commands.last.should match(/eysd deploy rollback --app rails232app/)
15
+ end
16
+
17
+ it "rollback the current environment by default" do
18
+ ey "rollback", :debug => true
19
+ @out.should match(/rolling back giblets/i)
20
+ @err.should be_empty
21
+ @ssh_commands.last.should match(/eysd deploy rollback --app rails232app/)
22
+ end
23
+
24
+ it "fails when the environment name is bogus" do
25
+ ey "rollback typo", :expect_failure => true
26
+ @err.should match(/'typo'/)
27
+ @ssh_commands.should be_empty
28
+ end
29
+ end
30
+
31
+ describe "ey rollback ENV" do
32
+ it_should_behave_like "an integration test"
33
+
34
+ before(:all) do
35
+ api_scenario "one app, many similarly-named environments"
36
+ end
37
+
38
+ it "works when given an unambiguous substring" do
39
+ ey "rollback prod", :debug => true
40
+ @out.should match(/Rolling back railsapp_production/i)
41
+ @err.should be_empty
42
+ @ssh_commands.last.should match(/eysd deploy rollback --app rails232app/)
43
+ end
44
+
45
+ it "complains when given an ambiguous substring" do
46
+ ey "rollback staging", :hide_err => true, :expect_failure => true
47
+ @err.should =~ /'staging' is ambiguous/
48
+ @ssh_commands.should be_empty
49
+ end
50
+ end
data/spec/ey/ssh_spec.rb CHANGED
@@ -52,12 +52,17 @@ describe "ey ssh ENV" do
52
52
  end
53
53
 
54
54
  it "works when given an unambiguous substring" do
55
- print_my_args = "#!/bin/sh\necho ssh $*"
56
-
57
- ey "ssh prod", :prepend_to_path => {'ssh' => print_my_args}
55
+ ey "ssh prod", :prepend_to_path => {'ssh' => print_my_args_ssh}
58
56
  @raw_ssh_commands.should == ["ssh turkey@174.129.198.124"]
59
57
  end
60
58
 
59
+ it "doesn't require you to be in any app's directory if the name is unambiguous" do
60
+ Dir.chdir(Dir.tmpdir) do
61
+ ey "ssh prod", :prepend_to_path => {'ssh' => print_my_args_ssh}
62
+ @raw_ssh_commands.should == ["ssh turkey@174.129.198.124"]
63
+ end
64
+ end
65
+
61
66
  it "complains when given an ambiguous substring" do
62
67
  ey "ssh staging", :hide_err => true, :expect_failure => true
63
68
  @err.should match(/'staging' is ambiguous/)
data/spec/spec_helper.rb CHANGED
@@ -29,6 +29,7 @@ support.each{|helper| require helper }
29
29
 
30
30
  Spec::Runner.configure do |config|
31
31
  config.include Spec::Helpers
32
+ config.extend Spec::GitRepo
32
33
 
33
34
  config.before(:all) do
34
35
  FakeWeb.allow_net_connect = false
@@ -58,7 +59,15 @@ Spec::Matchers.define :have_command_like do |regex|
58
59
  end
59
60
  end
60
61
 
62
+ EY.define_git_repo("default") do |git_dir|
63
+ system("echo 'source :gemcutter' > Gemfile")
64
+ system("git add Gemfile")
65
+ system("git commit -m 'initial commit' >/dev/null 2>&1")
66
+ end
67
+
61
68
  shared_examples_for "an integration test without an eyrc file" do
69
+ use_git_repo('default')
70
+
62
71
  before(:all) do
63
72
  FakeFS.deactivate!
64
73
  ENV['EYRC'] = "/tmp/eyrc"
@@ -55,11 +55,28 @@ class FakeAwsm < Sinatra::Base
55
55
  end
56
56
 
57
57
  post "/api/v2/environments/:env_id/recipes" do
58
- {}.to_json
58
+ if params[:file][:tempfile]
59
+ files = `tar --list -z -f "#{params[:file][:tempfile].path}"`.split(/\n/)
60
+ if files.empty?
61
+ status(400)
62
+ "No files in uploaded tarball"
63
+ else
64
+ status(204)
65
+ ""
66
+ end
67
+ else
68
+ status(400)
69
+ "Recipe file not uploaded"
70
+ end
59
71
  end
60
72
 
61
73
  put "/api/v2/environments/:env_id/rebuild" do
62
- status(204)
74
+ status(202)
75
+ ""
76
+ end
77
+
78
+ put "/api/v2/environments/:env_id/run_custom_recipes" do
79
+ status(202)
63
80
  ""
64
81
  end
65
82
 
@@ -0,0 +1,21 @@
1
+ module Spec
2
+ module GitRepo
3
+ def define_git_repo(name, &setup)
4
+ # EY's ivars don't get cleared between examples, so we can keep
5
+ # a git repo around longer (and thus make our tests faster)
6
+ EY.define_git_repo(name, &setup)
7
+ end
8
+
9
+ def use_git_repo(repo_name)
10
+ before(:each) do
11
+ @_original_wd ||= []
12
+ @_original_wd << Dir.getwd
13
+ Dir.chdir(EY.git_repo_dir(repo_name))
14
+ end
15
+
16
+ after(:each) do
17
+ Dir.chdir(@_original_wd.pop)
18
+ end
19
+ end
20
+ end
21
+ end
@@ -17,7 +17,6 @@ module Spec
17
17
  ZeroExitStatus = Class.new(UnexpectedExit)
18
18
 
19
19
  def ey(cmd = nil, options = {}, &block)
20
- require "open3"
21
20
  hide_err = options.has_key?(:hide_err) ? options[:hide_err] : options[:expect_failure]
22
21
  path_prepends = options[:prepend_to_path]
23
22
 
@@ -75,23 +74,11 @@ module Spec
75
74
  @out
76
75
  end
77
76
 
78
- def api_scenario(scenario, remote = local_git_remote)
77
+ def api_scenario(scenario, remote = "user@git.host:path/to/repo.git")
79
78
  response = ::RestClient.put(EY.fake_awsm + '/scenario', {"scenario" => scenario, "remote" => remote}, {})
80
79
  raise "Setting scenario failed: #{response.inspect}" unless response.code == 200
81
80
  end
82
81
 
83
- def local_git_remote
84
- remotes = []
85
- `git remote -v`.each_line do |line|
86
- parts = line.split(/\t/)
87
- # the remote will look like
88
- # "git@github.com:engineyard/engineyard.git (fetch)\n"
89
- # so we need to chop it up a bit
90
- remotes << parts[1].gsub(/\s.*$/, "") if parts[1]
91
- end
92
- remotes.first
93
- end
94
-
95
82
  def read_yaml(file="ey.yml")
96
83
  YAML.load_file(File.expand_path(file))
97
84
  end
@@ -137,5 +124,29 @@ module EY
137
124
  end
138
125
  end
139
126
  alias_method :start_fake_awsm, :fake_awsm
127
+
128
+ def define_git_repo(name, &setup)
129
+ @git_repo_setup ||= {}
130
+ raise "Attempted to redefine git repo #{name}; don't do that!" if @git_repo_setup.has_key?(name)
131
+ @git_repo_setup[name] = setup
132
+ end
133
+
134
+ def git_repo_dir(name)
135
+ @git_repo_dir_cache ||= {}
136
+ return @git_repo_dir_cache[name] if @git_repo_dir_cache.has_key?(name)
137
+ raise ArgumentError, "No definition for git repo #{name}" unless @git_repo_setup[name]
138
+
139
+ git_dir = Pathname.new("/tmp/engineyard_test_repo_#{Time.now.tv_sec}_#{Time.now.tv_usec}_#{$$}")
140
+ git_dir.mkdir
141
+ Dir.chdir(git_dir) do
142
+ system("git init -q")
143
+ system('git config user.email ey@spec.test')
144
+ system('git config user.name "EY Specs"')
145
+ system("git remote add testremote user@git.host:path/to/repo.git")
146
+ @git_repo_setup[name].call(git_dir)
147
+ end
148
+ @git_repo_dir_cache[name] = git_dir
149
+ end
150
+
140
151
  end
141
152
  end
metadata CHANGED
@@ -4,9 +4,9 @@ version: !ruby/object:Gem::Version
4
4
  prerelease: false
5
5
  segments:
6
6
  - 0
7
- - 3
8
- - 3
9
- version: 0.3.3
7
+ - 4
8
+ - 0
9
+ version: 0.4.0
10
10
  platform: ruby
11
11
  authors:
12
12
  - EY Cloud Team
@@ -14,7 +14,7 @@ autorequire:
14
14
  bindir: bin
15
15
  cert_chain: []
16
16
 
17
- date: 2010-06-03 00:00:00 -07:00
17
+ date: 2010-06-11 00:00:00 -07:00
18
18
  default_executable: ey
19
19
  dependencies:
20
20
  - !ruby/object:Gem::Dependency
@@ -109,7 +109,6 @@ extra_rdoc_files: []
109
109
  files:
110
110
  - bin/ey
111
111
  - lib/engineyard/api.rb
112
- - lib/engineyard/cli/action/deploy.rb
113
112
  - lib/engineyard/cli/api.rb
114
113
  - lib/engineyard/cli/recipes.rb
115
114
  - lib/engineyard/cli/thor_fixes.rb
@@ -177,10 +176,13 @@ test_files:
177
176
  - spec/ey/list_environments_spec.rb
178
177
  - spec/ey/logs_spec.rb
179
178
  - spec/ey/rebuild_spec.rb
179
+ - spec/ey/recipes/apply_spec.rb
180
+ - spec/ey/recipes/upload_spec.rb
181
+ - spec/ey/rollback_spec.rb
180
182
  - spec/ey/ssh_spec.rb
181
- - spec/ey/upload_recipes_spec.rb
182
183
  - spec/spec_helper.rb
183
184
  - spec/support/bundled_ey
184
185
  - spec/support/fake_awsm.ru
186
+ - spec/support/git_repo.rb
185
187
  - spec/support/helpers.rb
186
188
  - spec/support/ruby_ext.rb
@@ -1,120 +0,0 @@
1
- module EY
2
- class CLI
3
- module Action
4
- class Deploy
5
-
6
- EYSD_VERSION = "~>0.3.0"
7
-
8
- def self.call(env_name, branch, options)
9
- env_name ||= EY.config.default_environment
10
-
11
- app = fetch_app
12
- env = fetch_environment(env_name, app)
13
- branch = fetch_branch(env.name, branch, options[:force])
14
- master = fetch_app_master(env)
15
-
16
- EY.ui.info "Connecting to the server..."
17
- ensure_eysd_present(master, options[:install_eysd])
18
-
19
- EY.ui.info "Running deploy for '#{env.name}' on server..."
20
- deployed = master.deploy!(app, branch, options[:migrate], env.config)
21
-
22
- if deployed
23
- EY.ui.info "Deploy complete"
24
- else
25
- raise EY::Error, "Deploy failed"
26
- end
27
- end
28
-
29
- private
30
-
31
- def self.fetch_app_master(env)
32
- master = env.app_master
33
-
34
- if !master
35
- raise EnvironmentError, "No running instances for environment #{env.name}\nStart one at #{EY.config.endpoint}"
36
- elsif master.status != "running"
37
- raise EnvironmentError, "Cannot deploy: application master's status is not \"running\" (green); it is \"#{master.status}\"."
38
- end
39
-
40
- master
41
- end
42
-
43
- def self.api
44
- @api ||= EY::CLI::API.new
45
- end
46
-
47
- def self.repo
48
- @repo ||= EY::Repo.new
49
- end
50
-
51
- def self.fetch_app
52
- app = api.app_for_repo(repo)
53
- raise NoAppError.new(repo) unless app
54
- app
55
- end
56
-
57
- def self.fetch_environment(env_name, app)
58
- # if the name's not specified and there's not exactly one
59
- # environment, we can't figure out which environment to deploy
60
- raise DeployArgumentError if !env_name && app.environments.size != 1
61
-
62
- env = if env_name
63
- app.environments.match_one(env_name)
64
- else
65
- app.environments.first
66
- end
67
-
68
- # the environment exists, but doesn't have this app
69
- if !env && api.environments.named(env_name)
70
- raise EnvironmentError, "Environment '#{env_name}' doesn't run this application\nYou can add it at #{EY.config.endpoint}"
71
- end
72
-
73
- if !env
74
- raise NoEnvironmentError.new(env_name)
75
- end
76
-
77
- env
78
- end
79
-
80
- def self.fetch_branch(env_name, user_specified_branch, force)
81
- default_branch = EY.config.default_branch(env_name)
82
-
83
- branch = if user_specified_branch
84
- if default_branch && (user_specified_branch != default_branch) && !force
85
- raise BranchMismatch.new(default_branch, user_specified_branch)
86
- end
87
- user_specified_branch
88
- else
89
- default_branch || repo.current_branch
90
- end
91
-
92
- raise DeployArgumentError unless branch
93
- branch
94
- end
95
-
96
- def self.ensure_eysd_present(instance, install_eysd)
97
- eysd_status = instance.ey_deploy_check
98
- case eysd_status
99
- when :ssh_failed
100
- raise EnvironmentError, "SSH connection to #{instance.hostname} failed"
101
- when :eysd_missing
102
- EY.ui.warn "Instance does not have server-side component installed"
103
- EY.ui.info "Installing server-side component..."
104
- instance.install_ey_deploy!
105
- when :too_new
106
- raise EnvironmentError, "server-side component too new; please upgrade your copy of the engineyard gem."
107
- when :too_old
108
- EY.ui.info "Upgrading server-side component..."
109
- instance.upgrade_ey_deploy!
110
- when :ok
111
- # no action needed
112
- else
113
- raise EY::Error, "Internal error: Unexpected status from Instance#ey_deploy_check; got #{eysd_status.inspect}"
114
- end
115
- end
116
-
117
- end
118
- end
119
- end
120
- end
@@ -1,54 +0,0 @@
1
- require 'spec_helper'
2
-
3
- describe "ey recipes upload" do
4
- it_should_behave_like "an integration test"
5
-
6
- before(:all) do
7
- @recipe_dir = Pathname.new("/tmp/#{$$}")
8
- @recipe_dir.mkdir
9
- Dir.chdir(@recipe_dir) do
10
- @recipe_dir.join("cookbooks").mkdir
11
- File.open(@recipe_dir.join("cookbooks/file"), "w"){|f| f << "boo" }
12
- `git init`
13
- `git add .`
14
- `git commit -m "OMG"`
15
- `git remote add testremote user@host.tld:path/to/repo.git`
16
- end
17
- end
18
-
19
- it "posts the recipes to the correct url" do
20
- api_scenario "one app, one environment"
21
- Dir.chdir(@recipe_dir) do
22
- ey "recipes upload giblets", :debug => true
23
- end
24
-
25
- @out.should =~ /recipes uploaded successfully/i
26
- end
27
-
28
- it "errors correctly on bogus env name" do
29
- api_scenario "one app, one environment"
30
- ey "recipes upload bogusenv", :expect_failure => true
31
-
32
- @err.should =~ /can't be found/i
33
- end
34
-
35
- it "can infer the environment from the current application" do
36
- api_scenario "one app, one environment", "user@host.tld:path/to/repo.git"
37
-
38
- Dir.chdir(@recipe_dir) do
39
- ey "recipes upload", :debug => true
40
- end
41
-
42
- @out.should =~ /recipes uploaded successfully/i
43
- end
44
-
45
- it "complains when it can't infer the environment from the current application" do
46
- api_scenario "one app, one environment, not linked", "user@host.tld:path/to/repo.git"
47
-
48
- Dir.chdir(@recipe_dir) do
49
- ey "recipes upload", :debug => true, :expect_failure => true
50
- end
51
-
52
- @err.should =~ /single environment/i
53
- end
54
- end