engineyard 1.3.1 → 1.3.2

Sign up to get free protection for your applications and to get access to all the features.
data/lib/engineyard.rb CHANGED
@@ -8,6 +8,7 @@ module EY
8
8
  autoload :Error, 'engineyard/error'
9
9
  autoload :Model, 'engineyard/model'
10
10
  autoload :Repo, 'engineyard/repo'
11
+ autoload :Resolver, 'engineyard/resolver'
11
12
 
12
13
  class UI
13
14
  # stub debug outside of the CLI
@@ -30,20 +30,12 @@ module EY
30
30
  @apps ||= EY::Model::App.from_array(request('/apps')["apps"], :api => self)
31
31
  end
32
32
 
33
- def apps_for_repo(repo)
34
- apps.find_all {|a| repo.urls.include?(a.repository_uri) }
33
+ def resolver
34
+ @resolver ||= Resolver.new(self)
35
35
  end
36
36
 
37
- def app_for_repo(repo)
38
- candidates = apps_for_repo(repo)
39
- if candidates.size > 1
40
- raise EY::AmbiguousGitUriError.new(repo.urls, candidates.map{|x| x.name})
41
- end
42
- candidates.first
43
- end
44
-
45
- def app_for_repo!(repo)
46
- app_for_repo(repo) || raise(NoAppError.new(repo))
37
+ def apps_for_repo(repo)
38
+ apps.find_all {|a| repo.urls.include?(a.repository_uri) }
47
39
  end
48
40
 
49
41
  class InvalidCredentials < EY::Error; end
@@ -44,13 +44,14 @@ module EY
44
44
  :desc => "Git ref to deploy. May be a branch, a tag, or a SHA."
45
45
  method_option :app, :type => :string, :aliases => %w(-a),
46
46
  :desc => "Name of the application to deploy"
47
+ method_option :account, :type => :string, :aliases => %w(-c),
48
+ :desc => "Name of the account you want to deploy in"
47
49
  method_option :verbose, :type => :boolean, :aliases => %w(-v),
48
50
  :desc => "Be verbose"
49
51
  method_option :extra_deploy_hook_options, :type => :hash, :default => {},
50
52
  :desc => "Additional options to be made available in deploy hooks (in the 'config' hash)"
51
53
  def deploy
52
- app = fetch_app(options[:app])
53
- environment = fetch_environment(options[:environment], app)
54
+ app, environment = fetch_app_and_environment(options[:app], options[:environment], options[:account])
54
55
  environment.ignore_bad_master = options[:ignore_bad_master]
55
56
  deploy_ref = if options[:app]
56
57
  environment.resolve_branch(options[:ref], options[:ignore_default_branch]) ||
@@ -90,8 +91,8 @@ module EY
90
91
  def environments
91
92
  if options[:all] && options[:simple]
92
93
  # just put each env
93
- api.environments.each do |env|
94
- puts env.name
94
+ api.environments.each do |environment|
95
+ puts environment.name
95
96
  end
96
97
  else
97
98
  apps = get_apps(options[:all])
@@ -120,10 +121,12 @@ module EY
120
121
 
121
122
  method_option :environment, :type => :string, :aliases => %w(-e),
122
123
  :desc => "Environment to rebuild"
124
+ method_option :account, :type => :string, :aliases => %w(-c),
125
+ :desc => "Name of the account you want to rebuild in"
123
126
  def rebuild
124
- env = fetch_environment(options[:environment])
125
- EY.ui.debug("Rebuilding #{env.name}")
126
- env.rebuild
127
+ environment = fetch_environment(options[:environment], options[:account])
128
+ EY.ui.debug("Rebuilding #{environment.name}")
129
+ environment.rebuild
127
130
  end
128
131
 
129
132
  desc "rollback [--environment ENVIRONMENT]", "Rollback to the previous deploy."
@@ -136,18 +139,17 @@ module EY
136
139
  :desc => "Environment in which to roll back the application"
