engineyard 1.4.29 → 1.7.0.pre2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (68) hide show
  1. data/README.rdoc +139 -4
  2. data/bin/ey +1 -7
  3. data/lib/engineyard.rb +1 -22
  4. data/lib/engineyard/cli.rb +192 -94
  5. data/lib/engineyard/cli/#recipes.rb# +32 -0
  6. data/lib/engineyard/cli/api.rb +42 -28
  7. data/lib/engineyard/cli/recipes.rb +13 -6
  8. data/lib/engineyard/cli/ui.rb +103 -42
  9. data/lib/engineyard/cli/web.rb +16 -10
  10. data/lib/engineyard/config.rb +92 -18
  11. data/lib/engineyard/deploy_config.rb +66 -0
  12. data/lib/engineyard/deploy_config/migrate.rb +125 -0
  13. data/lib/engineyard/deploy_config/ref.rb +56 -0
  14. data/lib/engineyard/error.rb +38 -78
  15. data/lib/engineyard/repo.rb +75 -27
  16. data/lib/engineyard/serverside_runner.rb +133 -0
  17. data/lib/engineyard/thor.rb +110 -18
  18. data/lib/engineyard/version.rb +1 -1
  19. data/spec/engineyard/cli/api_spec.rb +10 -16
  20. data/spec/engineyard/cli_spec.rb +0 -11
  21. data/spec/engineyard/config_spec.rb +1 -8
  22. data/spec/engineyard/deploy_config_spec.rb +203 -0
  23. data/spec/engineyard/eyrc_spec.rb +2 -0
  24. data/spec/engineyard/repo_spec.rb +57 -34
  25. data/spec/ey/deploy_spec.rb +102 -52
  26. data/spec/ey/list_environments_spec.rb +69 -14
  27. data/spec/ey/login_spec.rb +11 -7
  28. data/spec/ey/logout_spec.rb +4 -4
  29. data/spec/ey/logs_spec.rb +6 -6
  30. data/spec/ey/recipes/apply_spec.rb +1 -1
  31. data/spec/ey/recipes/download_spec.rb +1 -1
  32. data/spec/ey/recipes/upload_spec.rb +6 -6
  33. data/spec/ey/rollback_spec.rb +3 -3
  34. data/spec/ey/ssh_spec.rb +9 -9
  35. data/spec/ey/status_spec.rb +2 -2
  36. data/spec/ey/whoami_spec.rb +9 -8
  37. data/spec/spec_helper.rb +18 -15
  38. data/spec/support/{fake_awsm.rb → git_repos.rb} +0 -14
  39. data/spec/support/helpers.rb +84 -28
  40. data/spec/support/matchers.rb +0 -16
  41. data/spec/support/shared_behavior.rb +83 -103
  42. metadata +65 -51
  43. data/lib/engineyard/api.rb +0 -117
  44. data/lib/engineyard/collection.rb +0 -7
  45. data/lib/engineyard/collection/abstract.rb +0 -71
  46. data/lib/engineyard/collection/apps.rb +0 -8
  47. data/lib/engineyard/collection/environments.rb +0 -8
  48. data/lib/engineyard/model.rb +0 -12
  49. data/lib/engineyard/model/account.rb +0 -8
  50. data/lib/engineyard/model/api_struct.rb +0 -33
  51. data/lib/engineyard/model/app.rb +0 -32
  52. data/lib/engineyard/model/deployment.rb +0 -90
  53. data/lib/engineyard/model/environment.rb +0 -194
  54. data/lib/engineyard/model/instance.rb +0 -166
  55. data/lib/engineyard/model/log.rb +0 -9
  56. data/lib/engineyard/model/user.rb +0 -6
  57. data/lib/engineyard/resolver.rb +0 -134
  58. data/lib/engineyard/rest_client_ext.rb +0 -9
  59. data/lib/engineyard/ruby_ext.rb +0 -9
  60. data/spec/engineyard/api_spec.rb +0 -39
  61. data/spec/engineyard/collection/apps_spec.rb +0 -16
  62. data/spec/engineyard/collection/environments_spec.rb +0 -16
  63. data/spec/engineyard/model/api_struct_spec.rb +0 -41
  64. data/spec/engineyard/model/environment_spec.rb +0 -198
  65. data/spec/engineyard/model/instance_spec.rb +0 -27
  66. data/spec/engineyard/resolver_spec.rb +0 -112
  67. data/spec/support/fake_awsm.ru +0 -245
  68. data/spec/support/scenarios.rb +0 -417
