aptible-cli 0.13.0 → 0.14.0

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