engineyard 0.2.11 → 0.2.12

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