aptible-cli 0.13.0 → 0.14.0

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: 85166609cc820ace0f7d804059e47335a9e9dd04
4
- data.tar.gz: aff75a2d86c878dd185e18a0b5eeb153328ee248
3
+ metadata.gz: a6a4807415f02ddc44c0472480e3452ff0945d4a
4
+ data.tar.gz: 7bc74d1f880230acbbec467bdb0c8802a8d3b8cb
5
5
  SHA512:
6
- metadata.gz: 0760707d72ec0f5795ca9f63211a778978a555b681827359e3634e17e3129e43826d2f68beebd34fdfc95bfb234375f66d3a230012a7e6cff52c67c37f8d7e87
7
- data.tar.gz: 482ddca8bf3c13ffd67e5c4398b335fdfcbeaac44d509e64193a5e671ba471083b577b5f7d0713a4f27176c556ed64f5393ed770e0e64e96e8d075d2b5f0afe1
6
+ metadata.gz: c8477ebfa7cf401acdcc3e49d37e06a633489e6cbf44318f34d39bb2050e1e18452c965535964b6bc70b94c9ee9a4bcfe91131bb05c63db87ea32ddce021b309
7
+ data.tar.gz: 3cc698ae24440386dc5063560eb4af84d295745d043f9888cbc8104a8f6f51470231ceaa26edffb780b5fcbbf8df6c3f22480b6e1497f6d1f9065536fd57bd9f
data/README.md CHANGED
@@ -52,10 +52,20 @@ Commands:
52
52
  aptible db:tunnel HANDLE # Create a local tunnel to a database
53
53
  aptible db:url HANDLE # Display a database URL
54
54
  aptible deploy [OPTIONS] [VAR1=VAL1] [VAR=VAL2] ... # Deploy an app
55
- aptible domains # Print an app's current virtual domains
55
+ aptible domains # Print an app's current virtual domains - DEPRECATED
56
+ aptible endpoints:database:create DATABASE # Create a Database Endpoint
57
+ aptible endpoints:deprovision [--app APP | --database DATABASE] ENDPOINT_HOSTNAME # Deprovision an App or Database Endpoint
58
+ aptible endpoints:https:create [--app APP] SERVICE # Create an App HTTPS Endpoint
59
+ aptible endpoints:https:modify [--app APP] ENDPOINT_HOSTNAME # Modify an App HTTPS Endpoint
60
+ aptible endpoints:list [--app APP | --database DATABASE] # List Endpoints for an App or Database
61
+ aptible endpoints:renew [--app APP] ENDPOINT_HOSTNAME # Renew an App Managed TLS Endpoint
62
+ aptible endpoints:tcp:create [--app APP] SERVICE # Create an App TCP Endpoint
63
+ aptible endpoints:tcp:modify [--app APP] ENDPOINT_HOSTNAME # Modify an App TCP Endpoint
64
+ aptible endpoints:tls:create [--app APP] SERVICE # Create an App TLS Endpoint
65
+ aptible endpoints:tls:modify [--app APP] ENDPOINT_HOSTNAME # Modify an App TLS Endpoint
56
66
  aptible help [COMMAND] # Describe available commands or one specific command
57
67
  aptible login # Log in to Aptible
58
- aptible logs # Follows logs from a running app or database
68
+ aptible logs [--app APP | --database DATABASE] # Follows logs from a running app or database
59
69
  aptible operation:cancel OPERATION_ID # Cancel a running operation
60
70
  aptible ps # Display running processes for an app - DEPRECATED
61
71
  aptible rebuild # Rebuild an app, and restart its services
data/aptible-cli.gemspec CHANGED
@@ -20,9 +20,10 @@ Gem::Specification.new do |spec|
20
20
  spec.test_files = spec.files.grep(%r{spec/})
21
21
  spec.require_paths = ['lib']
22
22
 
