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,32 @@
1
+ module EY
2
+ class CLI
3
+ class Recipes < EY::Thor
4
+ X1gx1GGG desc "recipes apply [ENVIRONMENT]", <<-DESC
5
+ Run uploaded chef recipes on specified environment.
6
+
7
+ This is similar to '#{banner_base} rebuild' except Engine Yard's main
8
+ configuration step is skipped.
9
+ DESC
10
+
11
+ def apply(name = nil)
12
+ environment = fetch_environment(name)
13
+ environment.run_custom_recipes
14
+ EY.ui.say "Uploaded recipes started for #{environment.name}"
15
+ end
16
+
17
+ desc "recipes upload [ENVIRONMENT]", <<-DESC
18
+ Upload custom chef recipes to specified environment.
19
+
20
+ The current directory should contain a subdirectory named "cookbooks" to be
21
+ uploaded.
22
+ DESC
23
+
24
+ def upload(name = nil)
25
+ environment = fetch_environment(name)
26
+ environment.upload_recipes
27
+ EY.ui.say "Recipes uploaded successfully for #{environment.name}"
28
+ end
29
+ end
30
+ end
31
+
32
+ end
@@ -1,44 +1,58 @@
1
1
  require 'highline'
2
- require 'engineyard/api'
2
+ require 'engineyard-cloud-client'
3
+ require 'engineyard/eyrc'
3
4
 
4
5
  module EY
5
6
  class CLI
6
- class API < EY::API
7
-
8
- def initialize(token = nil)
9
- @token = token
10
- if ENV['ENGINEYARD_API_TOKEN']
11
- @token = ENV['ENGINEYARD_API_TOKEN']
7
+ class API
8
+ def self.authenticate(ui)
9
+ ui.info("We need to fetch your API token; please log in.")
10
+ begin
11
+ email = ui.ask("Email: ")
12
+ password = ui.ask("Password: ", true)
13
+ token = EY::CloudClient.authenticate(email, password, ui)
14
+ EY::EYRC.load.api_token = token
15
+ token
16
+ rescue EY::CloudClient::InvalidCredentials
17
+ ui.warn "Invalid username or password; please try again."
18
+ retry
12
19
  end
13
- @token ||= EY::EYRC.load.api_token
14
- @token ||= self.class.fetch_token
15
- raise EY::Error, "Sorry, we couldn't get your API token." unless @token
16
20
  end
17
21
 
18
- def request(*)
19
- begin
20
- super
21
- rescue EY::API::InvalidCredentials
22
- EY.ui.warn "Credentials rejected; please authenticate again."
23
- refresh
24
- retry
22
+ attr_reader :token
23
+
24
+ def initialize(endpoint, ui)
25
+ @ui = ui
26
+ EY::CloudClient.endpoint = endpoint
27
+
28
+ @token = ENV['ENGINEYARD_API_TOKEN'] if ENV['ENGINEYARD_API_TOKEN']
29
+ @token ||= EY::EYRC.load.api_token
30
+ @token ||= self.class.authenticate(ui)
31
+
32
+ unless @token
33
+ raise EY::Error, "Sorry, we couldn't get your API token."
25
34
  end
35
+
36
+ @api = EY::CloudClient.new(@token, @ui)
26
37
  end
27
38
 
28
- def refresh
29
- @token = self.class.fetch_token
39
+ def respond_to?(*a)
40
+ super or @api.respond_to?(*a)
30
41
  end
31
42
 
32
- def self.fetch_token
33
- EY.ui.info("We need to fetch your API token; please log in.")
34
- begin
35
- email = EY.ui.ask("Email: ")
36
- password = EY.ui.ask("Password: ", true)
37
- super(email, password)
38
- rescue EY::API::InvalidCredentials
39
- EY.ui.warn "Invalid username or password; please try again."
40
- retry
43
+ protected
44
+
45
+ def method_missing(meth, *args, &block)
46
+ if @api.respond_to?(meth)
47
+ @api.send(meth, *args, &block)
48
+ else
49
+ super
41
50
  end
51
+ rescue EY::CloudClient::InvalidCredentials
52
+ ui.warn "Authentication failed."
53
+ @token = self.class.authenticate(@ui)
54
+ @api = EY::CloudClient.new(@token, @ui)
55
+ retry
42
56
  end
43
57
 
44
58
  end
@@ -12,8 +12,10 @@ module EY
12
12
  DESC
13
13
 
14
14
  method_option :environment, :type => :string, :aliases => %w(-e),
15
+ :required => true, :default => '',
15
16
  :desc => "Environment in which to apply recipes"
