elastic_beans 0.9.1 → 0.10.0.alpha1

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: c144928ae084c0496a67956390849f40aae25ed5
4
- data.tar.gz: '019811a87002509e40108fdc1d8cc76ae8fa0777'
3
+ metadata.gz: 44f1c4f70c8a932819315f3d02623b75423ff299
4
+ data.tar.gz: 270d9d55029d816b5f45f91b13d2ff3e89e6b456
5
5
  SHA512:
6
- metadata.gz: 5453396cb2fc49b0a58e06a824a37d4c985a93d8ac9d76cb430aba33323cf437063628c49cddc1b40ceadf1031522a020e5fb80caac29619e049700f68f0c797
7
- data.tar.gz: 0d6cddec37f72a87367d4b3816b1a763fa86d0aebe5e4e21b50754df97854667890767b45fa2397d78afd2753b467f23f2b340df183e9b878b368c22f903ea37
6
+ metadata.gz: c9808cfe16431ef5bd7ac8df11ac31e85331d36c0fefb89d24c6aa95bd91ca593762e09db378c3913a29bd0dd4d732cc4e0b831aaf39645684cbd123cc720acc
7
+ data.tar.gz: e223d9b76c7b922dd3271958458e86a100eaf45842c47012b33de0d4b647fc405f68d009f56daa7cb9d37365dec7230181b93631240fb0bbf0de8a26daad345c
data/Gemfile CHANGED
@@ -4,7 +4,6 @@ source 'https://rubygems.org'
4
4
  gemspec
5
5
 
6
6
  group :test do
7
- gem "net-ssh-gateway"
8
7
  gem "rails", "~> 5.0"
9
8
  gem "rspec_junit_formatter"
10
9
  gem "timecop"
data/README.md CHANGED
@@ -53,6 +53,10 @@ As the SDK documentation suggests, using environment variables is recommended.
53
53
  # Run one-off tasks
54
54
  beans exec -a myapp rake db:migrate
55
55
 
56
+ # SSH to an instance for debugging, tunneling through a bastion instance to reach the private network
57
+ beans ssh -a myapp webserver [-n INDEX] [-i IDENTITY_FILE] [-u USERNAME] \
58
+ [-b BASTION_HOST] [--bastion-identity-file BASTION_IDENTITY_FILE] [--bastion-username BASTION_USERNAME]
59
+
56
60
  # Update all existing environments and configuration
57
61
  beans configure -n myapp-networking -a myapp \
58
62
  [-b SECRET_KEY_BASE] [-d DATABASE_URL] [-k KEYPAIR] \
@@ -20,6 +20,7 @@ Gem::Specification.new do |spec|
20
20
 
21
21
  spec.add_dependency "aws-sdk", "~> 2.3"
22
22
  spec.add_dependency "nesty", "~> 1.0"
23
+ spec.add_dependency "net-ssh-gateway"
23
24
  spec.add_dependency "rails"
24
25
  spec.add_dependency "ruby-progressbar", "~> 1.2"
25
26
  spec.add_dependency "rubyzip", "~> 1.2"
@@ -200,6 +200,37 @@ class ElasticBeans::CLI < Thor
200
200
 
201
201
  map ["delvar", "rmvar"] => "unsetenv"
202
202
 
203
+ desc ElasticBeans::Command::Ssh::USAGE, ElasticBeans::Command::Ssh::DESC
204
+ long_desc ElasticBeans::Command::Ssh::LONG_DESC
205
+ option :application, aliases: %w(-a), required: true, desc: APPLICATION_DESC
206
+ option :bastion_host, aliases: %w(-b), desc: "The hostname of the bastion server in the VPC"
207
+ option :bastion_identity_file, desc: "The SSH private key to use to connect to the bastion server"
208
+ option :bastion_username, desc: "The username to use to connect to the bastion server"
209
+ option :identity_file, aliases: %w(-i), desc: "The SSH private key associated with the keypair used when creating the environment"
210
+ option :index, aliases: %w(-n), desc: "The 0-based index of the instance to connect to"
211
+ option :queue, aliases: %w(-q), desc: "The name of the queue to identify the worker environment to connect to, e.g. `default`"
212
+ option :username, aliases: %w(-u), desc: "The username to log into the instance with, e.g. `ec2-user`"
213
+ def ssh(environment_type)
214
+ @verbose = options[:verbose]
215
+ ElasticBeans::Command::Ssh.new(
216
+ application: application(
217
+ name: options[:application],
218
+ ),
219
+ bastion_host: options[:bastion_host],
220
+ bastion_identity_file: options[:bastion_identity_file],
221
+ bastion_username: options[:bastion_username],
222
+ identity_file: options[:identity_file],
223
+ index: options[:index],
224
+ queue: options[:queue],
225
+ username: options[:username],
226
+ ec2: ec2_client,
227
+ elastic_beanstalk: elastic_beanstalk_client,
228
+ ui: ui,
229
+ ).run(environment_type)
230
+ rescue StandardError => e
231
+ error(e)
232
+ end
233
+
203
234
  desc "talk", ""