137
140
  method_option :app, :type => :string, :aliases => %w(-a),
138
141
  :desc => "Name of the application to roll back"
142
+ method_option :account, :type => :string, :aliases => %w(-c),
143
+ :desc => "Name of the account you want to roll back in"
139
144
  method_option :verbose, :type => :boolean, :aliases => %w(-v),
140
145
  :desc => "Be verbose"
141
146
  method_option :extra_deploy_hook_options, :type => :hash, :default => {},
142
147
  :desc => "Additional options to be made available in deploy hooks (in the 'config' hash)"
143
148
  def rollback
144
- app = fetch_app(options[:app])
145
- env = fetch_environment(options[:environment], app)
149
+ app, environment = fetch_app_and_environment(options[:app], options[:environment], options[:account])
146
150
 
147
- EY.ui.info("Rolling back '#{app.name}' in '#{env.name}'")
148
- if env.rollback(app,
149
- options[:extra_deploy_hook_options],
150
- options[:verbose])
151
+ EY.ui.info("Rolling back '#{app.name}' in '#{environment.name}'")
152
+ if environment.rollback(app, options[:extra_deploy_hook_options], options[:verbose])
151
153
  EY.ui.info "Rollback complete"
152
154
  else
153
155
  raise EY::Error, "Rollback failed"
@@ -167,6 +169,8 @@ module EY
167
169
  DESC
168
170
  method_option :environment, :type => :string, :aliases => %w(-e),
169
171
  :desc => "Environment to ssh into"
172
+ method_option :account, :type => :string, :aliases => %w(-c),
173
+ :desc => "Name of the account you want to deploy in"
170
174
  method_option :all, :type => :boolean, :aliases => %(-a),
171
175
  :desc => "Run command on all servers"
172
176
  method_option :app_servers, :type => :boolean,
@@ -181,13 +185,13 @@ module EY
181
185
  :desc => "Run command on the utility servers with the given names. If no names are given, run on all utility servers."
182
186
 
183
187
  def ssh(cmd=nil)
184
- env = fetch_environment_without_app(options[:environment])
185
- hosts = ssh_hosts(options, env)
188
+ environment = fetch_environment(options[:environment], options[:account])
189
+ hosts = ssh_hosts(options, environment)
186
190
 
187
191
  raise NoCommandError.new if cmd.nil? and hosts.count != 1
188
192
 
189
193
  hosts.each do |host|
190
- system "ssh #{env.username}@#{host} #{cmd}"
194
+ system "ssh #{environment.username}@#{host} #{cmd}"
191
195
  end
192
196
  end
193
197
 
@@ -203,7 +207,7 @@ module EY
203
207
  return lambda {|instance| %w(solo app_master ).include?(instance.role) }
204
208
  end
205
209
 
206
- def ssh_hosts(opts, env)
210
+ def ssh_hosts(opts, environment)
207
211
  if opts[:utilities] and not opts[:utilities].respond_to?(:include?)
208
212
  includes_everything = []
209
213
  class << includes_everything
@@ -214,8 +218,8 @@ module EY
214
218
  filter = ssh_host_filter(opts)
215
219
  end
216
220
 
217
- instances = env.instances.select {|instance| filter[instance] }
218
- raise NoInstancesError.new(env.name) if instances.empty?
221
+ instances = environment.instances.select {|instance| filter[instance] }
222
+ raise NoInstancesError.new(environment.name) if instances.empty?
219
223
  return instances.map { |instance| instance.public_hostname }
220
224
  end
221
225
  end
@@ -228,18 +232,20 @@ module EY
228
232
  DESC
229
233
  method_option :environment, :type => :string, :aliases => %w(-e),
230
234
  :desc => "Environment with the interesting logs"
235
+ method_option :account, :type => :string, :aliases => %w(-c),
236
+ :desc => "Name of the account you want to deploy in"
231
237
  def logs