16
17
  method_option :account, :type => :string, :aliases => %w(-c),
18
+ :required => true, :default => '',
17
19
  :desc => "Name of the account in which the environment can be found"
18
20
  def apply
19
21
  environment = fetch_environment(options[:environment], options[:account])
@@ -35,12 +37,15 @@ module EY
35
37
  DESC
36
38
 
37
39
  method_option :environment, :type => :string, :aliases => %w(-e),
40
+ :required => true, :default => '',
38
41
  :desc => "Environment that will receive the recipes"
39
42
  method_option :account, :type => :string, :aliases => %w(-c),
43
+ :required => true, :default => '',
40
44
  :desc => "Name of the account in which the environment can be found"
41
45
  method_option :apply, :type => :boolean,
42
46
  :desc => "Apply the recipes immediately after they are uploaded"
43
47
  method_option :file, :type => :string, :aliases => %w(-f),
48
+ :required => true, :default => '',
44
49
  :desc => "Specify a gzipped tar file (.tgz) for upload instead of cookbooks/ directory"
45
50
  def upload
46
51
  environment = fetch_environment(options[:environment], options[:account])
@@ -53,16 +58,16 @@ module EY
53
58
  no_tasks do
54
59
  def apply_recipes(environment)
55
60
  environment.run_custom_recipes
56
- EY.ui.say "Uploaded recipes started for #{environment.name}"
61
+ ui.say "Uploaded recipes started for #{environment.name}"
57
62
  end
58
63
 
59
64
  def upload_recipes(environment, filename)
60
- if options[:file]
61
- environment.upload_recipes_at_path(options[:file])
62
- EY.ui.say "Recipes file #{options[:file]} uploaded successfully for #{environment.name}"
65
+ if filename && filename != ''
66
+ environment.upload_recipes_at_path(filename)
67
+ ui.say "Recipes file #{filename} uploaded successfully for #{environment.name}"
63
68
  else
64
69
  environment.tar_and_upload_recipes_in_cookbooks_dir
65
- EY.ui.say "Recipes in cookbooks/ uploaded successfully for #{environment.name}"
70
+ ui.say "Recipes in cookbooks/ uploaded successfully for #{environment.name}"
66
71
  end
67
72
  end
68
73
  end
@@ -76,13 +81,15 @@ module EY
76
81
  If the cookbooks directory already exists, an error will be raised.
77
82
  DESC
78
83
  method_option :environment, :type => :string, :aliases => %w(-e),
84
+ :required => true, :default => '',
79
85
  :desc => "Environment for which to download the recipes"
80
86
  method_option :account, :type => :string, :aliases => %w(-c),
87
+ :required => true, :default => '',
81
88
  :desc => "Name of the account in which the environment can be found"
82
89
  def download
83
90
  environment = fetch_environment(options[:environment], options[:account])
84
91
  environment.download_recipes
85
- EY.ui.say "Recipes downloaded successfully for #{environment.name}"
92
+ ui.say "Recipes downloaded successfully for #{environment.name}"
86
93
  end
87
94
 
88
95
  end
@@ -1,29 +1,84 @@
1
+ require 'highline'
2
+
1
3
  module EY
2
4
  class CLI
3
5
  class UI < Thor::Base.shell
4
6
 
7
+ class Tee
8
+ def initialize(*ios)
9
+ @ios = ios
10
+ end
11
+
12
+ def <<(str)
13
+ @ios.each { |io| io << str }
14
+ self
15
+ end
16
+ end
17
+
5
18
  class Prompter
6
- class Mock
7
- def next_answer=(arg)
8
- @answers ||= []
9
- @answers << arg
10
- end
11
- def ask(*args, &block)
12
- @questions ||= []
13
- @questions << args.first
14
- @answers.pop
15
- end
16
- attr_reader :questions
19
+ def self.add_answer(arg)
20
+ @answers ||= []
21
+ @answers << arg
17
22
  end
23
+
24
+ def self.questions
25
+ @questions
26
+ end
27
+
18
28
  def self.enable_mock!
19
- @backend = Mock.new
29
+ @questions = []
30
+ @answers = []
31
+ @mock = true
20
32
  end
21
- def self.backend
22
- require 'highline'
23
- @backend ||= HighLine.new($stdin)
33
+
34
+ def self.highline
35
+ @highline ||= HighLine.new($stdin)
24
36
  end
