engineyard 0.3.1 → 0.3.2

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.
data/lib/engineyard.rb CHANGED
@@ -1,12 +1,12 @@
1
1
  module EY
2
2
  require 'engineyard/ruby_ext'
3
3
 
4
- VERSION = "0.3.1"
4
+ VERSION = "0.3.2"
5
5
 
6
- autoload :Account, 'engineyard/account'
7
6
  autoload :API, 'engineyard/api'
8
7
  autoload :Config, 'engineyard/config'
9
8
  autoload :Error, 'engineyard/error'
9
+ autoload :Model, 'engineyard/model'
10
10
  autoload :Repo, 'engineyard/repo'
11
11
 
12
12
  class UI
@@ -1,3 +1,5 @@
1
+ require 'engineyard/model'
2
+
1
3
  module EY
2
4
  class API
3
5
  attr_reader :token
@@ -20,9 +22,26 @@ module EY
20
22
  self.class.request(url, opts)
21
23
  end
22
24
 
25
+ def environments
26
+ @environments ||= EY::Model::Environment.from_array(request('/environments')["environments"], :api => self)
27
+ end
28
+
29
+ def apps
30
+ @apps ||= EY::Model::App.from_array(request('/apps')["apps"], :api => self)
31
+ end
32
+
33
+ def environment_named(name, envs = self.environments)
34
+ envs.find{|e| e.name == name }
35
+ end
36
+
37
+ def app_for_repo(repo)
38
+ apps.find{|a| repo.urls.include?(a.repository_uri) }
39
+ end
40
+
23
41
  class InvalidCredentials < EY::Error; end
24
42
  class RequestFailed < EY::Error; end
25
43
 
44
+
26
45
  def self.request(path, opts={})
27
46
  require 'rest_client'
28
47
  require 'json'
@@ -24,8 +24,8 @@ module EY
24
24
  method_option :install_eysd, :type => :boolean, :aliases => %(-s),
25
25
  :desc => "Force remote install of eysd"
26
26
  def deploy(env_name = nil, branch = nil)
27
- require 'engineyard/action/deploy'
28
- EY::Action::Deploy.call(env_name, branch, options)
27
+ require 'engineyard/cli/action/deploy'
28
+ EY::CLI::Action::Deploy.call(env_name, branch, options)
29
29
  end
30
30
 
31
31
 
@@ -47,15 +47,27 @@ module EY
47
47
 
48
48
  desc "rebuild [ENV]", "Rebuild environment (ensure configuration is up-to-date)"
49
49
  def rebuild(name = nil)
50
- require 'engineyard/action/rebuild'
51
- EY::Action::Rebuild.call(name)
50
+ env = if name
51
+ env = api.environment_named(name) or raise NoEnvironmentError.new(name)
52
+ end
53
+
54
+ unless env
55
+ repo = Repo.new
56
+ app = api.app_for_repo(repo) or raise NoAppError.new(repo)
57
+ env = app.one_and_only_environment or raise EnvironmentError, "Unable to determine a single environment for the current application (found #{app.environments.size} environments)"
58
+ end
59
+
60
+ EY.ui.debug("Rebuilding #{env.name}")
61
+ env.rebuild
52
62
  end
53
63
 
54
64
  desc "ssh ENV", "Open an ssh session to the environment's application server"
55
65
  def ssh(name)
56
- env = account.environment_named(name)
57
- if env
66
+ env = api.environment_named(name)
67
+ if env && env.app_master
58
68
  Kernel.exec "ssh", "#{env.username}@#{env.app_master.public_hostname}", *ARGV[2..-1]
69
+ elsif env
70
+ raise NoAppMaster.new(env.name)
59
71
  else
60
72
  EY.ui.warn %|Could not find an environment named "#{name}"|
61
73
  end
@@ -80,7 +92,7 @@ module EY
80
92
 
81
93
  desc "upload_recipes ENV", "Upload custom chef recipes from the current directory to ENV"
82
94
  def upload_recipes(name)
83
- if account.upload_recipes_for(env_named(name))
95
+ if env_named(name).upload_recipes
84
96
  EY.ui.say "Recipes uploaded successfully"
85
97
  else
86
98
  EY.ui.error "Recipes upload failed"
@@ -94,8 +106,8 @@ module EY
94
106
  map ["-v", "--version"] => :version
95
107
 
96
108
  private
97
- def account
98
- @account ||= EY::Account.new(EY::CLI::API.new)
109
+ def api
110
+ @api ||= EY::CLI::API.new
99
111
  end
100
112
 
101
113
  def repo
@@ -103,7 +115,7 @@ module EY
103
115
  end
104
116
 
