engineyard 1.4.29 → 1.7.0.pre2

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.
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