23
- spec.add_dependency 'aptible-resource', '~> 0.4.0'
24
- spec.add_dependency 'aptible-api', '~> 0.9.27'
25
- spec.add_dependency 'aptible-auth', '~> 0.11.13'
23
+ spec.add_dependency 'aptible-resource', '~> 1.0', '>= 1.0.1'
24
+ spec.add_dependency 'aptible-api', '~> 1.0'
25
+ spec.add_dependency 'aptible-auth', '~> 1.0'
26
+ spec.add_dependency 'aptible-billing', '~> 1.0'
26
27
  spec.add_dependency 'thor', '~> 0.19.1'
27
28
  spec.add_dependency 'git'
28
29
  spec.add_dependency 'term-ansicolor'
@@ -11,6 +11,9 @@ require_relative 'helpers/operation'
11
11
  require_relative 'helpers/environment'
12
12
  require_relative 'helpers/app'
13
13
  require_relative 'helpers/database'
14
+ require_relative 'helpers/app_or_database'
15
+ require_relative 'helpers/vhost'
16
+ require_relative 'helpers/vhost/option_set_builder'
14
17
  require_relative 'helpers/tunnel'
15
18
 
16
19
  require_relative 'subcommands/apps'
@@ -26,6 +29,7 @@ require_relative 'subcommands/ssh'
26
29
  require_relative 'subcommands/backup'
27
30
  require_relative 'subcommands/operation'
28
31
  require_relative 'subcommands/inspect'
32
+ require_relative 'subcommands/endpoints'
29
33
 
30
34
  module Aptible
31
35
  module CLI
@@ -47,6 +51,7 @@ module Aptible
47
51
  include Subcommands::Backup
48
52
  include Subcommands::Operation
49
53
  include Subcommands::Inspect
54
+ include Subcommands::Endpoints
50
55
 
51
56
  # Forward return codes on failures.
52
57
  def self.exit_on_failure?
@@ -116,8 +121,10 @@ module Aptible
116
121
  private
117
122
 
118
123
  def deprecated(msg)
119
- say "DEPRECATION NOTICE: #{msg}"
120
- say 'Please contact support@aptible.com with any questions.'
124
+ $stderr.puts yellow([
125
+ "DEPRECATION NOTICE: #{msg}",
126
+ 'Please contact support@aptible.com with any questions.'
127
+ ].join("\n"))
121
128
  end
122
129
 
123
130
  def nag_toolbelt
@@ -132,6 +132,25 @@ module Aptible
132
132
  end
133
133
  end
134
134
 
135
+ def ensure_service(options, type)
136
+ app = ensure_app(options)
137
+ service = app.services.find { |s| s.process_type == type }
138
+
139
+ if service.nil?
140
+ valid_types = if app.services.empty?
141
+ 'NONE (deploy the app first)'
142
+ else
143
+ app.services.map(&:process_type).join(', ')
144
+ end
145
+
146
+ raise Thor::Error, "Service with type #{type} does not " \
147
+ "exist for app #{app.handle}. Valid " \
148
+ "types: #{valid_types}."
149
+ end
150
+
151
+ service
152
+ end
153
+
135
154
  def apps_from_handle(handle, environment)
136
155
  if environment
137
156
  environment.apps