105
117
  def env_named(env_name)
106
- env = account.environment_named(env_name)
118
+ env = api.environment_named(env_name)
107
119
 
108
120
  if env.nil?
109
121
  raise EnvironmentError, "Environment '#{env_name}' can't be found\n" +
@@ -114,10 +126,10 @@ module EY
114
126
  end
115
127
 
116
128
  def app_and_envs(all_envs = false)
117
- app = account.app_for_repo(repo)
129
+ app = api.app_for_repo(repo)
118
130
 
119
131
  if all_envs || !app
120
- envs = account.environments
132
+ envs = api.environments
121
133
  EY.ui.warn(NoAppError.new(repo).message) unless app || all_envs
122
134
  [nil, envs]
123
135
  else
@@ -0,0 +1,112 @@
1
+ module EY
2
+ class CLI
3
+ module Action
4
+ class Deploy
5
+
6
+ EYSD_VERSION = "~>0.3.0"
7
+
8
+ def self.call(env_name, branch, options)
9
+ env_name ||= EY.config.default_environment
10
+
11
+ app = fetch_app
12
+ env = fetch_environment(env_name, app)
13
+ branch = fetch_branch(env.name, branch, options[:force])
14
+
15
+ running = env.app_master && env.app_master.status == "running"
16
+ raise EnvironmentError, "No running instances for environment #{env.name}\nStart one at #{EY.config.endpoint}" unless running
17
+
18
+ master = env.app_master
19
+
20
+ EY.ui.info "Connecting to the server..."
21
+ ensure_eysd_present(master, options[:install_eysd])
22
+
23
+ EY.ui.info "Running deploy for '#{env.name}' on server..."
24
+ deployed = master.deploy!(app, branch, options[:migrate], env.config)
25
+
26
+ if deployed
27
+ EY.ui.info "Deploy complete"
28
+ else
29
+ raise EY::Error, "Deploy failed"
30
+ end
31
+ end
32
+
33
+ private
34
+
35
+ def self.api
36
+ @api ||= EY::CLI::API.new
37
+ end
38
+
39
+ def self.repo
40
+ @repo ||= EY::Repo.new
41
+ end
42
+
43
+ def self.fetch_app
44
+ app = api.app_for_repo(repo)
45
+ raise NoAppError.new(repo) unless app
46
+ app
47
+ end
48
+
49
+ def self.fetch_environment(env_name, app)
50
+ # if the name's not specified and there's not exactly one
51
+ # environment, we can't figure out which environment to deploy
52
+ raise DeployArgumentError if !env_name && app.environments.size != 1
53
+
54
+ env = if env_name
55
+ api.environment_named(env_name, app.environments)
56
+ else
57
+ app.environments.first
58
+ end
59
+
60
+ # the environment exists, but doesn't have this app
61
+ if !env && api.environment_named(env_name)
62
+ raise EnvironmentError, "Environment '#{env_name}' doesn't run this application\nYou can add it at #{EY.config.endpoint}"
63
+ end
64
+
65
+ if !env
66
+ raise NoEnvironmentError.new(env_name)
67
+ end
68
+
69
+ env
70
+ end
71
+
72
+ def self.fetch_branch(env_name, user_specified_branch, force)
73
+ default_branch = EY.config.default_branch(env_name)
74
+
75
+ branch = if user_specified_branch
76
+ if default_branch && (user_specified_branch != default_branch) && !force
77
+ raise BranchMismatch.new(default_branch, user_specified_branch)
78
+ end
79
+ user_specified_branch
80
+ else
81
+ default_branch || repo.current_branch
82
+ end
83
+
84
+ raise DeployArgumentError unless branch
85
+ branch
86
+ end
87
+
88
+ def self.ensure_eysd_present(instance, install_eysd)
89
+ eysd_status = instance.ey_deploy_check
90
+ case eysd_status
91
+ when :ssh_failed
92
+ raise EnvironmentError, "SSH connection to #{instance.hostname} failed"
93
+ when :eysd_missing
94
+ EY.ui.warn "Instance does not have server-side component installed"
95
+ EY.ui.info "Installing server-side component..."
96
+ instance.install_ey_deploy!
97
+ when :too_new
98
+ raise EnvironmentError, "server-side component too new; please upgrade your copy of the engineyard gem."
99
+ when :too_old
100
+ EY.ui.info "Upgrading server-side component..."
101
+ instance.upgrade_ey_deploy!
102
+ when :ok
103
+ # no action needed
104
+ else
105
+ raise EY::Error, "Internal error: Unexpected status from Instance#ey_deploy_check; got #{eysd_status.inspect}"
106
+ end
107
+ end
108
+
109
+ end
110
+ end
111
+ end
112
+ end
@@ -23,6 +23,10 @@ module EY
23
23
  @token = self.class.fetch_token