@@ -0,0 +1,66 @@
1
+ module EY
2
+ class DeployConfig
3
+ def initialize(cli_opts, env_config, repo, ui)
4
+ @cli_opts = cli_opts
5
+ @env_config = env_config
6
+ @repo = repo
7
+ @ui = ui
8
+ end
9
+
10
+ def ref
11
+ @ref ||= decide_ref
12
+ end
13
+
14
+ def migrate
15
+ decide_migrate
16
+ @migrate
17
+ end
18
+
19
+ def migrate_command
20
+ decide_migrate
21
+ @migrate_command
22
+ end
23
+
24
+ def verbose
25
+ @cli_opts.fetch('verbose') { in_repo? && @env_config.verbose }
26
+ end
27
+
28
+ def extra_config
29
+ extras = @cli_opts.fetch('extra_deploy_hook_options', {})
30
+ if in_repo?
31
+ extras = @env_config.merge(extras)
32
+ end
33
+ extras
34
+ end
35
+
36
+ private
37
+
38
+ # passing an app means we assume PWD is not the app.
39
+ def in_repo?
40
+ @cli_opts['app'].nil? || @cli_opts['app'] == ''
41
+ end
42
+
43
+ def decide_ref
44
+ ref_decider = EY::DeployConfig::Ref.new(@cli_opts, @env_config, @repo, @ui)
45
+ if in_repo?
46
+ ref_decider.when_inside_repo
47
+ else
48
+ ref_decider.when_outside_repo
49
+ end
50
+ end
51
+
52
+ def decide_migrate
53
+ return if @migrate_decider
54
+ @migrate_decider = EY::DeployConfig::Migrate.new(@cli_opts, @env_config, @ui)
55
+ @migrate, @migrate_command =
56
+ if in_repo?
57
+ @migrate_decider.when_inside_repo
58
+ else
59
+ @migrate_decider.when_outside_repo
60
+ end
61
+ end
62
+ end
63
+ end
64
+
65
+ require 'engineyard/deploy_config/migrate'
66
+ require 'engineyard/deploy_config/ref'
@@ -0,0 +1,125 @@
1
+ module EY
2
+ class DeployConfig
3
+ class Migrate
4
+
5
+ DEFAULT = 'rake db:migrate'
6
+
7
+ def initialize(cli_opts, env_config, ui)
8
+ @cli_opts = cli_opts
9
+ @env_config = env_config
10
+ @ui = ui
11
+
12
+ @perform = nil
13
+ @command = nil
14
+ end
15
+
16
+ # Returns an array of [perform_migration, migrate_command] on success.
17
+ # Yields the block if no migrate options are set.
18
+ def when_outside_repo
19
+ if perform_from_cli_opts
20
+ if @perform
21
+ @command ||= command_from_opts || DEFAULT
22
+ else
23
+ @command = nil
24
+ end
25
+ [@perform, @command]
26
+ else
27
+ raise RefAndMigrateRequiredOutsideRepo.new(@cli_opts)
28
+ end
29
+ end
30
+
31
+ # Returns an array of [perform_migration, migrate_command] on success.
32
+ # Should always return successfully.
33
+ def when_inside_repo
34
+ if perform_from_cli_opts || perform_from_config || perform_from_interaction
35
+ if @perform
36
+ @command ||= command_from_opts || command_from_config || DEFAULT
37
+ else
38
+ @command = nil
39
+ end
40
+ [@perform, @command]
41
+ else
42
+ raise MigrateRequired.new(@cli_opts)
43
+ end
44
+ end
45
+
46
+ private
47
+
48
+ attr_reader :cli_opts, :env_config, :ui
49
+
50
+ def command_from_opts
51
+ cli_migrate = cli_opts.fetch('migrate', nil)
52
+ cli_migrate.respond_to?(:to_str) && cli_migrate.to_str
53
+ end
54
+
55
+ def command_from_config
56
+ env_config.migrate_command
57
+ end
58
+
59
+ def perform_from_cli_opts
60
+ @perform = !!cli_opts.fetch('migrate') { return false } # yields on not found
61
+ true
62
+ end
63
+
64
+ def perform_from_config
65
+ @perform = !!env_config.migrate { return perform_implied_via_command_in_config }
66
+ if @perform
67
+ unless command_from_config
68
+ env_config.migration_command = DEFAULT
69
+ end
70
+ end
71
+ true
72
+ end
73
+
74
+ # if the command is set in ey.yml and perform isn't explicitly turned off,
75
+ # then we'll write out the old default of migrating always, since that's
76
+ # probably what is expected.
77
+ def perform_implied_via_command_in_config
78
+ if @perfom.nil? && @command = command_from_config
79
+ @perform = true
80
+ env_config.migrate = @perform
81
+ ui.warn "********************************************************************************"
82
+ ui.info "#{env_config.path} config for #{env_config.name} has been updated to"
83
+ ui.info "migrate by default to maintain previous expected default behavior."
84
+ ui.warn "********************************************************************************"
85
+ ui.say "It's a good idea to git commit #{env_config.path} with these new changes."
86
+ true
87
+ else
88
+ false
89
+ end
90
+ end
91
+
92
+ def perform_from_interaction
93
+ ui.warn "********************************************************************************"
94
+ ui.warn "No default migrate choice for environment: #{env_config.name}"
95
+ ui.warn "Migrate can be toggled per-deploy using --migrate or --no-migrate."
96
+ ui.warn "Let's set a default migration choice."
97
+ ui.warn "********************************************************************************"
98
+ @perform = ui.agree('Migrate every deploy by default? ', true)
99
+ env_config.migrate = @perform
100
+ if @perform
101
+ command_from_interaction
102
+ end
103
+ ui.say "#{env_config.path}: migrate settings saved for #{env_config.name}."
104
+ ui.say "It's a good idea to git commit #{env_config.path} with these new changes."
105
+ true
106
+ rescue Timeout::Error
107
+ @perform = nil
108
+ @command = nil
109
+ ui.error "Timeout when waiting for input. This is not a terminal."
110
+ ui.error "ey deploy no longer migrates when no default is set in ey.yml."
111
+ ui.error "Run interactively for step-by-step ey.yml migration setup."
112
+ return false
113
+ end
114
+
115
+ # only interactively request a command if we interactively requested the perform setting.
116
+ # don't call this outside of the interactive setting (otherwise, why even have a default?)
117
+ def command_from_interaction
118
+ default = env_config.migration_command || DEFAULT
119
+ @command = ui.ask("Migration command? ", false, default)
120
+ env_config.migration_command = @command
121
+ @command
122
+ end
123
+ end
124
+ end
125
+ end
@@ -0,0 +1,56 @@
1
+ require 'engineyard/error'
2
+
3
+ module EY
4
+ class DeployConfig
5
+ class Ref
6
+
7
+ def initialize(cli_opts, env_config, repo, ui)
8
+ @cli_opts = cli_opts
9
+ @default = env_config.branch
10
+ @repo = repo
11
+ @force_ref = @cli_opts.fetch('force_ref', false)
12
+ @ui = ui
13
+
14
+ if @force_ref.kind_of?(String)
15
+ @ref, @force_ref = @force_ref, true
16
+ else
17
+ @ref = @cli_opts.fetch('ref', nil)
18
+ @ref = nil if @ref == ''
19
+ end
20
+ end
21
+
22
+ def when_inside_repo
23
+ if !@force_ref && @ref && @default && @ref != @default
24
+ raise BranchMismatchError.new(@default, @ref)
25
+ elsif @force_ref && @ref && @default
26
+ @ui.say "Default ref overridden with #{@ref.inspect}."
27
+ end
28
+
29
+ @ref || use_default || use_current_branch || raise(RefRequired.new(@cli_opts))
30
+ end
31
+
32
+ def use_default
33
+ if @default
34
+ @ui.say "Using default branch #{@default.inspect} from ey.yml."
35
+ @default
36
+ end
37
+ end
38
+
39
+ def use_current_branch
40
+ if current = @repo.current_branch
41
+ @ui.say "Using current HEAD branch #{current.inspect}."
42
+ current
43
+ end
44
+ end
45
+
46
+ # a.k.a. not in the correct repo
47
+ #
48
+ # returns the ref if it was passed in the cli opts.
49
+ # or raise
50
+ def when_outside_repo
51
+ @ref or raise RefAndMigrateRequiredOutsideRepo.new(@cli_opts)
52
+ end
53
+
54
+ end
55
+ end
56
+ end
@@ -1,78 +1,24 @@
1
1
  module EY