204
235
  def talk
205
236
  ElasticBeans::Command::Talk.new(ui: ui).run
@@ -233,6 +264,10 @@ class ElasticBeans::CLI < Thor
233
264
  ::Aws::CloudFormation::Client.new
234
265
  end
235
266
 
267
+ def ec2_client
268
+ ::Aws::EC2::Client.new
269
+ end
270
+
236
271
  def elastic_beanstalk_client
237
272
  ::Aws::ElasticBeanstalk::Client.new
238
273
  end
@@ -34,9 +34,7 @@ Requires AWS credentials to be set in the environment, i.e. AWS_ACCESS_KEY_ID an
34
34
  )
35
35
  @environment_type = environment_type
36
36
  @dns = dns
37
- if @environment_type == "worker"
38
- @queue = queue || "default"
39
- end
37
+ @queue = queue
40
38
  @tags = tags
41
39
  @application = application
42
40
  @ui = ui
@@ -18,7 +18,7 @@ Requires AWS credentials to be set in the environment, i.e. AWS_ACCESS_KEY_ID an
18
18
  @application = application
19
19
  @minimum = minimum
20
20
  @maximum = maximum
21
- @queue = queue || "default"
21
+ @queue = queue
22
22
  @elastic_beanstalk = elastic_beanstalk
23
23
  @ui = ui
24
24
  end
