ey-core 3.1.2 → 3.1.3

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 (63) hide show
  1. data/.ruby-version +1 -1
  2. data/.travis.yml +1 -0
  3. data/Gemfile +0 -2
  4. data/examples/add_instance.rb +74 -0
  5. data/examples/boot_env.rb +60 -0
  6. data/examples/stop_env.rb +51 -0
  7. data/examples/terminate_instance.rb +58 -0
  8. data/lib/ey-core/cli/accounts.rb +14 -6
  9. data/lib/ey-core/cli/applications.rb +32 -12
  10. data/lib/ey-core/cli/console.rb +24 -10
  11. data/lib/ey-core/cli/current_user.rb +13 -5
  12. data/lib/ey-core/cli/deploy.rb +110 -52
  13. data/lib/ey-core/cli/environments.rb +34 -12
  14. data/lib/ey-core/cli/errors.rb +10 -6
  15. data/lib/ey-core/cli/help.rb +30 -0
  16. data/lib/ey-core/cli/helpers/archive.rb +70 -0
  17. data/lib/ey-core/cli/helpers/chef.rb +35 -0
  18. data/lib/ey-core/cli/helpers/core.rb +195 -0
  19. data/lib/ey-core/cli/helpers/deprecated.rb +39 -0
  20. data/lib/ey-core/cli/helpers/log_streaming.rb +41 -0
  21. data/lib/ey-core/cli/helpers/stream_printer.rb +42 -0
  22. data/lib/ey-core/cli/init.rb +11 -8
  23. data/lib/ey-core/cli/login.rb +33 -21
  24. data/lib/ey-core/cli/logout.rb +18 -10
  25. data/lib/ey-core/cli/logs.rb +57 -35
  26. data/lib/ey-core/cli/main.rb +52 -15
  27. data/lib/ey-core/cli/recipes.rb +5 -87
  28. data/lib/ey-core/cli/recipes/apply.rb +83 -43
  29. data/lib/ey-core/cli/recipes/download.rb +48 -22
  30. data/lib/ey-core/cli/recipes/main.rb +21 -0
  31. data/lib/ey-core/cli/recipes/upload.rb +56 -23
  32. data/lib/ey-core/cli/scp.rb +11 -8
  33. data/lib/ey-core/cli/servers.rb +37 -15
  34. data/lib/ey-core/cli/ssh.rb +127 -70
  35. data/lib/ey-core/cli/status.rb +54 -14
  36. data/lib/ey-core/cli/subcommand.rb +47 -108
  37. data/lib/ey-core/cli/timeout_deploy.rb +56 -26
  38. data/lib/ey-core/cli/version.rb +13 -5
  39. data/lib/ey-core/cli/web.rb +7 -7
  40. data/lib/ey-core/cli/web/disable.rb +46 -20
  41. data/lib/ey-core/cli/web/enable.rb +40 -17
  42. data/lib/ey-core/cli/web/main.rb +21 -0
  43. data/lib/ey-core/cli/web/restart.rb +34 -15
  44. data/lib/ey-core/cli/whoami.rb +11 -3
  45. data/lib/ey-core/mock/searching.rb +4 -0
  46. data/lib/ey-core/model.rb +5 -0
  47. data/lib/ey-core/models/deployment.rb +7 -0
  48. data/lib/ey-core/models/environment.rb +5 -0
  49. data/lib/ey-core/models/request.rb +2 -0
  50. data/lib/ey-core/models/user.rb +2 -0
  51. data/lib/ey-core/requests/get_servers.rb +1 -1
  52. data/lib/ey-core/response.rb +4 -0
  53. data/lib/ey-core/subscribable.rb +3 -3
  54. data/lib/ey-core/version.rb +1 -1
  55. data/spec/ey-core/cli/accounts_spec.rb +20 -0
  56. data/spec/ey-core/cli/recipes/apply_spec.rb +4 -17
  57. data/spec/ey-core/cli/recipes/download_spec.rb +93 -0
  58. data/spec/ey-core/cli/recipes/upload_spec.rb +80 -0
  59. data/spec/servers_spec.rb +15 -0
  60. data/spec/spec_helper.rb +7 -0
  61. data/spec/support/cli_helpers.rb +38 -2
  62. metadata +116 -53
  63. checksums.yaml +0 -7
