engineyard 0.2.11 → 0.2.12

Sign up to get free protection for your applications and to get access to all the features.
Files changed (38) hide show
  1. data/lib/engineyard.rb +4 -3
  2. data/lib/engineyard/#repo.rb# +24 -0
  3. data/lib/engineyard/account.rb +32 -11
  4. data/lib/engineyard/account/api_struct.rb +25 -0
  5. data/lib/engineyard/account/app.rb +11 -10
  6. data/lib/engineyard/account/app_master.rb +1 -7
  7. data/lib/engineyard/account/environment.rb +31 -16
  8. data/lib/engineyard/account/instance.rb +6 -0
  9. data/lib/engineyard/account/log.rb +7 -16
  10. data/lib/engineyard/action/deploy.rb +138 -0
  11. data/lib/engineyard/action/list_environments.rb +22 -0
  12. data/lib/engineyard/action/rebuild.rb +31 -0
  13. data/lib/engineyard/action/show_logs.rb +26 -0
  14. data/lib/engineyard/action/ssh.rb +19 -0
  15. data/lib/engineyard/action/upload_recipes.rb +17 -0
  16. data/lib/engineyard/action/util.rb +47 -0
  17. data/lib/engineyard/cli.rb +24 -150
  18. data/lib/engineyard/cli/thor_fixes.rb +26 -0
  19. data/lib/engineyard/error.rb +48 -0
  20. data/lib/engineyard/repo.rb +1 -1
  21. data/lib/engineyard/ruby_ext.rb +9 -0
  22. data/spec/engineyard/account/api_struct_spec.rb +37 -0
  23. data/spec/engineyard/account/environment_spec.rb +20 -0
  24. data/spec/engineyard/account_spec.rb +18 -0
  25. data/spec/engineyard/cli_spec.rb +3 -3
  26. data/spec/ey/deploy_spec.rb +166 -28
  27. data/spec/ey/ey_spec.rb +3 -3
  28. data/spec/ey/list_environments_spec.rb +16 -0
  29. data/spec/ey/logs_spec.rb +2 -17
  30. data/spec/ey/rebuild_spec.rb +25 -0
  31. data/spec/ey/ssh_spec.rb +24 -0
  32. data/spec/ey/upload_recipes_spec.rb +21 -0
  33. data/spec/spec_helper.rb +32 -0
  34. data/spec/support/fake_awsm.ru +25 -1
  35. data/spec/support/helpers.rb +72 -7
  36. metadata +44 -56
  37. data/lib/engineyard/cli/error.rb +0 -44
  38. data/spec/spec.opts +0 -2
data/lib/engineyard.rb CHANGED
@@ -1,13 +1,14 @@
1
1
  module EY
2
- VERSION = "0.2.11"
2
+ require 'engineyard/ruby_ext'
3
+
4
+ VERSION = "0.2.12"
3
5
 
4
6
  autoload :Account, 'engineyard/account'
5
7
  autoload :API, 'engineyard/api'
6
8
  autoload :Config, 'engineyard/config'
9
+ autoload :Error, 'engineyard/error'
7
10
  autoload :Repo, 'engineyard/repo'
8
11
 
9
- class Error < RuntimeError; end
10
-
11
12
  class UI
12
13
  # stub debug outside of the CLI
13
14
  def debug(*); end
@@ -0,0 +1,24 @@
1
+ module EY
2
+ class Repo
3
+
4
+ def initialize(path=File.expand_path('.'))
5
+ @path = path
6
+ end
7
+
8
+ def current_branch
9
+ head = File.read(File.join(@path, ".git/HEAD")).chomp
10
+ if head.gsub!("ref: refs/heads/", "")
11
+ head
12
+ else
13
+ nil
14
+ end
15
+ end
16
+
17
+ def urls
18
+ `git config -f #{@path}/.git/config --get-regexp 'remote.*.url'`.split(/\n/).map do |c|
19
+ c.split.last
20
+ end
21
+ end
22
+
23
+ end # Repo
24
+ end # EY
@@ -1,7 +1,9 @@
1
+ require 'engineyard/account/api_struct'
1
2
  require 'engineyard/account/app'
2
3
  require 'engineyard/account/app_master'
3
4
  require 'engineyard/account/environment'
4
5
  require 'engineyard/account/log'
6
+ require 'engineyard/account/instance'
5
7
 
6
8
  module EY
7
9
  class Account
@@ -10,24 +12,43 @@ module EY
10
12
  @api = api
11
13
  end
12
14
 