232
- env = fetch_environment(options[:environment])
233
- env.logs.each do |log|
238
+ environment = fetch_environment(options[:environment], options[:account])
239
+ environment.logs.each do |log|
234
240
  EY.ui.info log.instance_name
235
241
 
236
242
  if log.main
237
- EY.ui.info "Main logs for #{env.name}:"
243
+ EY.ui.info "Main logs for #{environment.name}:"
238
244
  EY.ui.say log.main
239
245
  end
240
246
 
241
247
  if log.custom
242
- EY.ui.info "Custom logs for #{env.name}:"
248
+ EY.ui.info "Custom logs for #{environment.name}:"
243
249
  EY.ui.say log.custom
244
250
  end
245
251
  end
@@ -10,8 +10,10 @@ module EY
10
10
 
11
11
  method_option :environment, :type => :string, :aliases => %w(-e),
12
12
  :desc => "Environment in which to apply recipes"
13
+ method_option :account, :type => :string, :aliases => %w(-c),
14
+ :desc => "Name of the account you want to deploy in"
13
15
  def apply
14
- environment = fetch_environment_without_app(options[:environment])
16
+ environment = fetch_environment(options[:environment], options[:account])
15
17
  environment.run_custom_recipes
16
18
  EY.ui.say "Uploaded recipes started for #{environment.name}"
17
19
  end
@@ -25,8 +27,10 @@ module EY
25
27
 
26
28
  method_option :environment, :type => :string, :aliases => %w(-e),
27
29
  :desc => "Environment that will receive the recipes"
30
+ method_option :account, :type => :string, :aliases => %w(-c),
31
+ :desc => "Name of the account you want to deploy in"
28
32
  def upload
29
- environment = fetch_environment_without_app(options[:environment])
33
+ environment = fetch_environment(options[:environment], options[:account])
30
34
  environment.upload_recipes
31
35
  EY.ui.say "Recipes uploaded successfully for #{environment.name}"
32
36
  end
@@ -41,8 +45,10 @@ module EY
41
45
  DESC
42
46
  method_option :environment, :type => :string, :aliases => %w(-e),
43
47
  :desc => "Environment for which to download the recipes"
48
+ method_option :account, :type => :string, :aliases => %w(-c),
49
+ :desc => "Name of the account you want to deploy in"
44
50
  def download
45
- environment = fetch_environment_without_app(options[:environment])
51
+ environment = fetch_environment(options[:environment], options[:account])
46
52
  environment.download_recipes
47
53
  EY.ui.say "Recipes downloaded successfully for #{environment.name}"
48
54
  end
@@ -67,7 +67,7 @@ module EY
67
67
  end
68
68
  else
69
69
  apps.each do |app|
70
- puts app.name
70
+ puts "#{app.name} (#{app.account.name})"
71
71
  if app.environments.any?
72
72
  app.environments.each do |env|
73
73
  short_name = env.shorten_name_for(app)
@@ -9,9 +9,10 @@ module EY
9
9
  :desc => "Name of the application whose maintenance page will be removed"
10
10
  method_option :verbose, :type => :boolean, :aliases => %w(-v),
11
11
  :desc => "Be verbose"
12
+ method_option :account, :type => :string, :aliases => %w(-c),
13
+ :desc => "Name of the account you want to deploy in"
12
14
  def enable
13
- app = fetch_app(options[:app])
14
- environment = fetch_environment(options[:environment], app)
15
+ app, environment = fetch_app_and_environment(options[:app], options[:environment], options[:account])
15
16
  EY.ui.info "Taking down maintenance page for '#{app.name}' in '#{environment.name}'"
16
17
  environment.take_down_maintenance_page(app, options[:verbose])
17
18
  end
@@ -35,9 +36,10 @@ module EY
35
36
  :desc => "Name of the application whose maintenance page will be put up"
36
37
  method_option :verbose, :type => :boolean, :aliases => %w(-v),
37
38
  :desc => "Be verbose"
39
+ method_option :account, :type => :string, :aliases => %w(-c),
40
+ :desc => "Name of the account you want to deploy in"
38
41
  def disable
