engineyard 0.2.7

Sign up to get free protection for your applications and to get access to all the features.
Files changed (52) hide show
  1. data/LICENSE +20 -0
  2. data/README.rdoc +7 -0
  3. data/bin/ey +14 -0
  4. data/lib/engineyard.rb +44 -0
  5. data/lib/engineyard/account.rb +78 -0
  6. data/lib/engineyard/api.rb +104 -0
  7. data/lib/engineyard/cli.rb +169 -0
  8. data/lib/engineyard/cli/api.rb +42 -0
  9. data/lib/engineyard/cli/error.rb +44 -0
  10. data/lib/engineyard/cli/ui.rb +96 -0
  11. data/lib/engineyard/config.rb +86 -0
  12. data/lib/engineyard/repo.rb +24 -0
  13. data/lib/vendor/thor.rb +244 -0
  14. data/lib/vendor/thor/actions.rb +275 -0
  15. data/lib/vendor/thor/actions/create_file.rb +103 -0
  16. data/lib/vendor/thor/actions/directory.rb +91 -0
  17. data/lib/vendor/thor/actions/empty_directory.rb +134 -0
  18. data/lib/vendor/thor/actions/file_manipulation.rb +223 -0
  19. data/lib/vendor/thor/actions/inject_into_file.rb +104 -0
  20. data/lib/vendor/thor/base.rb +540 -0
  21. data/lib/vendor/thor/core_ext/file_binary_read.rb +9 -0
  22. data/lib/vendor/thor/core_ext/hash_with_indifferent_access.rb +75 -0
  23. data/lib/vendor/thor/core_ext/ordered_hash.rb +100 -0
  24. data/lib/vendor/thor/error.rb +30 -0
  25. data/lib/vendor/thor/group.rb +271 -0
  26. data/lib/vendor/thor/invocation.rb +180 -0
  27. data/lib/vendor/thor/parser.rb +4 -0
  28. data/lib/vendor/thor/parser/argument.rb +67 -0
  29. data/lib/vendor/thor/parser/arguments.rb +150 -0
  30. data/lib/vendor/thor/parser/option.rb +128 -0
  31. data/lib/vendor/thor/parser/options.rb +169 -0
  32. data/lib/vendor/thor/rake_compat.rb +66 -0
  33. data/lib/vendor/thor/runner.rb +314 -0
  34. data/lib/vendor/thor/shell.rb +83 -0
  35. data/lib/vendor/thor/shell/basic.rb +239 -0
  36. data/lib/vendor/thor/shell/color.rb +108 -0
  37. data/lib/vendor/thor/task.rb +102 -0
  38. data/lib/vendor/thor/util.rb +230 -0
  39. data/lib/vendor/thor/version.rb +3 -0
  40. data/spec/engineyard/api_spec.rb +56 -0
  41. data/spec/engineyard/cli/api_spec.rb +44 -0
  42. data/spec/engineyard/cli_spec.rb +20 -0
  43. data/spec/engineyard/config_spec.rb +57 -0
  44. data/spec/engineyard/repo_spec.rb +52 -0
  45. data/spec/engineyard_spec.rb +7 -0
  46. data/spec/ey/deploy_spec.rb +65 -0
  47. data/spec/ey/ey_spec.rb +16 -0
  48. data/spec/spec.opts +2 -0
  49. data/spec/spec_helper.rb +40 -0
  50. data/spec/support/bundled_ey +10 -0
  51. data/spec/support/helpers.rb +46 -0
  52. 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
@@ -0,0 +1,7 @@
1
+ = ey
2
+
3
+ Description goes here.
4
+
5
+ == Copyright
6
+
7
+ Copyright (c) 2010 Engine Yard. See LICENSE for details.
data/bin/ey ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+ require 'engineyard/cli'
3
+
4
+ begin
5
+ EY::CLI.start
6
+ rescue EY::Error => e
7
+ EY.ui.print_exception(e)
8
+ exit(1)
9
+ rescue Interrupt => e
10
+ puts
11
+ EY.ui.print_exception(e)
12
+ EY.ui.say("Quitting...")
13
+ exit(1)
14
+ end
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