2
2
  class Error < RuntimeError
3
- def ambiguous(type, name, matches, desc="")
4
- pretty_names = matches.map {|x| "'#{x}'"}.join(', ')
5
- "The name '#{name}' is ambiguous; it matches all of the following #{type} names: #{pretty_names}.\n" +
6
- "Please use a longer, unambiguous substring or the entire #{type} name." + desc
7
- end
8
3
  end
9
4
 
10
- class ResolveError < EY::Error; end
11
- class NoMatchesError < ResolveError; end
12
- class MultipleMatchesError < ResolveError; end
13
-
14
5
  class NoCommandError < EY::Error
15
6
  def initialize
16
7
  super "Must specify a command to run via ssh"
17
8
  end
18
9
  end
19
10
 
20
- class NoRemotesError < EY::Error
21
- def initialize(path)
22
- super "fatal: No git remotes found in #{path}"
23
- end
24
- end
25
-
26
- class NoAppError < Error
27
- def initialize(repo)
28
- super <<-ERROR
29
- There is no application configured for any of the following remotes:
30
- \t#{repo ? repo.urls.join("\n\t") : "No remotes found."}
31
- You can add this application at #{EY.config.endpoint}
32
- ERROR
33
- end
34
- end
35
-
36
- class InvalidAppError < Error
37
- def initialize(name)
38
- super %|There is no app configured with the name "#{name}"|
39
- end
40
- end
41
-
42
- class AmbiguousAppNameError < EY::Error
43
- def initialize(name, matches, desc="")
44
- super ambiguous("app", name, matches, desc)
45
- end
46
- end
47
-
48
- class NoAppMasterError < EY::Error
49
- def initialize(env_name)
50
- super "The environment '#{env_name}' does not have a master instance."
51
- end
52
- end
53
-
54
11
  class NoInstancesError < EY::Error
