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,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}|