@@ -0,0 +1,34 @@
1
+ module Aptible
2
+ module CLI
3
+ module Helpers
4
+ module AppOrDatabase
5
+ include Helpers::App
6
+ include Helpers::Database
7
+
8
+ module ClassMethods
9
+ def app_or_database_options
10
+ app_options
11
+ option :database
12
+ end
13
+ end
14
+
15
+ def ensure_app_or_database(options = {})
16
+ if options[:app] && options[:database]
17
+ m = 'You must specify only one of --app and --database'
18
+ raise Thor::Error, m
19
+ end
20
+
21
+ if options[:database]
22
+ ensure_database(options.merge(db: options[:database]))
23
+ else
24
+ ensure_app(options)
25
+ end
26
+ end
27
+
28
+ def self.included(base)
29
+ base.extend(ClassMethods)
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,83 @@
1
+ module Aptible
2
+ module CLI
3
+ module Helpers
4
+ module Vhost
5
+ def explain_vhost(service, vhost)
6
+ say "Service: #{service.process_type}"
7
+ say "Hostname: #{vhost.external_host}"
8
+ say "Status: #{vhost.status}"
9
+
10
+ case vhost.type
11
+ when 'tcp', 'tls'
12
+ ports = if vhost.container_ports.any?
13
+ vhost.container_ports.map(&:to_s).join(' ')
14
+ else
15
+ 'all'
16
+ end
17
+ say "Type: #{vhost.type}"
18
+ say "Ports: #{ports}"
19
+ when 'http', 'http_proxy_protocol'
20
+ port = vhost.container_port ? vhost.container_port : 'default'
21
+ say 'Type: https'
22
+ say "Port: #{port}"
23
+ end
24
+
25
+ say "Internal: #{vhost.internal}"
26
+
27
+ ip_whitelist = if vhost.ip_whitelist.any?
28
+ vhost.ip_whitelist.join(' ')
29
+ else
30
+ 'all traffic'
31
+ end
32
+ say "IP Whitelist: #{ip_whitelist}"
33
+
34
+ say "Default Domain Enabled: #{vhost.default}"
35
+ say "Default Domain: #{vhost.virtual_domain}" if vhost.default
36
+
37
+ say "Managed TLS Enabled: #{vhost.acme}"
38
+ if vhost.acme
39
+ say "Managed TLS Domain: #{vhost.user_domain}"
40
+ say 'Managed TLS DNS Challenge Hostname: ' \
41
+ "#{vhost.acme_dns_challenge_host}"
42
+ say "Managed TLS Status: #{vhost.acme_status}"
43
+ end
44
+ end
45
+
46
+ def provision_vhost_and_explain(service, vhost)
47
+ op = vhost.create_operation!(type: 'provision')
48
+ attach_to_operation_logs(op)
49
+ explain_vhost(service, vhost.reload)
50
+ # TODO: Instructions if ACME is enabled?
51
+ end
52
+
53
+ def find_vhost(service_enumerator, hostname)
54
+ seen = []
55
+
56
+ service_enumerator.each do |service|
57
+ service.each_vhost do |vhost|
58
+ seen << vhost.external_host
59
+ return vhost if vhost.external_host == hostname
60
+ end
61
+ end
62
+
63
+ e = "Endpoint with hostname #{hostname} does not exist"
64
+ e = "#{e} (valid hostnames: #{seen.join(', ')})" if seen.any?
65
+ raise Thor::Error, e
66
+ end
67
+
68
+ def each_vhost(resource, &block)
69
+ return enum_for(:each_vhost, resource) unless block_given?
70
+
71
+ klass = resource.class
72
+ if klass == Aptible::Api::App
73
+ resource.each_service(&block)
74
+ elsif klass == Aptible::Api::Database
75
+ [resource.service].each(&block)
76
+ else
77
+ raise "Unexpected resource: #{klass}"
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,292 @@
1
+ module Aptible
2
+ module CLI
3
+ module Helpers
4
+ module Vhost
5
+ class OptionSetBuilder
6
+ FLAGS = %i(
7
+ environment
8
+ app
9
+ database
10
+ create
11
+ tls
12
+ ports
13
+ port
14
+ ).freeze
15
+
16
+ def initialize(&block)
17
+ FLAGS.each { |f| instance_variable_set("@#{f}", false) }
18
+ instance_exec(&block) if block
19
+ end
20
+
21
+ def declare_options(thor)
22
+ thor.instance_exec(self) do |builder|
23
+ option :environment
24
+
25
+ if builder.app?
26
+ app_options
27
+
28
+ if builder.create?
29
+ option(
30
+ :default_domain,
31
+ type: :boolean,
32
+ desc: 'Enable Default Domain on this Endpoint'
33
+ )
34
+
35
+ option(
36
+ :internal,
37
+ type: :boolean,
38
+ desc: 'Restrict this Endpoint to internal traffic'
39
+ )
40
+ end
41
+
42
+ if builder.ports?
43
+ option(
44
+ :ports,
45
+ type: :array,
46
+ desc: 'A list of ports to expose on this Endpoint'
47
+ )
48
+ end
49
+
50
+ if builder.port?
51
+ option(
52
+ :port,
53
+ type: :numeric,
54
+ desc: 'A port to expose on this Endpoint'
55
+ )
56
+ end
57
+ end
58
+
59
+ option(
60
+ :ip_whitelist,
61
+ type: :array,
62
+ desc: 'A list of IPv4 sources (addresses or CIDRs) to ' \
63
+ 'which to restrict traffic to this Endpoint'
64
+ )
65
+
66
+ unless builder.create?
67
+ # Yes, it has to be a dash...
68
+ # See: https://github.com/erikhuda/thor/pull/551
69
+ option(
70
+ :'no-ip_whitelist',
71
+ type: :boolean,
72
+ desc: 'Disable IP Whitelist'
73
+ )
74
+ end
75
+
76
+ if builder.tls?
77
+ option(
78
+ :certificate_file,
79
+ type: :string,
80
+ desc: 'A file containing a certificate to use on this ' \
81
+ 'Endpoint'
82
+ )
83
+ option(
84
+ :private_key_file,
85
+ type: :string,
86
+ desc: 'A file containing a private key to use on this ' \
87
+ 'Endpoint'
88
+ )
89
+
90
+ option(
91
+ :managed_tls,
92
+ type: :boolean,
93
+ desc: 'Enable Managed TLS on this Endpoint'
94
+ )
95
+
96
+ option(
97
+ :managed_tls_domain,
98
+ desc: 'A domain to use for Managed TLS'
99
+ )
100
+
101
+ option(
102
+ :certificate_fingerprint,
103
+ type: :string,
104
+ desc: 'The fingerprint of an existing Certificate to use ' \
105
+ 'on this Endpoint'
106
+ )
107
+ end
108
+ end
109
+ end
110
+
111
+ def prepare(account, options)
112
+ options = options.dup # We're going to delete keys here
113
+ verify_option_conflicts(options)
114
+
115
+ params = {}
116
+
117
+ params[:ip_whitelist] = options.delete(:ip_whitelist) do
118
+ create? ? [] : nil
119
+ end
120
+
121
+ if options.delete(:'no-ip_whitelist') { false }
122
+ params[:ip_whitelist] = []
123
+ end
124
+
125
+ params[:container_port] = options.delete(:port) if port?
126
+
127
+ if ports?
128
+ raw_ports = options.delete(:ports) do
129
+ create? ? [] : nil
130
+ end
131
+
132
+ if raw_ports
133
+ params[:container_ports] = raw_ports.map do |p|
134
+ begin
135
+ Integer(p)
136
+ rescue ArgumentError
137
+ m = "Invalid port: #{p}"
138
+ raise Thor::Error, m
139
+ end
140
+ end
141
+ end
142
+ end
143
+
144
+ if app?
145
+ params[:internal] = options.delete(:internal) do
146
+ create? ? false : nil
147
+ end
148
+
149
+ params[:default] = options.delete(:default_domain) do
150
+ create? ? false : nil
151
+ end
152
+
153
+ options.delete(:app)
154
+ else
155
+ params[:internal] = false
156
+ end
157
+
158
+ process_tls(account, options, params) if tls?
159
+
160
+ options.delete(:environment)
161
+
162
+ # NOTE: This is here to ensure that specs don't test for options
163
+ # that are not declared. This is not expected to happen when using
164
+ # this.
165
+ raise "Unexpected options: #{options}" if options.any?
166
+
167
+ params.delete_if { |_, v| v.nil? }
168
+ end
169
+
170
+ FLAGS.each do |f|
171
+ define_method("#{f}?") { instance_variable_get("@#{f}") }
172
+ end
173
+
174
+ private
175
+
176
+ FLAGS.each do |f|
177
+ define_method("#{f}!") { instance_variable_set("@#{f}", true) }
178
+ end
179
+
180
+ def process_tls(account, options_in, params_out)
181
+ # Certificate fingerprint option
182
+ if (fingerprint = options_in.delete(:certificate_fingerprint))
183
+ params_out[:certificate] = find_certificate(account, fingerprint)
184
+ end
185
+
186
+ # Ad-hoc certificate option
187
+ certificate_file = options_in.delete(:certificate_file)
188
+ private_key_file = options_in.delete(:private_key_file)
189
+
190
+ if certificate_file || private_key_file
191
+ if certificate_file.nil?
192
+ raise Thor::Error, "Missing #{to_flag(:certificate_file)}"
193
+ end
194
+
195
+ if private_key_file.nil?
196
+ raise Thor::Error, "Missing #{to_flag(:private_key_file)}"
197
+ end
198
+
199
+ opts = begin
200
+ {
201
+ certificate_body: File.read(certificate_file),
202
+ private_key: File.read(private_key_file)
203
+ }
204
+ rescue StandardError => e
205
+ m = 'Failed to read certificate or private key ' \
206
+ "file: #{e}"
207
+ raise Thor::Error, m
208
+ end
209
+
210
+ params_out[:certificate] = account.create_certificate!(opts)
211
+ end
212
+
213
+ # ACME option
214
+ params_out[:acme] = options_in.delete(:managed_tls) do
215
+ create? ? false : nil
216
+ end
217
+
218
+ params_out[:user_domain] = options_in.delete(:managed_tls_domain)
219
+
220
+ if create? && params_out[:acme] && params_out[:user_domain].nil?
221
+ e = "#{to_flag(:managed_tls_domain)} is required to enable " \
222
+ 'Managed TLS'
223
+ raise Thor::Error, e
224
+ end
225
+ end
226
+
227
+ def find_certificate(account, fingerprint)
228
+ matches = []
229
+ account.each_certificate do |certificate|
230
+ if certificate.sha256_fingerprint == fingerprint
231
+ return certificate
232
+ end
233
+
234
+ if certificate.sha256_fingerprint.start_with?(fingerprint)
235
+ matches << certificate
236
+ end
237
+ end
238
+
239
+ matches = matches.uniq(&:sha256_fingerprint)
240
+
241
+ case matches.size
242
+ when 0
243
+ e = "No certificate matches fingerprint #{fingerprint}"
244
+ raise Thor::Error, e
245
+ when 1
246
+ return matches.first
247
+ else
248
+ e = 'Too many certificates match fingerprint ' \
249
+ "#{fingerprint}, pass a more specific fingerprint "
250
+ raise Thor::Error, e
251
+ end
252
+ end
253
+
254
+ def verify_option_conflicts(options)
255
+ conflict_groups = [
256
+ [
257
+ %i(certificate_file private_key_file),
258
+ %i(certificate_fingerprint),
259
+ %i(managed_tls managed_tls_domain),
260
+ %i(default_domain)
261
+ ],
262
+ [
263
+ %i(no-ip_whitelist),
264
+ %i(ip_whitelist)
265
+ ]
266
+ ]
267
+
268
+ conflict_groups.each do |group|
269
+ matches = group.map do |g|
270
+ g.any? { |k| !!options[k] }
271
+ end
272
+
273
+ next unless matches.select { |m| !!m }.size > 1
274
+
275
+ selected = group.flatten.select do |o|
276
+ !!options[o]
277
+ end
278
+
279
+ flags = selected.map { |s| to_flag(s) }
280
+ e = "Conflicting options provided: #{flags.join(', ')}"
281
+ raise Thor::Error, e
282
+ end
283
+ end
284
+
285
+ def to_flag(sym)
286
+ "--#{sym.to_s.tr('_', '-')}"
287
+ end
288
+ end
289
+ end
290
+ end
291
+ end
292
+ end