@@ -0,0 +1,185 @@
1
+ require "net/ssh/gateway"
2
+ require "elastic_beans/error/environments_not_ready"
3
+
4
+ module ElasticBeans
5
+ module Command
6
+ # :nodoc: all
7
+ class Ssh
8
+ USAGE = "ssh ENVIRONMENT_TYPE"
9
+ DESC = "Connect to an instance for debugging, tunneling through a bastion server"
10
+ LONG_DESC = <<-LONG_DESC
11
+ Connect to an instance for debugging, tunneling through a bastion server.
12
+ Opens a login shell.
13
+
14
+ Requires AWS credentials to be set in the environment, i.e. AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY.
15
+ LONG_DESC
16
+
17
+ def initialize(
18
+ application:,
19
+ bastion_host:,
20
+ bastion_identity_file:,
21
+ bastion_username:,
22
+ identity_file:,
23
+ index:,
24
+ queue:,
25
+ username:,
26
+ ec2:,
27
+ elastic_beanstalk:,
28
+ ui:
29
+ )
30
+ @application = application
31
+ @bastion_host = bastion_host
32
+ @bastion_identity_file = bastion_identity_file
33
+ @bastion_username = bastion_username
34
+ @identity_file = identity_file
35
+ @index = index.to_i
36
+ @queue = queue
37
+ @username = username || "ec2-user"
38
+ @ec2 = ec2
39
+ @elastic_beanstalk = elastic_beanstalk
40
+ @ui = ui
41
+ end
42
+
43
+ def run(environment_type)
44
+ environment = ElasticBeans::Environment.new_by_type(
45
+ environment_type,
46
+ queue: queue,
47
+ application: application,
48
+ elastic_beanstalk: elastic_beanstalk,
49
+ )
50
+ instance_id = environment.instance_ids[index]
51
+ ui.debug { "In environment '#{environment.name}' found instance '#{instance_id}' at index '#{index}' of #{environment.instance_ids.inspect}" }
52
+ if instance_id.nil?
53
+ raise NoInstanceError.new(environment: environment, index: index)
54
+ end
55
+
56
+ begin
57
+ instance_ip = ec2.describe_instances(instance_ids: [instance_id]).reservations[0].instances[0].private_ip_address
58
+ rescue ::Aws::EC2::Errors::InvalidInstanceIDMalformed
59
+ raise NoInstanceError.new(environment: environment, index: index)
60
+ end
61
+ # It's possible a retry would just work, but I'd like to see this happen in reality before I assume that.
62
+ if instance_ip.nil?
63
+ raise TerminatedInstanceError.new(instance_id: instance_id, environment: environment)
64
+ end
65
+
66
+ ssh_args = ["ssh"]
67
+
68
+ if identity_file
69
+ ssh_args << "-i"
70
+ ssh_args << identity_file
71
+ end
72
+
73
+ gateway = nil
74
+ if bastion_host
75
+ ssh_args << "#{username}@localhost"
76
+ ssh_args += ["-o", "UserKnownHostsFile=/dev/null", "-o", "StrictHostKeyChecking=no"]
77
+ begin
78
+ bastion_options = {auth_methods: ["publickey"]}
79
+ if bastion_identity_file
80
+ bastion_options[:key_data] = File.read(bastion_identity_file)
81
+ end
82
+
83
+ ui.info("Connecting to '#{bastion_username}@#{bastion_host}'...")
84
+ gateway = Net::SSH::Gateway.new(
85
+ bastion_host,
86
+ bastion_username,
87
+ bastion_options
88
+ )
89
+ port = gateway.open(instance_ip, 22)
90
+ ui.debug { "Opened gateway to '#{instance_ip}' on port '#{port}'" }
91
+ ssh_args += ["-p", port.to_s]
92
+ rescue Net::SSH::AuthenticationFailed => e
93
+ raise BastionAuthenticationError.new(cause: e)
94
+ end
95
+ else
96
+ ssh_args << "#{username}@#{instance_ip}"
97
+ end
98
+
99
+ ui.info("Connecting to '#{username}@#{instance_ip}'...")
100
+ pid = Process.spawn(*ssh_args)
101
+ _, status = Process.waitpid2(pid)
102
+ unless status.success?
103
+ raise SshFailedError
104
+ end
105
+ ensure
106
+ gateway.close(port) if gateway
107
+ end
108
+
109
+ private
110
+
111
+ attr_reader(
112
+ :application,
113
+ :bastion_host,
114
+ :bastion_identity_file,
115
+ :bastion_username,
116
+ :identity_file,
117
+ :index,
118
+ :queue,
119
+ :username,
120
+ :ec2,
121
+ :elastic_beanstalk,
122
+ :ui,
123
+ )
124
+
125
+ class BastionAuthenticationError < ElasticBeans::Error
126
+ def initialize(cause:)
127
+ @cause = cause
128
+ end
129
+
130
+ def message
131
+ <<-MESSAGE
132
+ #{@cause.message}
133
+
134
+ Please check the --bastion-identity-file or --bastion-username options and try again.
135
+
136
+ #{command_help "ssh"}
137
+ MESSAGE
138
+ end
139
+ end
140
+
141
+ class NoInstanceError < ElasticBeans::Error
142
+ def initialize(environment:, index:)
143
+ @environment = environment
144
+ @index = index
145
+ end
146
+
147
+ def message
148
+ msg = "The '#{@environment.name}' environment has no instance at index #{@index}.\n"
149
+ if @index > 0
150
+ msg << "Try again with a lower index.\n"
151
+ else
152
+ msg << "Wait for instances to be created and try again.\n"
153
+ end
154
+ msg << <<-MESSAGE
155
+
156
+ #{command_help "ssh"}
157
+ MESSAGE
158
+ msg
159
+ end
160
+ end
161
+
162
+ class SshFailedError < ElasticBeans::Error
163
+ def message
164
+ ""
165
+ end
166
+ end
167
+
168
+ class TerminatedInstanceError < ElasticBeans::Error
169
+ def initialize(instance_id:, environment:)
170
+ @instance_id = instance_id
171
+ @environment = environment
172
+ end
173
+
174
+ def message
175
+ <<-MESSAGE
176
+ The instance '#{@instance_id}' in the environment '#{@environment.name}' has been terminated.
177
+ Please try again in a moment.
178
+
179
+ #{command_help "ssh"}
180
+ MESSAGE
181
+ end
182
+ end
183
+ end
184
+ end
185
+ end
@@ -8,6 +8,7 @@ require "elastic_beans/command/scale"
8
8
  require "elastic_beans/command/get_env"
9
9
  require "elastic_beans/command/set_env"
10
10
  require "elastic_beans/command/unset_env"
11
+ require "elastic_beans/command/ssh"
11
12
  require "elastic_beans/command/talk"
12
13
  require "elastic_beans/command/version"
13
14
 
@@ -12,9 +12,9 @@ module ElasticBeans
12
12
 
13
13
  attr_reader :queue
14
14
 
15
- def initialize(name, queue:, **_)
15
+ def initialize(name, queue: nil, **_)
16
16
  super
17
- @queue = queue
17
+ @queue = queue || "default"
18
18
  end
19
19
 
