engineyard 0.3.3 → 0.4.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.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