25
- def self.ask(*args, &block)
26
- backend.ask(*args, &block)
37
+
38
+ def self.interactive?
39
+ @mock || ($stdin && $stdin.tty?)
40
+ end
41
+
42
+ def self.ask(question, password = false, default = nil)
43
+ if @mock
44
+ @questions ||= []
45
+ @questions << question
46
+ answer = @answers.shift
47
+ (answer == '' && default) ? default : answer
48
+ else
49
+ timeout_if_not_interactive do
50
+ highline.ask(question) do |q|
51
+ q.echo = "*" if password
52
+ q.default = default if default
53
+ end
54
+ end
55
+ end
56
+ end
57
+
58
+ def self.agree(question, default)
59
+ if @mock
60
+ @questions ||= []
61
+ @questions << question
62
+ answer = @answers.shift
63
+ answer == '' ? default : %w[y yes].include?(answer)
64
+ else
65
+ timeout_if_not_interactive do
66
+ answer = highline.agree(question) {|q| q.default = default ? 'Y/n' : 'N/y' }
67
+ case answer
68
+ when 'Y/n' then true
69
+ when 'N/y' then false
70
+ else answer
71
+ end
72
+ end
73
+ end
74
+ end
75
+
76
+ def self.timeout_if_not_interactive(&block)
77
+ if interactive?
78
+ block.call
79
+ else
80
+ Timeout.timeout(5, &block)
81
+ end
27
82
  end
28
83
  end
29
84
 
@@ -58,27 +113,27 @@ module EY
58
113
  end
59
114
  end
60
115
 
61
- def ask(message, password = false)
62
- begin
63
- if !$stdin || !$stdin.tty?
64
- Prompter.ask(message)
65
- elsif password
66
- Prompter.ask(message) {|q| q.echo = "*" }
67
- else
68
- Prompter.ask(message) {|q| q.readline = true }
69
- end
70
- rescue EOFError
71
- return ''
72
- end
116
+ def interactive?
117
+ Prompter.interactive?
118
+ end
119
+
120
+ def agree(message, default)
121
+ Prompter.agree(message, default)
73
122
  end
74
123
 
75
- def print_envs(apps, default_env_name = nil, simple = false)
124
+ def ask(message, password = false, default = nil)
125
+ Prompter.ask(message, password, default)
126
+ rescue EOFError
127
+ return ''
128
+ end
129
+
130
+ def print_envs(apps, default_env_name = nil, simple = false, endpoint = 'https://cloud.engineyard.com')
76
131
  if simple
77
132
  envs = apps.map{ |app| app.environments.to_a }
78
133
  puts envs.flatten.map{|env| env.name }.uniq
79
134
  else
80
135
  apps.each do |app|
81
- puts "#{app.name} (#{app.account.name})"
136
+ puts "#{app.account.name}/#{app.name}"
82
137
  if app.environments.any?
83
138
  app.environments.each do |env|
84
139
  short_name = env.shorten_name_for(app)
@@ -91,7 +146,7 @@ module EY
91
146
  puts " #{short_name}#{default_text} (#{icount} #{iname})"
92
147
  end
93
148
  else
94
- puts " (This application is not in any environments; you can make one at #{EY.config.endpoint})"
149
+ puts " (This application is not in any environments; you can make one at #{endpoint})"
95
150
  end
96
151
 
97
152
  puts ""
@@ -100,27 +155,25 @@ module EY
100
155
  end
101
156
 
102
157
  def show_deployment(dep)
103
- puts "# Status of last deployment of #{dep.app.account.name}/#{dep.app.name}/#{dep.environment.name}:"
104
- puts "#"
105
-
106
158
  output = []
107
159
  output << ["Account", dep.app.account.name]
108
160
  output << ["Application", dep.app.name]
109
161
  output << ["Environment", dep.environment.name]
110
162
  output << ["Input Ref", dep.ref]
111
- output << ["Resolved Ref", dep.ref]
112
- output << ["Commit", dep.commit || '(Unable to resolve)']
163
+ output << ["Resolved Ref", dep.resolved_ref]
164
+ output << ["Commit", dep.commit || '(not resolved)']
113
165
  output << ["Migrate", dep.migrate]
114
166
  output << ["Migrate command", dep.migrate_command] if dep.migrate
115
- output << ["Deployed by", dep.user_name]
116
- output << ["Created at", dep.created_at]
117
- output << ["Finished at", dep.finished_at]
167
+ output << ["Deployed by", dep.deployed_by]
168
+ output << ["Started at", dep.created_at] if dep.created_at
169
+ output << ["Finished at", dep.finished_at] if dep.finished_at
118
170
 
119
171
  output.each do |att, val|