@@ -1,17 +1,39 @@
1
- class Ey::Core::Cli::Environments < Ey::Core::Cli::Subcommand
2
- title "environments"
3
- summary "Retrieve a list of Engine Yard environments that you have access to."
4
- option :account, short: 'c', long: 'account', description: 'Filter by account name or id', argument: 'Account'
1
+ require 'ey-core/cli/subcommand'
2
+ require 'ey-core/cli/helpers/stream_printer'
5
3
 
6
- def handle
7
- environments = if option(:account)
8
- core_account_for(options).environments.all
9
- else
10
- current_accounts.map(&:environments).flatten.sort_by(&:id)
11
- end
4
+ module Ey
5
+ module Core
6
+ module Cli
7
+ class Environments < Subcommand
8
+ include Ey::Core::Cli::Helpers::StreamPrinter
9
+ title "environments"
10
+ summary "Retrieve a list of Engine Yard environments that you have access to."
12
11
 
12
+ option :account,
13
+ short: 'c',
14
+ long: 'account',
15
+ description: 'Filter by account name or id',
16
+ argument: 'Account'
13
17
 
14
- table_data = TablePrint::Printer.new(environments, [{id: {width: 10}}, :name])
15
- puts table_data.table_print
18
+ def handle
19
+ if option(:account)
20
+ stream_print("ID" => 10, "Name" => 50) do |printer|
21
+ core_account.environments.each_entry do |env|
22
+ printer.print(env.id, env.name)
23
+ end
24
+ end
25
+ else
26
+ stream_print("ID" => 10, "Name" => 50, "Account" => 50) do |printer|
27
+ core_accounts.each_entry do |account|
28
+ account.environments.each_entry do |env|
29
+ printer.print(env.id, env.name, account.name)
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
35
+
36
+ end
37
+ end
16
38
  end
17
39
  end
@@ -1,7 +1,11 @@
1
- class Ey::Core::Cli::Errors
2
- ::Ey::Core::Cli::RecipesNotFound = Class.new(ArgumentError)
3
- ::Ey::Core::Cli::NoCommand = Class.new(ArgumentError)
4
- ::Ey::Core::Cli::NoRepository = Class.new(ArgumentError)
5
- ::Ey::Core::Cli::RecipesExist = Class.new(ArgumentError)
6
- ::Ey::Core::Cli::AmbiguousSearch = Class.new(ArgumentError)
1
+ module Ey
2
+ module Core
3
+ module Cli
4
+ RecipesNotFound = Class.new(ArgumentError)
5
+ NoCommand = Class.new(ArgumentError)
6
+ NoRepository = Class.new(ArgumentError)
7
+ RecipesExist = Class.new(ArgumentError)
8
+ AmbiguousSearch = Class.new(ArgumentError)
9
+ end
10
+ end
7
11
  end