24
24
  end
25
25
 
26
+ def environment_named(env_name, envs = self.environments)
27
+ super || find_environment_by_unambiguous_substring(env_name, envs)
28
+ end
29
+
26
30
  def self.fetch_token
27
31
  EY.ui.warn("The engineyard gem is prerelease software. Please do not use")
28
32
  EY.ui.warn("this tool to deploy to mission-critical environments, yet.")
@@ -37,6 +41,15 @@ module EY
37
41
  end
38
42
  end
39
43
 
44
+ private
45
+ def find_environment_by_unambiguous_substring(env_name, envs)
46
+ candidates = envs.find_all{|e| e.name[env_name] }
47
+ if candidates.size > 1
48
+ raise AmbiguousEnvironmentName.new(env_name, candidates.map {|e| e.name})
49
+ end
50
+ candidates.first
51
+ end
52
+
40
53
  end
41
54
  end
42
- end
55
+ end
@@ -1,6 +1,12 @@
1
1
  module EY
2
2
  class Error < RuntimeError; end
3
3
 
4
+ class NoRemotesError < EY::Error
5
+ def initialize(path)
6
+ super "fatal: No git remotes found in #{path}"
7
+ end
8
+ end
9
+
4
10
  class NoAppError < Error
5
11
  def initialize(repo)
6
12
  @repo = repo
@@ -14,9 +20,30 @@ module EY
14
20
  end
15
21
  end
16
22
 
23
+ class NoAppMaster < EY::Error
24
+ def initialize(env_name)
25
+ @env_name = env_name
26
+ end
27
+
28
+ def message
29
+ "The environment '#{@env_name}' does not have a master instance."
30
+ end
31
+ end
32
+
17
33
  class EnvironmentError < EY::Error
18
34
  end
19
35
 
36
+ class AmbiguousEnvironmentName < EY::Error
37
+ def initialize(name, matches)
38
+ @name, @matches = name, matches
39
+ end
40
+
41
+ def message
42
+ pretty_names = @matches.map {|x| "'#{x}'"}.join(', ')
43
+ "The name '#{@name}' is ambiguous; it matches all of the following environment names: #{pretty_names}.\nPlease use a longer, unambiguous substring or the entire environment name."
44
+ end
45
+ end
46
+
20
47
  class NoEnvironmentError < EY::Error
21
48
  def initialize(env_name=nil)
22
49
  @env_name = env_name
@@ -0,0 +1,9 @@
1
+ module EY
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'
8
+ end
9
+ end
@@ -1,13 +1,13 @@
1
1
  module EY
2
- class Account
2
+ module Model
3
3
  class ApiStruct < Struct
4
4
  def self.new(*args, &block)
5
5
  super(*args) do |*block_args|
6
6
  block.call(*block_args) if block
7
7
 
8
- def self.from_array(array, account = nil)
8
+ def self.from_array(array, common_values = {})
9
9
  array.map do |values|
10
- from_hash(values.merge(:account => account))
10
+ from_hash(values.merge(common_values))
11
11
  end if array
12
12
  end
13
13
 
@@ -20,6 +20,11 @@ module EY
20
20
 
21
21
  end
22
22
  end
23
+
24
+ def api_get(uri, options = {})
25
+ api.request(uri, options.merge(:method => :get))
26
+ end
27
+
23
28
  end
24
29
  end
25
30
  end
@@ -1,10 +1,10 @@
1
1
  module EY
2
- class Account
3
- class App < ApiStruct.new(:name, :repository_uri, :environments, :account)
2
+ module Model
3
+ class App < ApiStruct.new(:name, :repository_uri, :environments, :api)
4
4
 
5
5
  def self.from_hash(hash)
6
6
  super.tap do |app|
7
- app.environments = Environment.from_array(app.environments, app.account)
7
+ app.environments = Environment.from_array(app.environments, :api => app.api)
8
8
  end
9
9
  end
10
10
 
@@ -1,24 +1,31 @@
1
1
  module EY
2
- class Account
3
- class Environment < ApiStruct.new(:id, :name, :instances_count, :apps, :app_master, :username, :account)
2
+ module Model
3
+ class Environment < ApiStruct.new(:id, :name, :instances_count, :apps, :app_master, :username, :api)
4
4
  def self.from_hash(hash)
5
5
  super.tap do |env|
6
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)
7
+ env.apps = App.from_array(env.apps, :api => env.api)
8
+ env.app_master = Instance.from_hash(env.app_master.merge(:environment => env)) if env.app_master
9
9
  end