13
- def request(path, options = { })
14
- @api.request(path, {:method => :get}.merge(options))
15
+ def environments
16
+ @environments ||= begin
17
+ data = @api.request('/environments')["environments"]
18
+ Environment.from_array(data, self)
19
+ end
15
20
  end
16
21
 
17
- def environments
18
- return @environments if @environments
19
- data = request('/environments')["environments"]
20
- @environments = Environment.from_array(data || [], self)
22
+ def apps
23
+ @apps ||= App.from_array(@api.request('/apps')["apps"], self)
21
24
  end
22
25
 
23
26
  def environment_named(name)
24
27
  environments.find{|e| e.name == name }
25
28
  end
26
29
 
27
- def apps
28
- return @apps if @apps
29
- data = @api.request('/apps')["apps"]
30
- @apps = App.from_array(data || [], self)
30
+ def logs_for(env)
31
+ data = @api.request("/environments/#{env.id}/logs")["logs"]
32
+ Log.from_array(data)
33
+ end
34
+
35
+ def instances_for(env)
36
+ @instances ||= begin
37
+ data = @api.request("/environments/#{env.id}/instances")["instances"]
38
+ Instance.from_array(data)
39
+ end
40
+ end
41
+
42
+ def upload_recipes_for(env)
43
+ @api.request("/environments/#{env.id}/recipes",
44
+ :method => :post,
45
+ :params => {:file => env.recipe_file}
46
+ )
47
+ end
48
+
49
+ def rebuild(env)
50
+ @api.request("/environments/#{env.id}/rebuild",
51
+ :method => :put)
31
52
  end
32
53
 
33
54
  def app_named(name)
@@ -35,7 +56,7 @@ module EY
35
56
  end
36
57
 
37
58
  def app_for_repo(repo)
38
- apps.find{|a| repo.urls.include?(a.repository_url) }
59
+ apps.find{|a| repo.urls.include?(a.repository_uri) }
39
60
  end
40
61
 
41
62
  end # Account
@@ -0,0 +1,25 @@
1
+ module EY
2
+ class Account
3
+ class ApiStruct < Struct
4
+ def self.new(*args, &block)
5
+ super(*args) do |*block_args|
6
+ block.call(*block_args) if block
7
+
8
+ def self.from_array(array, account = nil)
9
+ array.map do |values|
10
+ from_hash(values.merge(:account => account))
11
+ end if array
12
+ end
13
+
14
+ def self.from_hash(hash)
15
+ return nil unless hash
16
+ members = new.members
17
+ values = members.map{|a| hash.has_key?(a.to_sym) ? hash[a.to_sym] : hash[a] }
18
+ new(*values)
19
+ end
20
+
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -1,18 +1,19 @@
1
1
  module EY
2
2
  class Account
3
- class App < Struct.new(:name, :repository_url, :environments, :account)
4
- def self.from_hash(hash, account)
5
- new(
6
- hash["name"],
7
- hash["repository_uri"], # We use url canonically in the ey gem
8
- Environment.from_array(hash["environments"], account),
9
- account
10
- ) if hash && hash != "null"
3
+ class App < ApiStruct.new(:name, :repository_uri, :environments, :account)
4
+
5
+ def self.from_hash(hash)
6
+ super.tap do |app|
7
+ app.environments = Environment.from_array(app.environments, app.account)
8
+ end
11
9
  end
12
10
 
13
- def self.from_array(array, account)
14
- array.map{|n| from_hash(n, account) } if array && array != "null"
11
+ def one_and_only_environment
12
+ if environments.size == 1
13
+ environments.first
14
+ end
15
15
  end
16
+
16
17
  end
17
18
  end
18
19
  end
@@ -1,12 +1,6 @@
1
1
  module EY
2
2
  class Account
3
- class AppMaster < Struct.new(:status, :public_hostname)
4
- def self.from_hash(hash)
5
- new(
6
- hash["status"],
7
- hash["public_hostname"]
8
- ) if hash && hash != "null"
9
- end
3
+ class AppMaster < ApiStruct.new(:status, :public_hostname)
10
4
  end
11
5
  end
12
6
  end
@@ -1,25 +1,40 @@
1
1
  module EY
2
2
  class Account
3
- class Environment < Struct.new(:id, :name, :instances_count, :apps, :app_master, :username, :account)
4
- def self.from_hash(hash, account)
5
- new(
6
- hash["id"],
7
- hash["name"],
8
- hash["instances_count"],
9
- App.from_array(hash["apps"], account),
10
- AppMaster.from_hash(hash["app_master"]),
11
- hash["ssh_username"],
12
- account
13
- ) if hash && hash != "null"
3
+ class Environment < ApiStruct.new(:id, :name, :instances_count, :apps, :app_master, :username, :account)
4
+ def self.from_hash(hash)
5
+ super.tap do |env|
6
+ env.username = hash['ssh_username']
7
+ env.apps = App.from_array(env.apps, env.account)
8
+ env.app_master = AppMaster.from_hash(env.app_master)
9
+ end
14
10
  end