55
12
  def initialize(env_name)
56
13
  super "The environment '#{env_name}' does not have any matching instances."
57
14
  end
58
15
  end
59
16
 
60
- class BadAppMasterStatusError < EY::Error
61
- def initialize(master_status)
62
- super "Application master's status is not \"running\" (green); it is \"#{master_status}\"."
63
- end
64
- end
65
-
66
- class EnvironmentError < EY::Error
67
- end
17
+ class ResolverError < Error; end
18
+ class NoMatchesError < ResolverError; end
19
+ class MultipleMatchesError < ResolverError; end
68
20
 
69
- class AmbiguousEnvironmentNameError < EY::EnvironmentError
70
- def initialize(name, matches, desc="")
71
- super ambiguous("environment", name, matches, desc)
72
- end
73
- end
74
-
75
- class AmbiguousEnvironmentGitUriError < EY::EnvironmentError
21
+ class AmbiguousEnvironmentGitUriError < ResolverError
76
22
  def initialize(environments)
77
23
  message = "The repository url in this directory is ambiguous.\n"
78
24
  message << "Please use -e <envname> to specify one of the following environments:\n"
@@ -87,36 +33,50 @@ You can add this application at #{EY.config.endpoint}
87
33
  end
88
34
  end
89
35
 
90
- class NoSingleEnvironmentError < EY::EnvironmentError
91
- def initialize(app)
92
- size = app.environments.size
93
- super "Unable to determine a single environment for the current application (found #{size} environments)"
94
- end
95
- end
96
36
 