120
- puts "#\t%-15s %s" % ["#{att}:", val.to_s]
172
+ puts "#\t%-16s %s" % ["#{att}:", val.to_s]
121
173
  end
122
- puts "#"
174
+ end
123
175
 
176
+ def deployment_result(dep)
124
177
  if dep.successful?
125
178
  info 'This deployment was successful.'
126
179
  elsif dep.finished_at.nil?
@@ -153,6 +206,14 @@ module EY
153
206
  ($stdout.tty? || ENV['THOR_SHELL']) ? super : string
154
207
  end
155
208
 
209
+ def err
210
+ $stderr
211
+ end
212
+
213
+ def out
214
+ $stdout
215
+ end
216
+
156
217
  end
157
218
  end
158
219
  end
@@ -4,17 +4,20 @@ module EY
4
4
  desc "enable [--environment/-e ENVIRONMENT]",
5
5
  "Remove the maintenance page for this application in the given environment."
6
6
  method_option :environment, :type => :string, :aliases => %w(-e),
7
+ :required => true, :default => '',
7
8
  :desc => "Environment on which to take down the maintenance page"
8
9
  method_option :app, :type => :string, :aliases => %w(-a),
10
+ :required => true, :default => '',
9
11
  :desc => "Name of the application whose maintenance page will be removed"
10
- method_option :verbose, :type => :boolean, :aliases => %w(-v),
11
- :desc => "Be verbose"
12
12
  method_option :account, :type => :string, :aliases => %w(-c),
13
+ :required => true, :default => '',
13
14
  :desc => "Name of the account in which the environment can be found"
15
+ method_option :verbose, :type => :boolean, :aliases => %w(-v),
16
+ :desc => "Be verbose"
14
17
  def enable
15
- app, environment = fetch_app_and_environment(options[:app], options[:environment], options[:account])
16
- EY.ui.info "Taking down maintenance page for '#{app.name}' in '#{environment.name}'"
17
- environment.take_down_maintenance_page(app, options[:verbose])
18
+ app_env = fetch_app_environment(options[:app], options[:environment], options[:account])
19
+ ui.info "Taking down maintenance page for '#{app_env.app.name}' in '#{app_env.environment.name}'"
20
+ serverside_runner(app_env, options[:verbose]).take_down_maintenance_page.call(ui.out, ui.err)
18
21
  end
19
22
 
20
23
  desc "disable [--environment/-e ENVIRONMENT]",
@@ -31,17 +34,20 @@ module EY
31
34
  * public/system/maintenance.html.default
32
35
  DESC
33
36
  method_option :environment, :type => :string, :aliases => %w(-e),
37
+ :required => true, :default => '',
34
38
  :desc => "Environment on which to put up the maintenance page"
35
39
  method_option :app, :type => :string, :aliases => %w(-a),
40
+ :required => true, :default => '',
36
41
  :desc => "Name of the application whose maintenance page will be put up"
37
- method_option :verbose, :type => :boolean, :aliases => %w(-v),
38
- :desc => "Be verbose"
39
42
  method_option :account, :type => :string, :aliases => %w(-c),
43
+ :required => true, :default => '',
40
44
  :desc => "Name of the account in which the environment can be found"
45
+ method_option :verbose, :type => :boolean, :aliases => %w(-v),
46
+ :desc => "Be verbose"
41
47
  def disable
42
- app, environment = fetch_app_and_environment(options[:app], options[:environment], options[:account])
43
- EY.ui.info "Putting up maintenance page for '#{app.name}' in '#{environment.name}'"
44
- environment.put_up_maintenance_page(app, options[:verbose])
48
+ app_env = fetch_app_environment(options[:app], options[:environment], options[:account])
49
+ ui.info "Putting up maintenance page for '#{app_env.app.name}' in '#{app_env.environment.name}'"
50
+ serverside_runner(app_env, options[:verbose]).put_up_maintenance_page.call(ui.out, ui.err)
45
51
  end
46
52
  end
47
53
  end
@@ -1,15 +1,17 @@
1
1
  require 'uri'
2
+ require 'yaml'
2
3
  require 'engineyard/error'
3
4
 
4
5
  module EY
5
6
  class Config
6
- CONFIG_FILES = ["config/ey.yml", "ey.yml"]
7
+ CONFIG_FILES = ["config/ey.yml", "ey.yml"].map {|path| Pathname.new(path)}.freeze
8
+
9
+ attr_reader :path
7
10
 
8
11
  def initialize(file = nil)