39
- app = fetch_app(options[:app])
40
- environment = fetch_environment(options[:environment], app)
42
+ app, environment = fetch_app_and_environment(options[:app], options[:environment], options[:account])
41
43
  EY.ui.info "Putting up maintenance page for '#{app.name}' in '#{environment.name}'"
42
44
  environment.put_up_maintenance_page(app, options[:verbose])
43
45
  end
@@ -8,8 +8,14 @@ module EY
8
8
  We are working on letting you specify the account name to resolve this ambiguity.
9
9
  MSG
10
10
 
11
- def named(name)
12
- candidates = find_all {|x| x.name == name }
11
+ def named(name, account_name=nil)
12
+ candidates = find_all do |x|
13
+ if account_name
14
+ x.name == name && x.account.name == account_name
15
+ else
16
+ x.name == name
17
+ end
18
+ end
13
19
  if candidates.size > 1
14
20
  raise ambiguous_error(name, candidates.map {|e| e.name}, COLLAB_MESSAGE )
15
21
  end
@@ -7,6 +7,10 @@ module EY
7
7
  end
8
8
  end
9
9
 
10
+ class ResolveError < EY::Error; end
11
+ class NoMatchesError < ResolveError; end
12
+ class MultipleMatchesError < ResolveError; end
13
+
10
14
  class NoCommandError < EY::Error
11
15
  def initialize
12
16
  super "Must specify a command to run via ssh"
@@ -41,18 +45,6 @@ module EY
41
45
  end
42
46
  end
43
47
 
44
- class AmbiguousGitUriError < EY::Error
45
- def initialize(uris, matches)
46
- message = "The following Git Remote urls match multiple Applications in AppCloud\n"
47
- uris.each { |uri| message << "\t#{uri}\n" }
48
-
49
- message << "Please use -a <appname> to specify one of the following applications:\n"
50
- matches.each { |app| message << "\t#{app}\n" }
51
-
52
- super message
53
- end
54
- end
55
-
56
48
  class NoAppMasterError < EY::Error
57
49
  def initialize(env_name)
58
50
  super "The environment '#{env_name}' does not have a master instance."
@@ -84,7 +76,13 @@ module EY
84
76
  def initialize(environments)
85
77
  message = "The repository url in this directory is ambiguous.\n"
86
78
  message << "Please use -e <envname> to specify one of the following environments:\n"
87
- environments.each { |env| message << "\t#{env.name}\n" }
79
+ environments.sort do |a, b|
80
+ if a.account == b.account
81
+ a.name <=> b.name
82
+ else
83
+ a.account.name <=> b.account.name
84
+ end
85
+ end.each { |env| message << "\t#{env.name} (#{env.account.name})\n" }
88
86
  super message
89
87
  end
90
88
  end
@@ -110,15 +108,15 @@ module EY
110
108
 
111
109
  class BranchMismatchError < EY::Error
112
110
  def initialize(default_branch, branch)
113
- super %|Your deploy branch is set to "#{default_branch}".\n| +
114
- %|If you want to deploy branch "#{branch}", use --ignore-default_branch.|
111
+ super(%|Your deploy branch is set to "#{default_branch}".\n| +
112
+ %|If you want to deploy branch "#{branch}", use --ignore-default_branch.|)
115
113
  end
116
114
  end
117
115
 
118
116
  class DeployArgumentError < EY::Error
119
117
  def initialize
120
- super %("deploy" was called incorrectly. Call as "deploy [--environment <env>] [--ref <branch|tag|ref>]"\n) +
121
- %|You can set default environments and branches in ey.yml|
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|)
122
120
  end
123
121
  end
124
122
  end
@@ -1,9 +1,10 @@
1
1
  module EY
2
2
  module Model
