engineyard 0.2.7
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/LICENSE +20 -0
- data/README.rdoc +7 -0
- data/bin/ey +14 -0
- data/lib/engineyard.rb +44 -0
- data/lib/engineyard/account.rb +78 -0
- data/lib/engineyard/api.rb +104 -0
- data/lib/engineyard/cli.rb +169 -0
- data/lib/engineyard/cli/api.rb +42 -0
- data/lib/engineyard/cli/error.rb +44 -0
- data/lib/engineyard/cli/ui.rb +96 -0
- data/lib/engineyard/config.rb +86 -0
- data/lib/engineyard/repo.rb +24 -0
- data/lib/vendor/thor.rb +244 -0
- data/lib/vendor/thor/actions.rb +275 -0
- data/lib/vendor/thor/actions/create_file.rb +103 -0
- data/lib/vendor/thor/actions/directory.rb +91 -0
- data/lib/vendor/thor/actions/empty_directory.rb +134 -0
- data/lib/vendor/thor/actions/file_manipulation.rb +223 -0
- data/lib/vendor/thor/actions/inject_into_file.rb +104 -0
- data/lib/vendor/thor/base.rb +540 -0
- data/lib/vendor/thor/core_ext/file_binary_read.rb +9 -0
- data/lib/vendor/thor/core_ext/hash_with_indifferent_access.rb +75 -0
- data/lib/vendor/thor/core_ext/ordered_hash.rb +100 -0
- data/lib/vendor/thor/error.rb +30 -0
- data/lib/vendor/thor/group.rb +271 -0
- data/lib/vendor/thor/invocation.rb +180 -0
- data/lib/vendor/thor/parser.rb +4 -0
- data/lib/vendor/thor/parser/argument.rb +67 -0
- data/lib/vendor/thor/parser/arguments.rb +150 -0
- data/lib/vendor/thor/parser/option.rb +128 -0
- data/lib/vendor/thor/parser/options.rb +169 -0
- data/lib/vendor/thor/rake_compat.rb +66 -0
- data/lib/vendor/thor/runner.rb +314 -0
- data/lib/vendor/thor/shell.rb +83 -0
- data/lib/vendor/thor/shell/basic.rb +239 -0
- data/lib/vendor/thor/shell/color.rb +108 -0
- data/lib/vendor/thor/task.rb +102 -0
- data/lib/vendor/thor/util.rb +230 -0
- data/lib/vendor/thor/version.rb +3 -0
- data/spec/engineyard/api_spec.rb +56 -0
- data/spec/engineyard/cli/api_spec.rb +44 -0
- data/spec/engineyard/cli_spec.rb +20 -0
- data/spec/engineyard/config_spec.rb +57 -0
- data/spec/engineyard/repo_spec.rb +52 -0
- data/spec/engineyard_spec.rb +7 -0
- data/spec/ey/deploy_spec.rb +65 -0
- data/spec/ey/ey_spec.rb +16 -0
- data/spec/spec.opts +2 -0
- data/spec/spec_helper.rb +40 -0
- data/spec/support/bundled_ey +10 -0
- data/spec/support/helpers.rb +46 -0
- metadata +231 -0
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2010 Engine Yard, Inc
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
data/bin/ey
ADDED
data/lib/engineyard.rb
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
module EY
|
2
|
+
VERSION = "0.2.7"
|
3
|
+
|
4
|
+
autoload :Account, 'engineyard/account'
|
5
|
+
autoload :API, 'engineyard/api'
|
6
|
+
autoload :Config, 'engineyard/config'
|
7
|
+
autoload :Repo, 'engineyard/repo'
|
8
|
+
|
9
|
+
class Error < RuntimeError; end
|
10
|
+
|
11
|
+
class UI
|
12
|
+
# stub debug outside of the CLI
|
13
|
+
def debug(*); end
|
14
|
+
end
|
15
|
+
|
16
|
+
class << self
|
17
|
+
attr_accessor :ui
|
18
|
+
|
19
|
+
def ui
|
20
|
+
@ui ||= UI.new
|
21
|
+
end
|
22
|
+
|
23
|
+
def config
|
24
|
+
@config ||= EY::Config.new
|
25
|
+
end
|
26
|
+
|
27
|
+
def config=(config)
|
28
|
+
@config = config
|
29
|
+
end
|
30
|
+
|
31
|
+
def library(libname)
|
32
|
+
begin
|
33
|
+
require libname
|
34
|
+
rescue LoadError
|
35
|
+
unless @tried_rubygems
|
36
|
+
require 'rubygems' rescue LoadError nil
|
37
|
+
@tried_rubygems = true
|
38
|
+
retry
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
module EY
|
2
|
+
class Account
|
3
|
+
|
4
|
+
def initialize(api)
|
5
|
+
@api = api
|
6
|
+
end
|
7
|
+
|
8
|
+
def environments
|
9
|
+
return @environments if @environments
|
10
|
+
data = @api.request('/environments', :method => :get)["environments"]
|
11
|
+
@environments = Environment.from_array(data || [])
|
12
|
+
end
|
13
|
+
|
14
|
+
def environment_named(name)
|
15
|
+
environments.find{|e| e.name == name }
|
16
|
+
end
|
17
|
+
|
18
|
+
def apps
|
19
|
+
return @apps if @apps
|
20
|
+
data = @api.request('/apps', :method => :get)["apps"]
|
21
|
+
@apps = App.from_array(data || [])
|
22
|
+
end
|
23
|
+
|
24
|
+
def app_named(name)
|
25
|
+
apps.find{|a| a.name == name }
|
26
|
+
end
|
27
|
+
|
28
|
+
def app_for_repo(repo)
|
29
|
+
apps.find{|a| repo.urls.include?(a.repository_url) }
|
30
|
+
end
|
31
|
+
|
32
|
+
# Classes to represent the returned data
|
33
|
+
class Environment < Struct.new(:name, :instances_count, :apps, :app_master, :username)
|
34
|
+
def self.from_hash(hash)
|
35
|
+
new(
|
36
|
+
hash["name"],
|
37
|
+
hash["instances_count"],
|
38
|
+
App.from_array(hash["apps"]),
|
39
|
+
AppMaster.from_hash(hash["app_master"]),
|
40
|
+
hash["ssh_username"]
|
41
|
+
) if hash && hash != "null"
|
42
|
+
end
|
43
|
+
|
44
|
+
def self.from_array(array)
|
45
|
+
array.map{|n| from_hash(n) } if array && array != "null"
|
46
|
+
end
|
47
|
+
|
48
|
+
def configuration
|
49
|
+
EY.config.environments[self.name]
|
50
|
+
end
|
51
|
+
alias_method :config, :configuration
|
52
|
+
end
|
53
|
+
|
54
|
+
class App < Struct.new(:name, :repository_url, :environments)
|
55
|
+
def self.from_hash(hash)
|
56
|
+
new(
|
57
|
+
hash["name"],
|
58
|
+
hash["repository_uri"], # We use url canonically in the ey gem
|
59
|
+
Environment.from_array(hash["environments"])
|
60
|
+
) if hash && hash != "null"
|
61
|
+
end
|
62
|
+
|
63
|
+
def self.from_array(array)
|
64
|
+
array.map{|n| from_hash(n) } if array && array != "null"
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
class AppMaster < Struct.new(:status, :public_hostname)
|
69
|
+
def self.from_hash(hash)
|
70
|
+
new(
|
71
|
+
hash["status"],
|
72
|
+
hash["public_hostname"]
|
73
|
+
) if hash && hash != "null"
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
end # Account
|
78
|
+
end # EY
|
@@ -0,0 +1,104 @@
|
|
1
|
+
module EY
|
2
|
+
class API
|
3
|
+
attr_reader :token
|
4
|
+
|
5
|
+
def initialize(token = nil)
|
6
|
+
@token ||= token
|
7
|
+
@token ||= self.class.read_token
|
8
|
+
raise ArgumentError, "EY Cloud API token required" unless @token
|
9
|
+
end
|
10
|
+
|
11
|
+
def ==(other)
|
12
|
+
raise ArgumentError unless other.is_a?(self.class)
|
13
|
+
self.token == other.token
|
14
|
+
end
|
15
|
+
|
16
|
+
def request(url, opts={})
|
17
|
+
opts[:headers] ||= {}
|
18
|
+
opts[:headers]["X-EY-Cloud-Token"] = token
|
19
|
+
EY.ui.debug("Token", token)
|
20
|
+
self.class.request(url, opts)
|
21
|
+
end
|
22
|
+
|
23
|
+
class InvalidCredentials < EY::Error; end
|
24
|
+
class RequestFailed < EY::Error; end
|
25
|
+
|
26
|
+
def self.request(path, opts={})
|
27
|
+
EY.library 'rest_client'
|
28
|
+
EY.library 'json'
|
29
|
+
|
30
|
+
url = EY.config.endpoint + "api/v2#{path}"
|
31
|
+
method = ((meth = opts.delete(:method)) && meth.to_s || "get").downcase.to_sym
|
32
|
+
params = opts.delete(:params) || {}
|
33
|
+
headers = opts.delete(:headers) || {}
|
34
|
+
headers["Accept"] ||= "application/json"
|
35
|
+
|
36
|
+
begin
|
37
|
+
EY.ui.debug("Request", "#{method.to_s.upcase} #{url}")
|
38
|
+
case method
|
39
|
+
when :get, :delete, :head
|
40
|
+
url.query = RestClient::Payload::UrlEncoded.new(params).to_s
|
41
|
+
resp = RestClient.send(method, url.to_s, headers)
|
42
|
+
else
|
43
|
+
resp = RestClient.send(method, url.to_s, params, headers)
|
44
|
+
end
|
45
|
+
rescue RestClient::Unauthorized
|
46
|
+
raise InvalidCredentials
|
47
|
+
rescue Errno::ECONNREFUSED
|
48
|
+
raise RequestFailed, "Could not reach the cloud API"
|
49
|
+
rescue RestClient::ResourceNotFound
|
50
|
+
raise RequestFailed, "The requested resource could not be found"
|
51
|
+
rescue RestClient::RequestFailed => e
|
52
|
+
raise RequestFailed, "#{e.message}"
|
53
|
+
end
|
54
|
+
raise RequestFailed, "Response body was empty" if resp.body.empty?
|
55
|
+
|
56
|
+
begin
|
57
|
+
data = JSON.parse(resp.body)
|
58
|
+
EY.ui.debug("Response", data)
|
59
|
+
rescue JSON::ParserError
|
60
|
+
EY.ui.debug("Raw response", resp.body)
|
61
|
+
raise RequestFailed, "Response was not valid JSON."
|
62
|
+
end
|
63
|
+
|
64
|
+
data
|
65
|
+
end
|
66
|
+
|
67
|
+
def self.fetch_token(email, password)
|
68
|
+
api_token = request("/authenticate", :method => "post",
|
69
|
+
:params => { :email => email, :password => password })["api_token"]
|
70
|
+
save_token(api_token)
|
71
|
+
api_token
|
72
|
+
end
|
73
|
+
|
74
|
+
def self.read_token(file = nil)
|
75
|
+
file ||= ENV['EYRC'] || File.expand_path("~/.eyrc")
|
76
|
+
return false unless File.exists?(file)
|
77
|
+
|
78
|
+
require 'yaml'
|
79
|
+
|
80
|
+
data = YAML.load_file(file)
|
81
|
+
if EY.config.default_endpoint?
|
82
|
+
data["api_token"]
|
83
|
+
else
|
84
|
+
(data[EY.config.endpoint.to_s] || {})["api_token"]
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def self.save_token(token, file = nil)
|
89
|
+
file ||= ENV['EYRC'] || File.expand_path("~/.eyrc")
|
90
|
+
require 'yaml'
|
91
|
+
|
92
|
+
data = File.exists?(file) ? YAML.load_file(file) : {}
|
93
|
+
if EY.config.default_endpoint?
|
94
|
+
data.merge!("api_token" => token)
|
95
|
+
else
|
96
|
+
data.merge!(EY.config.endpoint.to_s => {"api_token" => token})
|
97
|
+
end
|
98
|
+
|
99
|
+
File.open(file, "w"){|f| YAML.dump(data, f) }
|
100
|
+
true
|
101
|
+
end
|
102
|
+
|
103
|
+
end # API
|
104
|
+
end # EY
|
@@ -0,0 +1,169 @@
|
|
1
|
+
$:.unshift File.expand_path('../../vendor', __FILE__)
|
2
|
+
require 'thor'
|
3
|
+
|
4
|
+
require 'engineyard'
|
5
|
+
require 'engineyard/cli/error'
|
6
|
+
|
7
|
+
module EY
|
8
|
+
class CLI < Thor
|
9
|
+
EYSD_VERSION = "~>0.2.4.pre1"
|
10
|
+
|
11
|
+
autoload :API, 'engineyard/cli/api'
|
12
|
+
autoload :UI, 'engineyard/cli/ui'
|
13
|
+
|
14
|
+
include Thor::Actions
|
15
|
+
|
16
|
+
def self.start(*)
|
17
|
+
EY.ui = EY::CLI::UI.new
|
18
|
+
super
|
19
|
+
end
|
20
|
+
|
21
|
+
desc "deploy [ENVIRONMENT] [BRANCH]", "Deploy [BRANCH] of the app in the current directory to [ENVIRONMENT]"
|
22
|
+
method_option :force, :type => :boolean, :aliases => %w(-f),
|
23
|
+
:desc => "Force a deploy of the specified branch"
|
24
|
+
method_option :migrate, :type => :string, :aliases => %w(-m),
|
25
|
+
:desc => "Run migrations via [MIGRATE], defaults to 'rake db:migrate'"
|
26
|
+
method_option :install_eysd, :type => :boolean, :aliases => %(-s),
|
27
|
+
:desc => "Force remote install of eysd"
|
28
|
+
def deploy(env_name = nil, branch = nil)
|
29
|
+
app = account.app_for_repo(repo)
|
30
|
+
raise NoAppError.new(repo) unless app
|
31
|
+
|
32
|
+
env_name ||= EY.config.default_environment
|
33
|
+
raise DeployArgumentError if !env_name && app.environments.size != 1
|
34
|
+
|
35
|
+
default_branch = EY.config.default_branch(env_name)
|
36
|
+
branch ||= (default_branch || repo.current_branch)
|
37
|
+
raise DeployArgumentError unless branch
|
38
|
+
|
39
|
+
invalid_branch = default_branch && (branch != default_branch) && !options[:force]
|
40
|
+
raise BranchMismatch.new(default_branch, branch) if invalid_branch
|
41
|
+
|
42
|
+
if env_name
|
43
|
+
env = app.environments.find{|e| e.name == env_name }
|
44
|
+
else
|
45
|
+
env = app.environments.first
|
46
|
+
end
|
47
|
+
|
48
|
+
if !env && account.environment_named(env_name)
|
49
|
+
raise EnvironmentError, "Environment '#{env_name}' doesn't run this application\nYou can add it at #{EY.config.endpoint}"
|
50
|
+
elsif !env
|
51
|
+
raise NoEnvironmentError
|
52
|
+
end
|
53
|
+
|
54
|
+
running = env.app_master && env.app_master.status == "running"
|
55
|
+
raise EnvironmentError, "No running instances for environment #{env.name}\nStart one at #{EY.config.endpoint}" unless running
|
56
|
+
|
57
|
+
hostname = env.app_master.public_hostname
|
58
|
+
username = env.username
|
59
|
+
|
60
|
+
EY.ui.info "Connecting to the server..."
|
61
|
+
ssh_to(hostname, "eysd check '#{EY::VERSION}' '#{EYSD_VERSION}'", username, false)
|
62
|
+
case $?.exitstatus
|
63
|
+
when 255
|
64
|
+
raise EnvironmentError, "SSH connection to #{hostname} failed"
|
65
|
+
when 127
|
66
|
+
EY.ui.warn "Server does not have ey-deploy gem installed"
|
67
|
+
eysd_installed = false
|
68
|
+
when 0
|
69
|
+
eysd_installed = true
|
70
|
+
else
|
71
|
+
raise EnvironmentError, "ey-deploy version not compatible"
|
72
|
+
end
|
73
|
+
|
74
|
+
if !eysd_installed || options[:install_eysd]
|
75
|
+
EY.ui.info "Installing ey-deploy gem..."
|
76
|
+
ssh_to(hostname,
|
77
|
+
"sudo gem install ey-deploy -v '#{EYSD_VERSION}'",
|
78
|
+
username)
|
79
|
+
end
|
80
|
+
|
81
|
+
deploy_cmd = "eysd deploy --app #{app.name} --branch #{branch}"
|
82
|
+
if env.config
|
83
|
+
escaped_config_option = env.config.to_json.gsub(/"/, "\\\"")
|
84
|
+
deploy_cmd << " --config '#{escaped_config_option}'"
|
85
|
+
end
|
86
|
+
|
87
|
+
if options.key(:migrate)
|
88
|
+
if options[:migrate]
|
89
|
+
deploy_cmd << " --migrate='#{options[:migrate]}'"
|
90
|
+
else
|
91
|
+
deploy_cmd << " --no-migrate"
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
EY.ui.info "Running deploy on server..."
|
96
|
+
deployed = ssh_to(hostname, deploy_cmd, username)
|
97
|
+
|
98
|
+
if deployed
|
99
|
+
EY.ui.info "Deploy complete"
|
100
|
+
else
|
101
|
+
raise EY::Error, "Deploy failed"
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
|
106
|
+
desc "environments [--all]", "List cloud environments for this app, or all environments"
|
107
|
+
method_option :all, :type => :boolean, :aliases => %(-a)
|
108
|
+
def environments
|
109
|
+
app = account.app_for_repo(repo)
|
110
|
+
if options[:all] || !app
|
111
|
+
envs = account.environments
|
112
|
+
if envs.empty?
|
113
|
+
EY.ui.say %|You do not have any cloud environments.|
|
114
|
+
else
|
115
|
+
EY.ui.say %|Cloud environments:|
|
116
|
+
EY.ui.print_envs(envs, EY.config.default_environment)
|
117
|
+
end
|
118
|
+
|
119
|
+
EY.ui.warn(NoAppError.new(repo).message) unless app || options[:all]
|
120
|
+
else
|
121
|
+
app = account.app_for_repo(repo)
|
122
|
+
envs = app.environments
|
123
|
+
if envs.empty?
|
124
|
+
EY.ui.warn %|You have no environments set up for the application "#{app.name}"|
|
125
|
+
EY.ui.warn %|You can make one at #{EY.config.endpoint}|
|
126
|
+
else
|
127
|
+
EY.ui.say %|Cloud environments for #{app.name}:|
|
128
|
+
EY.ui.print_envs(envs, EY.config.default_environment)
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
map "envs" => :environments
|
133
|
+
|
134
|
+
desc "ssh ENV", "Open an ssh session to the environment's application server"
|
135
|
+
def ssh(name)
|
136
|
+
env = account.environment_named(name)
|
137
|
+
if env
|
138
|
+
Kernel.exec "ssh", "#{env.username}@#{env.app_master.public_hostname}", *ARGV[2..-1]
|
139
|
+
else
|
140
|
+
EY.ui.warn %|Could not find an environment named "#{name}"|
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
desc "version", "Print the version of the engineyard gem"
|
145
|
+
def version
|
146
|
+
EY.ui.say %{engineyard version #{EY::VERSION}}
|
147
|
+
end
|
148
|
+
map "-v" => :version
|
149
|
+
|
150
|
+
private
|
151
|
+
|
152
|
+
def account
|
153
|
+
@account ||= EY::Account.new(API.new)
|
154
|
+
end
|
155
|
+
|
156
|
+
def repo
|
157
|
+
@repo ||= EY::Repo.new
|
158
|
+
end
|
159
|
+
|
160
|
+
def ssh_to(hostname, remote_cmd, user, output = true)
|
161
|
+
cmd = %{ssh -o StrictHostKeyChecking=no -q #{user}@#{hostname} "#{remote_cmd}"}
|
162
|
+
cmd << %{ &> /dev/null} unless output
|
163
|
+
EY.ui.debug(cmd)
|
164
|
+
puts cmd if output
|
165
|
+
system cmd unless ENV["NO_SSH"]
|
166
|
+
end
|
167
|
+
|
168
|
+
end # CLI
|
169
|
+
end # EY
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module EY
|
2
|
+
class CLI
|
3
|
+
class API < EY::API
|
4
|
+
|
5
|
+
def initialize(token = nil)
|
6
|
+
@token = token
|
7
|
+
@token ||= self.class.read_token
|
8
|
+
@token ||= self.class.fetch_token
|
9
|
+
raise EY::Error, "Sorry, we couldn't get your API token." unless @token
|
10
|
+
end
|
11
|
+
|
12
|
+
def request(*)
|
13
|
+
begin
|
14
|
+
super
|
15
|
+
rescue EY::API::InvalidCredentials
|
16
|
+
EY.ui.warn "Credentials rejected, please authenticate again"
|
17
|
+
refresh
|
18
|
+
retry
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def refresh
|
23
|
+
@token = self.class.fetch_token
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.fetch_token
|
27
|
+
EY.ui.warn("The engineyard gem is prerelease software. Please do not use")
|
28
|
+
EY.ui.warn("this tool to deploy to mission-critical environments, yet.")
|
29
|
+
EY.ui.info("We need to fetch your API token, please login")
|
30
|
+
begin
|
31
|
+
email = EY.ui.ask("Email: ")
|
32
|
+
password = EY.ui.ask("Password: ", true)
|
33
|
+
super(email, password)
|
34
|
+
rescue EY::API::InvalidCredentials
|
35
|
+
EY.ui.warn "Invalid username or password, please try again"
|
36
|
+
retry
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|