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