10
10
  end
11
11
 
12
12
  def logs
13
- account.logs_for(self)
13
+ Log.from_array(api_get("/environments/#{id}/logs")["logs"])
14
14
  end
15
15
 
16
16
  def instances
17
- account.instances_for(self)
17
+ Instance.from_array(api_get("/environments/#{id}/instances")["instances"], :environment => self)
18
18
  end
19
19
 
20
20
  def rebuild
21
- account.rebuild(self)
21
+ api.request("/environments/#{id}/rebuild", :method => :put)
22
+ end
23
+
24
+ def upload_recipes(file_to_upload = recipe_file)
25
+ api.request("/environments/#{id}/recipes",
26
+ :method => :post,
27
+ :params => {:file => file_to_upload}
28
+ )
22
29
  end
23
30
 
24
31
  def recipe_file
@@ -0,0 +1,91 @@
1
+ require 'escape'
2
+
3
+ module EY
4
+ module Model
5
+ class Instance < ApiStruct.new(:id, :role, :status, :amazon_id, :public_hostname, :environment)
6
+ EYSD_VERSION = "~>0.3.2"
7
+ CHECK_SCRIPT = <<-SCRIPT
8
+ require "rubygems"
9
+ requirement = Gem::Requirement.new("#{EYSD_VERSION}")
10
+ required_version = requirement.requirements.last.last # thanks thanks rubygems rubygems
11
+
12
+ # this will be a ["name-version", Gem::Specification] two-element array if present, nil otherwise
13
+ ey_deploy_geminfo = Gem.source_index.find{ |(name,_)| name =~ /^ey-deploy-\\\d/ }
14
+ exit(104) unless ey_deploy_geminfo
15
+
16
+ current_version = ey_deploy_geminfo.last.version
17
+ exit(0) if requirement.satisfied_by?(current_version)
18
+ exit(70) if required_version > current_version
19
+ exit(17) # required_version < current_version
20
+ SCRIPT
21
+ EXIT_STATUS = Hash.new { |h,k| raise EY::Error, "ey-deploy version checker exited with unknown status code #{k}" }
22
+ EXIT_STATUS.merge!({
23
+ 255 => :ssh_failed,
24
+ 104 => :eysd_missing,
25
+ 70 => :too_old,
26
+ 17 => :too_new,
27
+ 0 => :ok,
28
+ })
29
+
30
+ alias :hostname :public_hostname
31
+
32
+ def deploy!(app, ref, migration_command=nil, extra_configuration=nil)
33
+ deploy_cmd = [eysd_path, 'deploy', '--app', app.name, '--branch', ref]
34
+
35
+ if extra_configuration
36
+ deploy_cmd << '--config' << extra_configuration.to_json
37
+ end
38
+
39
+ if migration_command
40
+ deploy_cmd << "--migrate" << migration_command
41
+ end
42
+
43
+ ssh Escape.shell_command(deploy_cmd)
44
+ end
45
+
46
+ def ey_deploy_check
47
+ require 'base64'
48
+ encoded_script = Base64.encode64(CHECK_SCRIPT).gsub(/\n/, '')
49
+ ssh "#{ruby_path} -r base64 -e \"eval Base64.decode64(ARGV[0])\" #{encoded_script}", false
50
+ EXIT_STATUS[$?.exitstatus]
51
+ end
52
+
53
+ def install_ey_deploy!
54
+ ssh(Escape.shell_command(['sudo', gem_path, 'install', 'ey-deploy', '-v', EYSD_VERSION]))
55
+ end
56
+
57
+ def upgrade_ey_deploy!
58
+ ssh "sudo #{gem_path} uninstall -a -x ey-deploy"
59
+ install_ey_deploy!
60
+ end
61
+
62
+ private
63
+
64
+ def ssh(remote_command, output = true)
65
+ user = environment.username
66
+
67
+ cmd = Escape.shell_command(%w[ssh -o StrictHostKeyChecking=no -q] << "#{user}@#{hostname}" << remote_command)
68
+ cmd << " > /dev/null" unless output
69
+ output ? puts(cmd) : EY.ui.debug(cmd)
70
+ unless ENV["NO_SSH"]
71
+ system cmd
72
+ else
73
+ true
74
+ end
75
+ end
76
+
77
+ def eysd_path
78
+ "/usr/local/ey_resin/ruby/bin/eysd"
79
+ end
80
+
81
+ def gem_path
82
+ "/usr/local/ey_resin/ruby/bin/gem"
83
+ end
84
+
85
+ def ruby_path
86
+ "/usr/local/ey_resin/ruby/bin/ruby"
87
+ end
88
+
89
+ end
90
+ end
91
+ end