@@ -0,0 +1,30 @@
1
+ require 'belafonte'
2
+
3
+ module Ey
4
+ module Core
5
+ module Cli
6
+ class Help < Belafonte::App
7
+ title 'help'
8
+ summary 'Print out the help docs'
9
+
10
+ arg :command_line,
11
+ times: :unlimited
12
+
13
+ def handle
14
+ Main.new(
15
+ command_line.unshift('-h'),
16
+ stdin,
17
+ stdout,
18
+ stderr,
19
+ kernel
20
+ ).execute!
21
+ end
22
+
23
+ private
24
+ def command_line
25
+ arg(:command_line)
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,70 @@
1
+ require 'stringio'
2
+ require 'rubygems/package'
3
+ require 'zlib'
4
+
5
+ module Ey
6
+ module Core
7
+ module Cli
8
+ module Helpers
9
+ module Archive
10
+ def gzip(tarfile)
11
+ gz = StringIO.new("")
12
+ zipper = Zlib::GzipWriter.new(gz)
13
+ zipper.write tarfile.string
14
+ zipper.close # this is necessary!
15
+
16
+ # z was closed to write the gzip footer, so
17
+ # now we need a new StringIO
18
+ StringIO.new gz.string
19
+ end
20
+
21
+ def archive_directory(path)
22
+ tarfile = StringIO.new("")
23
+ Gem::Package::TarWriter.new(tarfile) do |tar|
24
+ Dir[File.join(path, "**/*")].each do |file|
25
+ mode = File.stat(file).mode
26
+ relative_file = "cookbooks/#{file.sub(/^#{Regexp::escape path}\/?/, '')}"
27
+
28
+ if File.directory?(file)
29
+ tar.mkdir relative_file, mode
30
+ else
31
+ tar.add_file relative_file, mode do |tf|
32
+ File.open(file, "rb") { |f| tf.write f.read }
33
+ end
34
+ end
35
+ end
36
+ end
37
+
38
+ tarfile.rewind
39
+ gzip(tarfile)
40
+ end
41
+
42
+ def ungzip(tarfile)
43
+ zipped = Zlib::GzipReader.new(tarfile)
44
+ unzipped = StringIO.new(zipped.read)
45
+ zipped.close
46
+ unzipped
47
+ end
48
+
49
+ def untar(io, destination)
50
+ Gem::Package::TarReader.new io do |tar|
51
+ tar.each do |tarfile|
52
+ destination_file = File.join destination, tarfile.full_name
53
+
54
+ if tarfile.directory?
55
+ FileUtils.mkdir_p destination_file
56
+ else
57
+ destination_directory = File.dirname(destination_file)
58
+ FileUtils.mkdir_p destination_directory unless File.directory?(destination_directory)
59
+ File.open destination_file, "wb" do |file|
60
+ file.print tarfile.read
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,35 @@
1
+ require 'ey-core/cli/errors'
2
+
3
+ module Ey
4
+ module Core
5
+ module Cli
6
+ module Helpers
7
+ module Chef
8
+ def run_chef(type, environment)
9
+ request = environment.apply(type)
10
+ puts "Started #{type} chef run".green
11
+ request.wait_for { |r| r.ready? }
12
+ if request.successful
13
+ puts "#{type.capitalize} chef run completed".green
14
+ else
15
+ puts "#{type.capitalize} chef run failed".red
16
+ ap request
17
+ end
18
+ end
19
+
20
+ def upload_recipes(environment, path="cookbooks/")
21
+ recipes_path = Pathname.new(path)
22
+
23
+ if recipes_path.exist? && recipes_path.to_s.match(/\.(tgz|tar\.gz)/)
24
+ environment.upload_recipes(recipes_path)
25
+ elsif recipes_path.exist?
26
+ environment.upload_recipes(archive_directory(path))
27
+ else
28
+ raise RecipesNotFound, "Recipes file not found: #{recipes_path}"
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,195 @@
1
+ require 'colorize'
2
+ require 'ey-core/cli/errors'
3
+
4
+ module Ey
5
+ module Core
6
+ module Cli
7
+ module Helpers
8
+ module Core
9
+ module ClassMethods
10
+ def core_file
11
+ @core_file ||= File.expand_path("~/.ey-core")
12
+ end
13
+
14
+ def eyrc
15
+ @eyrc ||= File.expand_path("~/.eyrc")
16
+ end
17
+ end
18
+
19
+ def core_url
20
+ env_url = ENV["CORE_URL"] || ENV["CLOUD_URL"]
21
+ (env_url && File.join(env_url, '/')) || "https://api.engineyard.com/"
22
+ end
23
+
24
+ def longest_length_by_name(collection)
25
+ collection.map(&:name).group_by(&:size).max.last.length
26
+ end
27
+
28
+ def core_yaml
29
+ @core_yaml ||= YAML.load_file(self.class.core_file) || {}
30
+ rescue Errno::ENOENT => e
31
+ puts "Creating #{self.class.core_file}".yellow
32
+ FileUtils.touch(self.class.core_file)
33
+ retry
34
+ end
35
+
36
+ def operator(options)
37
+ if options[:account]
38
+ core_account
39
+ elsif ENV["STAFF"]
40
+ core_client
41
+ else
42
+ core_client.users.current
43
+ end
44
+ end
45
+
46
+ def core_operator_and_environment_for(options={})
47
+ unless options[:environment]
48
+ raise "--environment is required (for a list of environments, try `ey environments`)"
49
+ end
50
+ operator = operator(options)
51
+ environment = nil
52
+ if options[:environment].to_i.to_s == options[:environment]
53
+ environment = operator.environments.get(options[:environment])
54
+ end
55
+ unless environment
56
+ candidate_envs = operator.environments.all(name: options[:environment])
57
+ if candidate_envs.size > 1
58
+ raise "Multiple matching environments found named '#{options[:environment]}', please specify --account"
59
+ else
60
+ environment = candidate_envs.first
61
+ end
62
+ end
63
+ unless environment
64
+ raise "environment '#{options[:environment]}' not found (for a list of environments, try `ey environments`)"
65
+ end
66
+ [operator, environment]
67
+ end
68
+
69
+ def core_environment_for(options={})
70
+ core_client.environments.get(options[:environment]) || core_client.environments.first(name: options[:environment])
71
+ end
72
+
73
+ def core_server_for(options={})
74
+ operator = options.fetch(:operator, core_client)
75
+ operator.servers.get(options[:server]) || operator.servers.first(provisioned_id: options[:server])
76
+ end
77
+
78
+ def core_application_for(environment, options={})
79
+ candidate_apps = nil
80
+ unless options[:app]
81
+ candidate_apps = environment.applications.map(&:name)
82
+ if candidate_apps.size == 1
83
+ options[:app] = candidate_apps.first
84
+ else
85
+ raise "--app is required (Candidate apps on environment #{environment.name}: #{candidate_apps.join(', ')})"
86
+ end
87
+ end
88
+
89
+ app = begin
90
+ Integer(options[:app])
91
+ rescue
92
+ options[:app]
93
+ end
94
+
95
+ if app.is_a?(Integer)
96
+ environment.applications.get(app)
97
+ else
98
+ applications = environment.applications.all(name: app)
99
+ if applications.count == 1
100
+ applications.first
101
+ else
102
+ error_msg = [
103
+ "Found multiple applications that matched that search.",
104
+ "Please be more specific by specifying the account, environment, and application name.",
105
+ "Matching applications: #{applications.map(&:name)}.",
106
+ ]
107
+ if candidate_apps
108
+ error_msg << "applications on this environment: #{candidate_apps}"
109
+ end
110
+ raise Ey::Core::Cli::AmbiguousSearch.new(error_msg.join(" "))
111
+ end
112
+ end
113
+ end
114
+
115
+ def unauthenticated_core_client
116
+ @unauthenticated_core_client ||= Ey::Core::Client.new(token: nil, url: core_url)
117
+ end
118
+
119
+ def core_client
120
+ @core_client ||= begin
121
+ opts = {url: core_url, config_file: self.class.core_file}
122
+ if ENV["DEBUG"]
123
+ opts[:logger] = ::Logger.new(STDOUT)
124
+ end
125
+ Ey::Core::Client.new(opts)
126
+ end
127
+ rescue RuntimeError => e
128
+ if legacy_token = e.message.match(/missing token/i) && eyrc_yaml["api_token"]
129
+ puts "Found legacy .eyrc token. Migrating to core file".green
130
+ write_core_yaml(legacy_token)
131
+ retry
132
+ elsif e.message.match(/missing token/i)
133
+ abort "Missing credentials: Run 'ey login' to retrieve your Engine Yard Cloud API token.".yellow
134
+ else
135
+ raise e
136
+ end
137
+ end
138
+
139
+ def core_account
140
+ @_core_account ||= begin
141
+ if options[:account]
142
+ found = core_client.accounts.get(options[:account]) ||
143
+ core_client.users.current.accounts.first(name: options[:account])
144
+ if ENV["STAFF"]
145
+ found ||= core_client.accounts.first(name: options[:account])
146
+ end
147
+ unless found
148
+ account_not_found_error_message = "Couldn't find account '#{options[:account]}'"
149
+ if core_client.users.current.staff && !ENV["STAFF"]
150
+ account_not_found_error_message += " (set environment variable STAFF=1 to search all accounts)"
151
+ end
152
+ raise account_not_found_error_message
153
+ end
154
+ found
155
+ else
156
+ if core_accounts.size == 1
157
+ core_accounts.first
158
+ else
159
+ raise "Please specify --account (options: #{core_accounts.map(&:name).join(', ')})"
160
+ end
161
+ end
162
+ end
163
+ end
164
+
165
+ def core_accounts
166
+ @_core_accounts ||= begin
167
+ if ENV["STAFF"]
168
+ core_client.accounts
169
+ else
170
+ core_client.users.current.accounts
171
+ end
172
+ end
173
+ end
174
+
175
+ def write_core_yaml(token=nil)
176
+ core_yaml[core_url] = token if token
177
+ File.open(self.class.core_file, "w") {|file|
178
+ file.puts core_yaml.to_yaml
179
+ }
180
+ end
181
+
182
+ def eyrc_yaml
183
+ @eyrc_yaml ||= YAML.load_file(self.class.eyrc) || {}
184
+ rescue Errno::ENOENT # we don't really care if this doesn't exist
185
+ {}
186
+ end
187
+
188
+ def self.included(base)
189
+ base.extend(ClassMethods)
190
+ end
191
+ end
192
+ end
193
+ end
194
+ end
195
+ end
@@ -0,0 +1,39 @@
1
+ require 'colorize'
2
+
3
+ module Ey
4
+ module Core
5
+ module Cli
6
+ module Helpers
7
+ module Deprecated
8
+ # Class-level helpers pulled into an including class
9
+ module ClassMethods
10
+
11
+ # A helper for deprecating a Belafonte-based command
12
+ #
13
+ # @param deprecated_title [String] the title of the command
14
+ #
15
+ # @return [NilClass] this method is not expected to return a value
16
+ def deprecate(deprecated_title)
17
+ title deprecated_title
18
+ summary 'This command has been deprecated'
19
+ description <<-DESCRIPTION
20
+ The #{meta[:title]} command has been deprecated. We apologize for any inconvenience.
21
+ DESCRIPTION
22
+ end
23
+ end
24
+
25
+ # Module magic
26
+ # @api private
27
+ def self.included(base)
28
+ base.extend(ClassMethods)
29
+ end
30
+
31
+ # Terminate the deprecated command with a message
32
+ def handle
33
+ abort "This command is deprecated".red
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,41 @@
1
+ module Ey
2
+ module Core
3
+ module Cli
4
+ module Helpers
5
+ module LogStreaming
6
+
7
+ def stream_deploy_log(request)
8
+ if request.finished_at
9
+ return finished_request(request)
10
+ end
11
+ unless request.read_channel
12
+ puts "Unable to stream log (streaming not enabled for this deploy)".yellow
13
+ return
14
+ end
15
+ request.subscribe { |m| print m["message"] if m.is_a?(Hash) }
16
+ puts "" # fix console output from stream
17
+ finished_request(request)
18
+ end
19
+
20
+ def finished_request(request)
21
+ if request.successful
22
+ if request.resource.successful
23
+ puts "Deploy successful!".green
24
+ else
25
+ puts "Deploy failed!".red
26
+ end
27
+ else
28
+ abort <<-EOF
29
+ Deploy failed!
30
+ Request output:
31
+ #{request.message}
32
+ EOF
33
+ .red
34
+ end
35
+ end
36
+
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end