20
20
  # Returns "worker"
@@ -40,7 +40,8 @@ module ElasticBeans
40
40
  when "webserver"
41
41
  ElasticBeans::Environment::Webserver.new(name, application: application, **args)
42
42
  when "worker"
43
- name = "#{name}-#{args[:queue]}"
43
+ queue_name = args[:queue] || "default"
44
+ name = "#{name}-#{queue_name}"
44
45
  ElasticBeans::Environment::Worker.new(name, application: application, **args)
45
46
  else
46
47
  raise UnknownEnvironmentType.new(environment_type: type)
@@ -78,7 +79,6 @@ module ElasticBeans
78
79
  ElasticBeans::Environment::Worker.new(
79
80
  environment.environment_name,
80
81
  application: application,
81
- queue: "default",
82
82
  **args,
83
83
  )
84
84
  else
@@ -211,6 +211,15 @@ module ElasticBeans
211
211
  environment.cname
212
212
  end
213
213
 
214
+ # Returns the instance IDs that are part of this environment.
215
+ #
216
+ # Raises an error if the environment does not exist.
217
+ def instance_ids
218
+ environment_resources.instances.map(&:id)
219
+ rescue ::Aws::ElasticBeanstalk::Errors::InvalidParameterValue
220
+ raise MissingEnvironmentError.new(environment: self, application: application)
221
+ end
222
+
214
223
  # Returns the status of the environment in Elastic Beanstalk.
215
224
  #
216
225
  # Raises an error if the environment does not exist.
@@ -242,6 +251,15 @@ module ElasticBeans
242
251
  retry
243
252
  end
244
253
 
254
+ def environment_resources
255
+ elastic_beanstalk.describe_environment_resources(
256
+ environment_name: name,
257
+ ).environment_resources
258
+ rescue ::Aws::ElasticBeanstalk::Errors::Throttling
259
+ sleep 5
260
+ retry
261
+ end
262
+
245
263
  def template_name
246
264
  TEMPLATE_NAME
247
265
  end
@@ -324,10 +342,13 @@ Please re-run `#{command_as_string "configure"}`.
324
342
 
325
343
  def message
326
344
  require "elastic_beans/command/create"
327
- <<-MESSAGE
328
- Environment `#{@environment.name}' does not exist.
329
- Please run `#{command_as_string "create #{@environment.type} -a #{@application.name}"}`.
330
- MESSAGE
345
+ msg = "Environment `#{@environment.name}' does not exist.\n"
346
+ msg << "Please run `#{command_as_string "create #{@environment.type}"}"
347
+ if @environment.respond_to?(:queue)
348
+ msg << " -q #{@environment.queue}"
349
+ end
350
+ msg << " -a #{@application.name}`.\n"
351
+ msg
331
352
  end
332
353
  end
333
354
 
@@ -1,3 +1,3 @@
1
1
  module ElasticBeans
2
- VERSION = "0.9.1"
2
+ VERSION = "0.10.0.alpha1"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: elastic_beans
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.9.1
4
+ version: 0.10.0.alpha1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Adam Stegman
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2017-03-10 00:00:00.000000000 Z
11
+ date: 2017-04-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: aws-sdk
@@ -38,6 +38,20 @@ dependencies:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
40
  version: '1.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: net-ssh-gateway
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
41
55
  - !ruby/object:Gem::Dependency
42
56
  name: rails
43
57
  requirement: !ruby/object:Gem::Requirement
@@ -186,6 +200,7 @@ files:
186
200
  - lib/elastic_beans/command/restart.rb
187
201
  - lib/elastic_beans/command/scale.rb
188
202
  - lib/elastic_beans/command/set_env.rb
203
+ - lib/elastic_beans/command/ssh.rb
189
204
  - lib/elastic_beans/command/talk.rb
190
205
  - lib/elastic_beans/command/unset_env.rb
191
206
  - lib/elastic_beans/command/version.rb
@@ -232,12 +247,12 @@ required_ruby_version: !ruby/object:Gem::Requirement
232
247
  version: '0'
233
248
  required_rubygems_version: !ruby/object:Gem::Requirement
234
249
  requirements:
235
- - - ">="
250
+ - - ">"
236
251
  - !ruby/object:Gem::Version
237
- version: '0'
252
+ version: 1.3.1
238
253
  requirements: []
239
254
  rubyforge_project:
240
- rubygems_version: 2.5.2
255
+ rubygems_version: 2.6.11
241
256
  signing_key:
242
257
  specification_version: 4
243
258
  summary: Elastic Beanstalk environment orchestration for a Rails app