97
- class NoEnvironmentError < EY::EnvironmentError
98
- def initialize(env_name=nil)
99
- super "No environment named '#{env_name}'\nYou can create one at #{EY.config.endpoint}"
37
+ class DeployArgumentError < EY::Error; end
38
+ class BranchMismatchError < DeployArgumentError
39
+ def initialize(default, ref)
40
+ super <<-ERR
41
+ Your default branch is set to #{default.inspect} in ey.yml.
42
+ To deploy #{ref.inspect} you can:
43
+ * Delete the line 'branch: #{default}' in ey.yml
44
+ OR
45
+ * Use the -R [REF] or --force-ref [REF] options as follows:
46
+ Usage: ey deploy -R #{ref}
47
+ ey deploy --force-ref #{ref}
48
+ ERR
100
49
  end
101
50
  end
102
51
 
103
- class EnvironmentUnlinkedError < EY::Error
104
- def initialize(env_name)
105
- super "Environment '#{env_name}' exists but does not run this application."
52
+ class RefAndMigrateRequiredOutsideRepo < DeployArgumentError
53
+ def initialize(options)
54
+ super <<-ERR
55
+ Because defaults are stored in a file in your application dir, when specifying
56
+ --app you must also specify the --ref and the --migrate or --no-migrate options.
57
+ Usage: ey deploy --app #{options[:app]} --ref [ref] --migrate [COMMAND]
58
+ ey deploy --app #{options[:app]} --ref [branch] --no-migrate
59
+ ERR
106
60
  end
107
61
  end
108
62
 
109
- class BranchMismatchError < EY::Error
110
- def initialize(default_branch, branch)
111
- super(%|Your deploy branch is set to "#{default_branch}".\n| +
112
- %|If you want to deploy branch "#{branch}", use --ignore-default-branch.|)
63
+ class RefRequired < DeployArgumentError
64
+ def initialize(options)
65
+ super <<-ERR
66
+ Unable to determine the branch or ref to deploy
67
+ Usage: ey deploy --ref [ref]
68
+ ERR
113
69
  end
114
70
  end
115
71
 
116
- class DeployArgumentError < EY::Error
117
- def initialize
118
- super(%("deploy" was called incorrectly. Call as "deploy [--environment <env>] [--ref <branch|tag|ref>]"\n) +
119
- %|You can set default environments and branches in ey.yml|)
72
+ class MigrateRequired < DeployArgumentError
73
+ def initialize(options)
74
+ super <<-ERR
75
+ Unable to determine migration choice. ey deploy no longer migrates by default.
76
+ Usage: ey deploy --migrate
77
+ ey deploy --no-migrate
78
+ ERR
120
79
  end
121
80
  end
81
+
122
82
  end
@@ -1,55 +1,103 @@
1
1
  require 'engineyard/error'
2
- require 'escape'
3
2
  require 'pathname'
4
3
 
5
4
  module EY
6
5
  class Repo
6
+ class NotAGitRepository < EY::Error
7
+ attr_reader :dir
8
+ def initialize(output)
9
+ @dir = File.expand_path(ENV['GIT_DIR'] || ENV['GIT_WORK_TREE'] || '.')
10
+ super("#{output} (#{@dir})")
11
+ end
12
+ end
7
13
 
8
- attr_reader :path
14
+ class NoRemotesError < EY::Error
15
+ def initialize(path)
16
+ super "fatal: No git remotes found in #{path}"
17
+ end
18
+ end
9
19
 