3
- autoload :ApiStruct, 'engineyard/model/api_struct'
4
- autoload :App, 'engineyard/model/app'
5
- autoload :Environment, 'engineyard/model/environment'
6
- autoload :Log, 'engineyard/model/log'
7
- autoload :Instance, 'engineyard/model/instance'
3
+ autoload :ApiStruct, 'engineyard/model/api_struct'
4
+ autoload :Account, 'engineyard/model/account'
5
+ autoload :App, 'engineyard/model/app'
6
+ autoload :Environment, 'engineyard/model/environment'
7
+ autoload :Log, 'engineyard/model/log'
8
+ autoload :Instance, 'engineyard/model/instance'
8
9
  end
9
10
  end
@@ -0,0 +1,8 @@
1
+ require 'escape'
2
+
3
+ module EY
4
+ module Model
5
+ class Account < ApiStruct.new(:id, :name)
6
+ end
7
+ end
8
+ end
@@ -1,10 +1,11 @@
1
1
  module EY
2
2
  module Model
3
- class App < ApiStruct.new(:name, :repository_uri, :environments, :api)
3
+ class App < ApiStruct.new(:id, :account, :name, :repository_uri, :environments, :api)
4
4
 
5
5
  def self.from_hash(hash)
6
6
  super.tap do |app|
7
7
  app.environments = Environment.from_array(app.environments, :api => app.api)
8
+ app.account = Account.from_hash(app.account)
8
9
  end
9
10
  end
10
11
 
@@ -1,6 +1,6 @@
1
1
  module EY
2
2
  module Model
3
- class Environment < ApiStruct.new(:id, :name, :framework_env, :instances, :instances_count, :apps, :app_master, :username, :stack_name, :api)
3
+ class Environment < ApiStruct.new(:id, :account, :name, :framework_env, :instances, :instances_count, :apps, :app_master, :username, :stack_name, :api)
4
4
 
5
5
  attr_accessor :ignore_bad_master
6
6
 
@@ -8,6 +8,7 @@ module EY
8
8
  super.tap do |env|
9
9
  env.username = hash['ssh_username']
10
10
  env.apps = App.from_array(env.apps, :api => env.api)
11
+ env.account = Account.from_hash(env.account)
11
12
  env.instances = Instance.from_array(hash['instances'], :environment => env)
12
13
  env.app_master = Instance.from_hash(env.app_master.merge(:environment => env)) if env.app_master
13
14
  end
@@ -7,8 +7,12 @@ module EY
7
7
  @path = path
8
8
  end
9
9
 
10
+ def exists?
11
+ File.directory?(File.join(@path, ".git"))
12
+ end
13
+
10
14
  def current_branch
11
- if File.directory?(File.join(@path, ".git"))
15
+ if exists?
12
16
  head = File.read(File.join(@path, ".git/HEAD")).chomp
13
17
  if head.gsub!("ref: refs/heads/", "")
14
18
  head