15
11
 
16
- def self.from_array(array, account)
17
- array.map{|n| from_hash(n, account) } if array && array != "null"
12
+ def logs
13
+ account.logs_for(self)
18
14
  end
19
15
 
20
- def logs
21
- data = account.request("/environments/#{id}/logs")['logs']
22
- Log.from_array(data || [])
16
+ def instances
17
+ account.instances_for(self)
18
+ end
19
+
20
+ def rebuild
21
+ account.rebuild(self)
22
+ end
23
+
24
+ def recipe_file
25
+ require 'tempfile'
26
+ unless File.exist?("cookbooks")
27
+ raise EY::Error, "Could not find chef recipes. Please run from the root of your recipes repo."
28
+ end
29
+
30
+ tmp = Tempfile.new("recipes")
31
+ cmd = "git archive --format=tar HEAD cookbooks | gzip > #{tmp.path}"
32
+
33
+ unless system(cmd)
34
+ raise EY::Error, "Could not archive recipes.\nCommand `#{cmd}` exited with an error."
35
+ end
36
+
37
+ tmp
23
38
  end
24
39
 
25
40
  def configuration
@@ -0,0 +1,6 @@
1
+ module EY
2
+ class Account
3
+ class Instance < ApiStruct.new(:id, :role, :amazon_id, :public_hostname)
4
+ end
5
+ end
6
+ end
@@ -1,18 +1,9 @@
1
- class Log < Struct.new(:id, :role, :main, :custom)
2
- def self.from_hash(hash)
3
- new(
4
- hash["id"],
5
- hash["role"],
6
- hash["main"],
7
- hash["custom"]
8
- ) if hash && hash != "null"
9
- end
10
-
11
- def self.from_array(array)
12
- array.map{|n| from_hash(n) } if array && array != "null"
13
- end
14
-
15
- def instance_name
16
- "#{role} #{id}"
1
+ module EY
2
+ class Account
3
+ class Log < ApiStruct.new(:id, :role, :main, :custom)
4
+ def instance_name
5
+ "#{role} #{id}"
6
+ end
7
+ end
17
8
  end
18
9
  end