9
- require 'yaml'
10
- @file = file || CONFIG_FILES.find{|f| File.exists?(f) }
11
- @config = (@file ? YAML.load_file(@file) : {}) || {} # load_file returns `false' when the file is empty
12
- @config["environments"] = {} unless @config["environments"]
12
+ @path = file || CONFIG_FILES.find{|pathname| pathname.exist? }
13
+ @config = (@path ? YAML.load_file(@path.to_s) : {}) || {} # load_file returns `false' when the file is empty
14
+ @config["environments"] ||= {}
13
15
  end
14
16
 
15
17
  def method_missing(meth, *args, &blk)
@@ -27,17 +29,15 @@ module EY
27
29
  end
28
30
 
29
31
  def endpoint
30
- @endpoint ||= env_var_endpoint || default_endpoint
32
+ env_var_endpoint || default_endpoint
31
33
  end
32
34
 
33
35
  def env_var_endpoint
34
- if endpoint = ENV["CLOUD_URL"]
35
- assert_valid_endpoint endpoint, "CLOUD_URL"
36
- end
36
+ ENV["CLOUD_URL"]
37
37
  end
38
38
 
39
39
  def default_endpoint
40
- URI.parse("https://cloud.engineyard.com/")
40
+ "https://cloud.engineyard.com/"
41
41
  end
42
42
 
43
43
  def default_endpoint?
@@ -51,20 +51,94 @@ module EY
51
51
  d && d.first
52
52
  end
53
53
 
54
- def default_branch(environment = default_environment)
55
- env = environments[environment]
56
- env && env["branch"]
54
+ def environment_config(environment_name)
55
+ environments[environment_name] ||= {}
56
+ EnvironmentConfig.new(environments[environment_name], environment_name, self)
57
57
  end
58
58
 
59
- private
59
+ def set_environment_option(environment_name, key, value)
60
+ environments[environment_name] ||= {}
61
+ environments[environment_name][key] = value
62
+ ensure_path
63
+ @path.open('w') do |f|
64
+ YAML.dump(@config, f)
65
+ end
66
+ end
60
67
 
61
- def assert_valid_endpoint(endpoint, source)
62
- endpoint = URI.parse(endpoint) if endpoint.is_a?(String)
63
- return endpoint if endpoint.absolute?
68
+ def ensure_path
69
+ return if @path && @path.exist?
70
+ unless EY::Repo.exist?
71
+ raise "Not in application directory. Unable to save configuration."
72
+ end
73
+ @path = Pathname.new('config/ey.yml')
74
+ @path.dirname.mkpath
75
+ @path
76
+ end
64
77
 
65
- raise ConfigurationError.new('endpoint', endpoint.to_s, source, "endpoint must be an absolute URI")
78
+ class EnvironmentConfig
79
+ attr_reader :name
80
+
81
+ def initialize(config, name, parent)
82
+ @config = config || {}
83
+ @name = name
84
+ @parent = parent
85
+ end
86
+
87
+ def path
88
+ @parent.path
89
+ end
90
+
91
+ def fetch(key, default = nil, &block)
92
+ if block
93
+ @config.fetch(key.to_s, &block)
94
+ else
95
+ @config.fetch(key.to_s, default)
96
+ end
97
+ end
98
+
99
+ def set(key, val)
100
+ @config[key.to_s] = val
101
+ @parent.set_environment_option(@name, key, val)
102
+ val
103
+ end
104
+
105
+ def merge(other)
106
+ to_clean_hash.merge(other)
107
+ end
108
+
109
+ def to_clean_hash
110
+ @config.reject { |k,v| %w[branch migrate migration_command verbose].include?(k) }
111
+ end
112
+
113
+ def branch
114
+ fetch('branch', nil)
115
+ end
116
+
117
+ def migrate(&block)
118
+ fetch('migrate', &block)
119
+ end
120
+
121
+ def migrate=(mig)
122
+ set('migrate', mig)
123
+ end
124
+
125
+ def migration_command
126
+ fetch('migration_command', nil)
127
+ end
128
+
129
+ def migration_command=(cmd)
130
+ set('migration_command', cmd)
131
+ end
132
+ alias migrate_command migration_command
133
+ alias migrate_command= migration_command=
134
+
135
+ def verbose
136
+ fetch('verbose', false)
137
+ end
66
138
  end
67
139
 
140
+ private
141
+
68
142
  class ConfigurationError < EY::Error
69
143
  def initialize(key, value, source, message=nil)
70
144
  super %|"#{key}" from #{source} has invalid value: #{value.inspect}#{": #{message}" if message}|