elastic_beans 0.9.1 → 0.10.0.alpha1

Sign up to get free protection for your applications and to get access to all the features.
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