@@ -0,0 +1,122 @@
1
+ module EY
2
+ class Resolver
3
+ attr_reader :api
4
+
5
+ def initialize(api)
6
+ @api = api
7
+ end
8
+
9
+ def environment(options)
10
+ raise ArgumentError if options[:app_name]
11
+ candidates, account_candidates, app_candidates, environment_candidates = filter_candidates(options)
12
+
13
+ environments = candidates.map{ |c| [c[:account_name], c[:environment_name]] }.uniq.map do |account_name, environment_name|
14
+ api.environments.named(environment_name, account_name)
15
+ end
16
+
17
+ if environments.empty?
18
+ if options[:environment_name]
19
+ raise EY::NoEnvironmentError.new(options[:environment_name])
20
+ else
21
+ raise EY::NoAppError.new(options[:repo])
22
+ end
23
+ elsif environments.size > 1
24
+ if options[:environment_name]
25
+ message = "Multiple environments possible, please be more specific:\n\n"
26
+ candidates.map{|e| [e[:account_name], e[:environment_name]]}.uniq.each do |account_name, environment_name|
27
+ message << "\t#{environment_name} # ey <command> --environment='#{environment_name}' --account='#{account_name}'\n"
28
+ end
29
+ raise MultipleMatchesError.new(message)
30
+ else
31
+ raise EY::AmbiguousEnvironmentGitUriError.new(environments)
32
+ end
33
+ end
34
+ environments.first
35
+ end
36
+
37
+ def app_and_environment(options)
38
+ candidates, account_candidates, app_candidates, environment_candidates = filter_candidates(options)
39
+
40
+ if candidates.empty?
41
+ if account_candidates.empty? && options[:account_name]
42
+ raise NoMatchesError.new("There were no accounts that matched #{options[:account_name]}")
43
+ elsif app_candidates.empty?
44
+ if options[:app_name]
45
+ raise InvalidAppError.new(options[:app_name])
46
+ else
47
+ raise NoAppError.new(options[:repo])
48
+ end
49
+ elsif environment_candidates.empty?
50
+ raise NoEnvironmentError.new(options[:environment_name])
51
+ else
52
+ message = "The matched apps & environments do not correspond with each other.\n"
53
+ message << "Applications:\n"
54
+ app_candidates.map{|ad| [ad[:account_name], ad[:app_name]]}.uniq.each do |account_name, app_name|
55
+ app = api.apps.named(app_name, account_name)
56
+ message << "\t#{app.name}\n"
57
+ app.environments.each do |env|
58
+ message << "\t\t#{env.name} # ey <command> -e #{env.name} -a #{app.name}\n"
59
+ end
60
+ end
61
+ end
62
+ raise NoMatchesError.new(message)
63
+ elsif candidates.size > 1
64
+ message = "Multiple app deployments possible, please be more specific:\n\n"
65
+ candidates.map{|c| [c[:account_name], c[:app_name]]}.uniq.each do |account_name, app_name|
66
+ message << "#{app_name}\n"
67
+ candidates.select {|x| x[:app_name] == app_name && x[:account_name] == account_name}.map{|x| x[:environment_name]}.uniq.each do |env_name|
68
+ message << "\t#{env_name} # ey <command> --environment='#{env_name}' --app='#{app_name}' --account='#{account_name}'\n"
69
+ end
70
+ end
71
+ raise MultipleMatchesError.new(message)
72
+ end
73
+ result = candidates.first
74
+ [api.apps.named(result[:app_name], result[:account_name]), api.environments.named(result[:environment_name], result[:account_name])]
75
+ end
76
+
77
+ private
78
+
79
+ def app_deployments
80
+ @app_deployments ||= api.apps.map do |app|
81
+ app.environments.map do |environment|
82
+ {
83
+ :app_name => app.name,
84
+ :repository_uri => app.repository_uri,
85
+ :environment_name => environment.name,
86
+ :account_name => app.account.name,
87
+ }
88
+ end
89
+ end.flatten
90
+ end
91
+
92
+ def filter_candidates(options)
93
+ raise ArgumentError if options.empty?
94
+
95
+ candidates = app_deployments
96
+
97
+ account_candidates = filter_candidates_by(:account_name, options, candidates)
98
+
99
+ app_candidates = if options[:app_name]
100
+ filter_candidates_by(:app_name, options, candidates)
101
+ elsif options[:repo]
102
+ candidates.select {|c| options[:repo].urls.include?(c[:repository_uri]) }
103
+ else
104
+ candidates
105
+ end
106
+
107
+ environment_candidates = filter_candidates_by(:environment_name, options, candidates)
108
+ candidates = app_candidates & environment_candidates & account_candidates
109
+ [candidates, account_candidates, app_candidates, environment_candidates]
110
+ end
111
+
112
+ def filter_candidates_by(type, options, candidates)
113
+ if options[type] && candidates.any?{|c| c[type] == options[type] }
114
+ candidates.select {|c| c[type] == options[type] }
115
+ elsif options[type]
116
+ candidates.select {|c| c[type][options[type]] }
117
+ else
118
+ candidates
119
+ end
120
+ end
121
+ end
122
+ end