engineyard 0.3.1 → 0.3.2

Sign up to get free protection for your applications and to get access to all the features.
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