10
- def initialize(repo_path='.')
11
- self.path = repo_path
20
+ def self.exist?
21
+ system("git rev-parse --git-dir > /dev/null 2>&1")
12
22
  end
13
23
 
14
- def path=(new_path)
15
- @path = Pathname.new(new_path).expand_path
24
+ attr_reader :root
25
+
26
+ # $GIT_DIR is what git uses to override the location of the .git dir.
27
+ # $GIT_WORK_TREE is the working tree for git, which we'll use after $GIT_DIR.
28
+ #
29
+ # We use this to specify which repo we should look at, since it would also
30
+ # specify where any git commands are directed, thus fooling commands we
31
+ # run anyway.
32
+ def initialize
16
33
  end
17
34
 
18
- def exist?
19
- dotgit.directory?
35
+ def root
36
+ @root ||= begin
37
+ out = `git rev-parse --show-toplevel 2>&1`.strip
38
+
39
+ if $?.success? && !out.empty?
40
+ Pathname.new(out)
41
+ else
42
+ raise EY::Repo::NotAGitRepository.new(out)
43
+ end
44
+ end
20
45
  end
21
46
 
22
- def current_branch
23
- if exist? && (head = dotgit("HEAD").read.chomp) && head.gsub!("ref: refs/heads/", "")
24
- head
25
- else
26
- nil
27
- end
47
+ def ensure_repository!
48
+ root
28
49
  end
29
50
 
30
- def urls
31
- @urls ||= config('remote.*.url').map { |c| c.split.last }
51
+ def has_committed_file?(file)
52
+ ensure_repository!
53
+ `git ls-files --full-name #{file}`.strip == file && $?.success?
32
54
  end
33
55
 
34
- def has_remote?(repository_uri)
35
- urls.include?(repository_uri)
56
+ def has_file?(file)
57
+ ensure_repository!
58
+ has_committed_file?(file) || root.join(file).exist?
36
59
  end
37
60
 
38
- def fail_on_no_remotes!
39
- if urls.empty?
40
- raise NoRemotesError.new(path)
61
+ # Read the committed version at HEAD (or ref) of a file using the git working tree relative filename.
62
+ # If the file is not committed, but does exist, a warning will be displayed
63
+ # and the file will be read anyway.
64
+ # If the file does not exist, returns nil.
65
+ #
66
+ # Example:
67
+ #
68
+ # read_file('config/ey.yml') # will read $GIT_WORK_TREE/config/ey.yml
69
+ #
70
+ def read_file(file, ref = 'HEAD')
71
+ ensure_repository!
72
+ if has_committed_file?(file)
73
+ # TODO warn if there are unstaged changes.
74
+ `git show #{ref}:#{file}`
75
+ else
76
+ EY.ui.warn <<-WARN
77
+ Warn: #{file} is not committed to this git repository:
78
+ \t#{root}
79
+ This can prevent ey deploy from loading this file for certain server side
80
+ deploy-time operations. Commit this file to fix this warning.
81
+ WARN
82
+ root.join(file).read
41
83
  end
42
84
  end
43
85
 
44
- private
86
+ def current_branch
87
+ ensure_repository!
88
+ branch = `git symbolic-ref -q HEAD`.chomp.gsub("refs/heads/", "")
89
+ branch.empty? ? nil : branch
90
+ end
45
91
 
46
- def dotgit(child='')
47
- path.join('.git', child)
92
+ def remotes
93
+ ensure_repository!
94
+ @remotes ||= `git remote -v`.scan(/\t[^\s]+\s/).map { |c| c.strip }.uniq
48
95
  end
49
96
 
50
- def config(pattern)
51
- config_file = Escape.shell_command([dotgit('config').to_s])
52
- `git config -f #{config_file} --get-regexp '#{pattern}'`.split(/\n/)
97
+ def fail_on_no_remotes!
98
+ if remotes.empty?
99
+ raise EY::Repo::NoRemotesError.new(root)
100
+ end
53
101
  end
54
102
 
55
103
  end # Repo