@@ -0,0 +1,138 @@
1
+ require 'engineyard/action/util'
2
+
3
+ module EY
4
+ module Action
5
+ class Deploy
6
+ extend Util
7
+
8
+ EYSD_VERSION = "~>0.2.7"
9
+
10
+ def self.call(env_name, branch, options)
11
+ env_name ||= EY.config.default_environment
12
+
13
+ app = fetch_app
14
+ env = fetch_environment(env_name, app)
15
+ branch = fetch_branch(env.name, branch, options[:force])
16
+
17
+ running = env.app_master && env.app_master.status == "running"
18
+ raise EnvironmentError, "No running instances for environment #{env.name}\nStart one at #{EY.config.endpoint}" unless running
19
+
20
+ hostname = env.app_master.public_hostname
21
+ username = env.username
22
+
23
+ EY.ui.info "Connecting to the server..."
24
+ ensure_eysd_present(hostname, username, options[:install_eysd])
25
+
26
+ deploy_cmd = "#{eysd_path} deploy --app #{app.name} --branch #{branch}"
27
+ if env.config
28
+ escaped_config_option = env.config.to_json.gsub(/"/, "\\\"")
29
+ deploy_cmd << " --config '#{escaped_config_option}'"
30
+ end
31
+
32
+ if options['migrate']
33
+ deploy_cmd << " --migrate='#{options[:migrate]}'"
34
+ end
35
+
36
+ EY.ui.info "Running deploy on server..."
37
+ deployed = ssh_to(hostname, deploy_cmd, username)
38
+
39
+ if deployed
40
+ EY.ui.info "Deploy complete"
41
+ else
42
+ raise EY::Error, "Deploy failed"
43
+ end
44
+ end
45
+
46
+ private
47
+
48
+ def self.fetch_app
49
+ app = account.app_for_repo(repo)
50
+ raise NoAppError.new(repo) unless app
51
+ app
52
+ end
53
+
54
+ def self.fetch_environment(env_name, app)
55
+ # if the name's not specified and there's not exactly one
56
+ # environment, we can't figure out which environment to deploy
57
+ raise DeployArgumentError if !env_name && app.environments.size != 1
58
+
59
+ env = if env_name
60
+ # environment names are unique per-customer, so
61
+ # there's no danger of finding two here
62
+ app.environments.find{|e| e.name == env_name }
63
+ else
64
+ app.environments.first
65
+ end
66
+
67
+ # the environment exists, but doesn't have this app
68
+ if !env && account.environment_named(env_name)
69
+ raise EnvironmentError, "Environment '#{env_name}' doesn't run this application\nYou can add it at #{EY.config.endpoint}"
70
+ end
71
+
72
+ if !env
73
+ raise NoEnvironmentError.new(env_name)
74
+ end
75
+
76
+ env
77
+ end
78
+
79
+ def self.fetch_branch(env_name, user_specified_branch, force)
80
+ default_branch = EY.config.default_branch(env_name)
81
+
82
+ branch = if user_specified_branch
83
+ if default_branch && (user_specified_branch != default_branch) && !force
84
+ raise BranchMismatch.new(default_branch, user_specified_branch)
85
+ end
86
+ user_specified_branch
87
+ else
88
+ default_branch || repo.current_branch
89
+ end
90
+
91
+ raise DeployArgumentError unless branch
92
+ branch
93
+ end
94
+
95
+ def self.ensure_eysd_present(hostname, username, install_eysd)
96
+ ssh_to(hostname, "#{eysd_path} check '#{EY::VERSION}' '#{EYSD_VERSION}'", username, false)
97
+ case $?.exitstatus
98
+ when 255
99
+ raise EnvironmentError, "SSH connection to #{hostname} failed"
100
+ when 127
101
+ EY.ui.warn "Server does not have ey-deploy gem installed"
102
+ eysd_installed = false
103
+ when 0
104
+ eysd_installed = true
105
+ else
106
+ raise EnvironmentError, "ey-deploy version not compatible"
107
+ end
108
+
109
+ if !eysd_installed || install_eysd
110
+ EY.ui.info "Installing ey-deploy gem..."
111
+ ssh_to(hostname,
112
+ "sudo #{gem_path} install ey-deploy -v '#{EYSD_VERSION}'",
113
+ username)
114
+ end
115
+ end
116
+
117
+ def self.eysd_path
118
+ "/usr/local/ey_resin/ruby/bin/eysd"
119
+ end
120
+
121
+ def self.gem_path
122
+ "/usr/local/ey_resin/ruby/bin/gem"
123
+ end
124
+
125
+ def self.ssh_to(hostname, remote_cmd, user, output = true)
126
+ cmd = %{ssh -o StrictHostKeyChecking=no -q #{user}@#{hostname} "#{remote_cmd}"}
127
+ cmd << %{ &> /dev/null} unless output
128
+ output ? puts(cmd) : EY.ui.debug(cmd)
129
+ unless ENV["NO_SSH"]
130
+ system cmd
131
+ else
132
+ true
133
+ end
134
+ end
135
+
136
+ end
137
+ end
138
+ end
@@ -0,0 +1,22 @@
1
+ require 'engineyard/action/util'
2
+
3
+ module EY
4
+ module Action
5
+ class ListEnvironments
6
+ extend Util
7
+
8
+ def self.call(all)
9
+ app, envs = app_and_envs(all)
10
+ if app
11
+ EY.ui.say %|Cloud environments for #{app.name}:|
12
+ EY.ui.print_envs(envs, EY.config.default_environment)
13
+ elsif envs
14
+ EY.ui.say %|Cloud environments:|
15
+ EY.ui.print_envs(envs, EY.config.default_environment)
16
+ else
17
+ EY.ui.say %|You do not have any cloud environments.|
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,31 @@
1
+ require 'engineyard/action/util'
2
+
3
+ module EY
4
+ module Action
5
+ class Rebuild
6
+ extend Util
7
+
8
+ def self.call(name)
9
+ env = fetch_environment_by_name(name) || fetch_environment_from_app
10
+ EY.ui.debug("Rebuilding #{env.name}")
11
+ env.rebuild
12
+ end
13
+
14
+ private
15
+ def self.fetch_environment_by_name(name)
16
+ if name
17
+ env = account.environment_named(name)
18
+ return env if env
19
+ raise NoEnvironmentError.new(name)
20
+ end
21
+ end
22
+
23
+ def self.fetch_environment_from_app
24
+ repo = Repo.new
25
+ app = account.app_for_repo(repo) or raise NoAppError.new(repo)
26
+ env = app.one_and_only_environment or raise EnvironmentError, "Unable to determine a single environment for the current application (found #{app.environments.size} environments)"
27
+ env
28
+ end
29
+ end
30
+ end
31
+ end