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.
@@ -43,8 +43,7 @@ module Aptible
43
43
  option :size, type: :numeric,
44
44
  desc: 'DEPRECATED, use --container-size'
45
45
  define_method 'apps:scale' do |type, *more|
46
- app = ensure_app(options)
47
- service = app.services.find { |s| s.process_type == type }
46
+ service = ensure_service(options, type)
48
47
 
49
48
  container_count = options[:container_count]
50
49
  container_size = options[:container_size]
@@ -94,17 +93,6 @@ module Aptible
94
93
  'Provide at least --container-count or --container-size'
95
94
  end
96
95
 
97
- if service.nil?
98
- valid_types = if app.services.empty?
99
- 'NONE (deploy the app first)'
100
- else
101
- app.services.map(&:process_type).join(', ')
102
- end
103
- raise Thor::Error, "Service with type #{type} does not " \
104
- "exist for app #{app.handle}. Valid " \
105
- "types: #{valid_types}."
106
- end
107
-
108
96
  # We don't validate any parameters here: API will do that for us.
109
97
  opts = { type: 'scale' }
110
98
  opts[:container_count] = container_count if container_count
@@ -9,10 +9,12 @@ module Aptible
9
9
  include Helpers::Operation
10
10
  include Helpers::App
11
11
 
12
- desc 'domains', "Print an app's current virtual domains"
12
+ desc 'domains',
13
+ "Print an app's current virtual domains - DEPRECATED"
13
14
  app_options
14
15
  option :verbose, aliases: '-v'
15
16
  def domains
17
+ deprecated 'This command is deprecated in favor of endpoints:list'
16
18
  app = ensure_app(options)
17
19
  print_vhosts(app) do |vhost|
18
20
  if options[:verbose]
@@ -0,0 +1,183 @@
1
+ require 'term/ansicolor'
2
+ require 'uri'
3
+
4
+ module Aptible
5
+ module CLI
6
+ module Subcommands
7
+ module Endpoints
8
+ def self.included(thor)
9
+ thor.class_eval do
10
+ include Helpers::Operation
11
+ include Helpers::AppOrDatabase
12
+ include Helpers::Vhost
13
+
14
+ database_create_flags = Helpers::Vhost::OptionSetBuilder.new do
15
+ create!
16
+ database!
17
+ end
18
+
19
+ desc 'endpoints:database:create DATABASE',
20
+ 'Create a Database Endpoint'
21
+ database_create_flags.declare_options(self)
22
+ define_method 'endpoints:database:create' do |handle|
23
+ database = ensure_database(options.merge(db: handle))
24
+ service = database.service
25
+ raise Thor::Error, 'Database is not provisioned' if service.nil?
26
+
27
+ vhost = service.create_vhost!(
28
+ type: 'tcp',
29
+ platform: 'elb',
30
+ **database_create_flags.prepare(database.account, options)
31
+ )
32
+
33
+ provision_vhost_and_explain(service, vhost)
34
+ end
35
+
36
+ tcp_create_flags = Helpers::Vhost::OptionSetBuilder.new do
37
+ app!
38
+ create!
39
+ ports!
40
+ end
41
+
42
+ desc 'endpoints:tcp:create [--app APP] SERVICE',
43
+ 'Create an App TCP Endpoint'
44
+ tcp_create_flags.declare_options(self)
45
+ define_method 'endpoints:tcp:create' do |type|
46
+ create_app_vhost(
47
+ tcp_create_flags, options, type,
48
+ type: 'tcp', platform: 'elb'
49
+ )
50
+ end
51
+
52
+ tcp_modify_flags = Helpers::Vhost::OptionSetBuilder.new do
53
+ app!
54
+ ports!
55
+ end
56
+
57
+ desc 'endpoints:tcp:modify [--app APP] ENDPOINT_HOSTNAME',
58
+ 'Modify an App TCP Endpoint'
59
+ tcp_modify_flags.declare_options(self)
60
+ define_method 'endpoints:tcp:modify' do |hostname|
61
+ modify_app_vhost(tcp_modify_flags, options, hostname)
62
+ end
63
+
64
+ tls_create_flags = Helpers::Vhost::OptionSetBuilder.new do
65
+ app!
66
+ create!
67
+ ports!
68
+ tls!
69
+ end
70
+
71
+ desc 'endpoints:tls:create [--app APP] SERVICE',
72
+ 'Create an App TLS Endpoint'
73
+ tls_create_flags.declare_options(self)
74
+ define_method 'endpoints:tls:create' do |type|
75
+ create_app_vhost(
76
+ tls_create_flags, options, type,
77
+ type: 'tls', platform: 'elb'
78
+ )
79
+ end
80
+
81
+ tls_modify_flags = Helpers::Vhost::OptionSetBuilder.new do
82
+ app!
83
+ ports!
84
+ tls!
85
+ end
86
+
87
+ desc 'endpoints:tls:modify [--app APP] ENDPOINT_HOSTNAME',
88
+ 'Modify an App TLS Endpoint'
89
+ tls_modify_flags.declare_options(self)
90
+ define_method 'endpoints:tls:modify' do |hostname|
91
+ modify_app_vhost(tls_modify_flags, options, hostname)
92
+ end
93
+
94
+ https_create_flags = Helpers::Vhost::OptionSetBuilder.new do
95
+ app!
96
+ create!
97
+ port!
98
+ tls!
99
+ end
100
+
101
+ desc 'endpoints:https:create [--app APP] SERVICE',
102
+ 'Create an App HTTPS Endpoint'
103
+ https_create_flags.declare_options(self)
104
+ define_method 'endpoints:https:create' do |type|
105
+ create_app_vhost(
106
+ https_create_flags, options, type,
107
+ type: 'http', platform: 'alb'
108
+ )
109
+ end
110
+
111
+ https_modify_flags = Helpers::Vhost::OptionSetBuilder.new do
112
+ app!
113
+ port!
114
+ tls!
115
+ end
116
+
117
+ desc 'endpoints:https:modify [--app APP] ENDPOINT_HOSTNAME',
118
+ 'Modify an App HTTPS Endpoint'
119
+ https_modify_flags.declare_options(self)
120
+ define_method 'endpoints:https:modify' do |hostname|
121
+ modify_app_vhost(https_modify_flags, options, hostname)
122
+ end
123
+
124
+ desc 'endpoints:list [--app APP | --database DATABASE]',
125
+ 'List Endpoints for an App or Database'
126
+ app_or_database_options
127
+ define_method 'endpoints:list' do
128
+ resource = ensure_app_or_database(options)
129
+
130
+ first = true
131
+ each_vhost(resource) do |service|
132
+ service.each_vhost do |vhost|
133
+ say '' unless first
134
+ first = false
135
+ explain_vhost(service, vhost)
136
+ end
137
+ end
138
+ end
139
+
140
+ desc 'endpoints:deprovision [--app APP | --database DATABASE] ' \
141
+ 'ENDPOINT_HOSTNAME', \
142
+ 'Deprovision an App or Database Endpoint'
143
+ app_or_database_options
144
+ define_method 'endpoints:deprovision' do |hostname|
145
+ resource = ensure_app_or_database(options)
146
+ vhost = find_vhost(each_vhost(resource), hostname)
147
+ op = vhost.create_operation!(type: 'deprovision')
148
+ attach_to_operation_logs(op)
149
+ end
150
+
151
+ desc 'endpoints:renew [--app APP] ENDPOINT_HOSTNAME', \
152
+ 'Renew an App Managed TLS Endpoint'
153
+ app_options
154
+ define_method 'endpoints:renew' do |hostname|
155
+ app = ensure_app(options)
156
+ vhost = find_vhost(app.each_service, hostname)
157
+ op = vhost.create_operation!(type: 'renew')
158
+ attach_to_operation_logs(op)
159
+ end
160
+
161
+ no_commands do
162
+ def create_app_vhost(flags, options, process_type, **attrs)
163
+ service = ensure_service(options, process_type)
164
+ vhost = service.create_vhost!(
165
+ **flags.prepare(service.account, options),
166
+ **attrs
167
+ )
168
+ provision_vhost_and_explain(service, vhost)
169
+ end
170
+
171
+ def modify_app_vhost(flags, options, hostname)
172
+ app = ensure_app(options)
173
+ vhost = find_vhost(each_vhost(app), hostname)
174
+ vhost.update!(**flags.prepare(vhost.service.account, options))
175
+ provision_vhost_and_explain(vhost.service, vhost)
176
+ end
177
+ end
178
+ end
179
+ end
180
+ end
181
+ end
182
+ end
183
+ end
@@ -7,24 +7,13 @@ module Aptible
7
7
  def self.included(thor)
8
8
  thor.class_eval do
9
9
  include Helpers::Operation
10
- include Helpers::App
11
- include Helpers::Database
10
+ include Helpers::AppOrDatabase
12
11
 
13
- desc 'logs', 'Follows logs from a running app or database'
14
- app_options
15
- option :database
12
+ desc 'logs [--app APP | --database DATABASE]',
13
+ 'Follows logs from a running app or database'
14
+ app_or_database_options
16
15
  def logs
17
- if options[:app] && options[:database]
18
- m = 'You must specify only one of --app and --database'
19
- raise Thor::Error, m
20
- end
21
-
22
- resource = \
23
- if options[:database]
24
- ensure_database(options.merge(db: options[:database]))
25
- else
26
- ensure_app(options)
27
- end
16
+ resource = ensure_app_or_database(options)
28
17
 
29
18
  unless resource.status == 'provisioned'
30
19
  raise Thor::Error, 'Unable to retrieve logs. ' \
@@ -1,5 +1,5 @@
1
1
  module Aptible
2
2
  module CLI
3
- VERSION = '0.13.0'.freeze
3
+ VERSION = '0.14.0'.freeze
4
4
  end
5
5
  end
@@ -0,0 +1,105 @@
1
+ require 'spec_helper'
2
+
3
+ describe Aptible::CLI::Helpers::Vhost do
4
+ subject { Class.new.send(:include, described_class).new }
5
+
6
+ describe '#explain_vhost' do
7
+ let(:lines) { [] }
8
+ before { allow(subject).to receive(:say) { |m| lines << m } }
9
+
10
+ it 'explains a VHOST' do
11
+ service = Fabricate(:service, process_type: 'web')
12
+ vhost = Fabricate(
13
+ :vhost,
14
+ service: service,
15
+ external_host: 'foo.io',
16
+ status: 'provisioned',
17
+ type: 'http_proxy_protocol',
18
+ internal: false,
19
+ ip_whitelist: [],
20
+ default: false,
21
+ acme: false
22
+ )
23
+ subject.explain_vhost(service, vhost)
24
+
25
+ expected = [
26
+ 'Service: web',
27
+ 'Hostname: foo.io',
28
+ 'Status: provisioned',
29
+ 'Type: https',
30
+ 'Port: default',
31
+ 'Internal: false',
32
+ 'IP Whitelist: all traffic',
33
+ 'Default Domain Enabled: false',
34
+ 'Managed TLS Enabled: false'
35
+ ]
36
+ expect(lines).to eq(expected)
37
+ end
38
+
39
+ it 'explains a failed VHOST' do
40
+ vhost = Fabricate(:vhost, status: 'provision_failed')
41
+ subject.explain_vhost(vhost.service, vhost)
42
+ expect(lines).to include('Status: provision_failed')
43
+ end
44
+
45
+ it 'explains an internal VHOST' do
46
+ vhost = Fabricate(:vhost, internal: true)
47
+ subject.explain_vhost(vhost.service, vhost)
48
+ expect(lines).to include('Internal: true')
49
+ end
50
+
51
+ it 'explains a default VHOST' do
52
+ vhost = Fabricate(:vhost, default: true, virtual_domain: 'qux.io')
53
+ subject.explain_vhost(vhost.service, vhost)
54
+ expect(lines).to include('Default Domain Enabled: true')
55
+ expect(lines).to include('Default Domain: qux.io')
56
+ end
57
+
58
+ it 'explains a TLS VHOST' do
59
+ vhost = Fabricate(:vhost, type: 'tls')
60
+ subject.explain_vhost(vhost.service, vhost)
61
+ expect(lines).to include('Type: tls')
62
+ expect(lines).to include('Ports: all')
63
+ end
64
+
65
+ it 'explains a TCP VHOST' do
66
+ vhost = Fabricate(:vhost, type: 'tcp')
67
+ subject.explain_vhost(vhost.service, vhost)
68
+ expect(lines).to include('Type: tcp')
69
+ expect(lines).to include('Ports: all')
70
+ end
71
+
72
+ it 'explains a VHOST with a container port' do
73
+ vhost = Fabricate(:vhost, type: 'http_proxy_protocol', container_port: 12)
74
+ subject.explain_vhost(vhost.service, vhost)
75
+ expect(lines).to include('Port: 12')
76
+ end
77
+
78
+ it 'explains a VHOST with container ports' do
79
+ vhost = Fabricate(:vhost, type: 'tls', container_ports: [12, 34])
80
+ subject.explain_vhost(vhost.service, vhost)
81
+ expect(lines).to include('Ports: 12 34')
82
+ end
83
+
84
+ it 'explains a VHOST with IP Filtering' do
85
+ vhost = Fabricate(:vhost, ip_whitelist: %w(1.1.1.1/1 2.2.2.2/2))
86
+ subject.explain_vhost(vhost.service, vhost)
87
+ expect(lines).to include('IP Whitelist: 1.1.1.1/1 2.2.2.2/2')
88
+ end
89
+
90
+ it 'explains a VHOST with Managed TLS' do
91
+ vhost = Fabricate(
92
+ :vhost,
93
+ acme: true,
94
+ user_domain: 'foo.io',
95
+ acme_dns_challenge_host: 'dns.qux.io',
96
+ acme_status: 'ready'
97
+ )
98
+ subject.explain_vhost(vhost.service, vhost)
99
+ expect(lines).to include('Managed TLS Enabled: true')
100
+ expect(lines).to include('Managed TLS Domain: foo.io')
101
+ expect(lines).to include('Managed TLS DNS Challenge Hostname: dns.qux.io')
102
+ expect(lines).to include('Managed TLS Status: ready')
103
+ end
104
+ end
105
+ end
@@ -26,7 +26,7 @@ describe Aptible::CLI::Agent do
26
26
 
27
27
  let!(:account) { Fabricate(:account) }
28
28
  let!(:app) { Fabricate(:app, handle: 'hello', account: account) }
29
- let!(:service) { Fabricate(:service, app: app) }
29
+ let!(:service) { Fabricate(:service, app: app, process_type: 'web') }
30
30
  let(:op) { Fabricate(:operation, status: 'succeeded', resource: app) }
31
31
 
32
32
  describe '#apps:scale' do
@@ -0,0 +1,604 @@
1
+ require 'spec_helper'
2
+
3
+ describe Aptible::CLI::Agent do
4
+ let!(:a1) { Fabricate(:account, handle: 'foo') }
5
+ let!(:a2) { Fabricate(:account, handle: 'bar') }
6
+
7
+ let(:token) { double 'token' }
8
+
9
+ before do
10
+ allow(subject).to receive(:fetch_token) { token }
11
+ allow(Aptible::Api::Account).to receive(:all).with(token: token)
12
+ .and_return([a1, a2])
13
+ end
14
+
15
+ def expect_create_certificate(account, options)
16
+ expect(account).to receive(:create_certificate!).with(
17
+ hash_including(options)
18
+ ) do |args|
19
+ Fabricate(:certificate, account: account, **args)
20
+ end
21
+ end
22
+
23
+ def expect_create_vhost(service, options)
24
+ expect(service).to receive(:create_vhost!).with(
25
+ hash_including(options)
26
+ ) do |args|
27
+ Fabricate(:vhost, service: service, **args).tap do |v|
28
+ expect_operation(v, 'provision')
29
+ expect(v).to receive(:reload).and_return(v)
30
+ expect(subject).to receive(:explain_vhost).with(service, v)
31
+ end
32
+ end
33
+ end
34
+
35
+ def expect_modify_vhost(vhost, options)
36
+ expect(vhost).to receive(:update!).with(options) do
37
+ expect_operation(vhost, 'provision')
38
+ expect(vhost).to receive(:reload).and_return(vhost)
39
+ expect(subject).to receive(:explain_vhost).with(vhost.service, vhost)
40
+ end
41
+ end
42
+
43
+ def expect_operation(vhost, type)
44
+ expect(vhost).to receive(:create_operation!).with(type: type) do
45
+ Fabricate(:operation).tap do |o|
46
+ expect(subject).to receive(:attach_to_operation_logs).with(o)
47
+ end
48
+ end
49
+ end
50
+
51
+ context 'Database Endpoints' do
52
+ def stub_options(**opts)
53
+ allow(subject).to receive(:options).and_return(opts)
54
+ end
55
+
56
+ let!(:db) { Fabricate(:database, handle: 'mydb', account: a1) }
57
+
58
+ before do
59
+ allow(Aptible::Api::Database).to receive(:all).with(token: token)
60
+ .and_return([db])
61
+ allow(db).to receive(:class).and_return(Aptible::Api::Database)
62
+ stub_options
63
+ end
64
+
65
+ describe 'endpoints:database:create' do
66
+ it 'fails if the DB does not exist' do
67
+ expect { subject.send('endpoints:database:create', 'some') }
68
+ .to raise_error(/could not find database some/im)
69
+ end
70
+
71
+ it 'fails if the DB is not in the account' do
72
+ stub_options(environment: 'bar')
73
+ expect { subject.send('endpoints:database:create', 'mydb') }
74
+ .to raise_error(/could not find database mydb/im)
75
+ end
76
+
77
+ it 'creates a new Endpoint' do
78
+ expect_create_vhost(
79
+ db.service,
80
+ type: 'tcp',
81
+ platform: 'elb',
82
+ internal: false,
83
+ ip_whitelist: []
84
+ )
85
+ subject.send('endpoints:database:create', 'mydb')
86
+ end
87
+
88
+ it 'creates a new Endpoint with IP Filtering' do
89
+ expect_create_vhost(db.service, ip_whitelist: %w(1.1.1.1))
90
+ stub_options(ip_whitelist: %w(1.1.1.1))
91
+ subject.send('endpoints:database:create', 'mydb')
92
+ end
93
+ end
94
+
95
+ describe 'endpoints:list' do
96
+ it 'lists Endpoints' do
97
+ lines = []
98
+ allow(subject).to receive(:say) { |m| lines << m }
99
+
100
+ s = Fabricate(:service, database: db)
101
+ v1 = Fabricate(:vhost, service: s)
102
+ v2 = Fabricate(:vhost, service: s)
103
+
104
+ stub_options(database: db.handle)
105
+ subject.send('endpoints:list')
106
+
107
+ expect(lines).to include("Hostname: #{v1.external_host}")
108
+ expect(lines).to include("Hostname: #{v2.external_host}")
109
+
110
+ expect(lines[0]).not_to eq("\n")
111
+ expect(lines[-1]).not_to eq("\n")
112
+ end
113
+ end
114
+
115
+ describe 'endpoints:deprovison' do
116
+ it 'deprovisions an Endpoint' do
117
+ s = Fabricate(:service, database: db)
118
+ Fabricate(:vhost, service: s)
119
+ v2 = Fabricate(:vhost, service: s)
120
+
121
+ stub_options(database: db.handle)
122
+
123
+ expect_operation(v2, 'deprovision')
124
+ subject.send('endpoints:deprovision', v2.external_host)
125
+ end
126
+
127
+ it 'fails if the Endpoint does not exist' do
128
+ stub_options(database: db.handle)
129
+
130
+ expect { subject.send('endpoints:deprovision', 'foo.io') }
131
+ .to raise_error(/endpoint.*foo\.io.*does not exist/im)
132
+ end
133
+ end
134
+ end
135
+
136
+ context 'App Endpoints' do
137
+ def stub_options(**opts)
138
+ base = { app: app.handle }
139
+ allow(subject).to receive(:options).and_return(base.merge(opts))
140
+ end
141
+
142
+ let!(:app) { Fabricate(:app, handle: 'myapp', account: a1) }
143
+ let!(:service) { Fabricate(:service, app: app, process_type: 'web') }
144
+
145
+ before do
146
+ allow(Aptible::Api::App).to receive(:all).with(token: token)
147
+ .and_return([app])
148
+ allow(app).to receive(:class).and_return(Aptible::Api::App)
149
+ stub_options
150
+ end
151
+
152
+ shared_examples 'shared create app vhost examples' do |method|
153
+ context 'App Vhost Options' do
154
+ it 'fails if the app does not exist' do
155
+ stub_options(app: 'foo')
156
+ expect { subject.send(method, 'foo') }
157
+ .to raise_error(/could not find app/im)
158
+ end
159
+
160
+ it 'fails if the service does not exist' do
161
+ expect { subject.send(method, 'foo') }
162
+ .to raise_error(/service.*does not exist/im)
163
+ end
164
+
165
+ it 'creates an internal Endpoint' do
166
+ expect_create_vhost(service, internal: true)
167
+ stub_options(internal: true)
168
+ subject.send(method, 'web')
169
+ end
170
+
171
+ it 'creates an Endpoint with IP Filtering' do
172
+ expect_create_vhost(service, ip_whitelist: %w(1.1.1.1))
173
+ stub_options(ip_whitelist: %w(1.1.1.1))
174
+ subject.send(method, 'web')
175
+ end
176
+
177
+ it 'creates a default Endpoint' do
178
+ expect_create_vhost(service, default: true)
179
+ stub_options(default_domain: true)
180
+ subject.send(method, 'web')
181
+ end
182
+ end
183
+ end
184
+
185
+ shared_examples 'shared create tcp vhost examples' do |method|
186
+ context 'TCP VHOST Options' do
187
+ it 'creates an Endpoint with Ports' do
188
+ expect_create_vhost(service, container_ports: [10, 20])
189
+ stub_options(ports: %w(10 20))
190
+ subject.send(method, 'web')
191
+ end
192
+
193
+ it 'raises an error if the ports are invalid' do
194
+ stub_options(ports: %w(foo))
195
+ expect { subject.send(method, 'web') }
196
+ .to raise_error(/invalid port: foo/im)
197
+ end
198
+ end
199
+ end
200
+
201
+ shared_examples 'shared create tls vhost examples' do |method|
202
+ context 'TLS Vhost Options' do
203
+ it 'creates an Endpoint with a new Certificate' do
204
+ expect_create_certificate(
205
+ a1,
206
+ certificate_body: 'the cert',
207
+ private_key: 'the key'
208
+ )
209
+
210
+ expect_create_vhost(
211
+ service,
212
+ certificate: an_instance_of(StubCertificate),
213
+ acme: false,
214
+ default: false
215
+ )
216
+
217
+ Dir.mktmpdir do |d|
218
+ cert, key = %w(cert key).map { |f| File.join(d, f) }
219
+ File.write(cert, 'the cert')
220
+ File.write(key, 'the key')
221
+ stub_options(certificate_file: cert, private_key_file: key)
222
+ subject.send(method, 'web')
223
+ end
224
+ end
225
+
226
+ it 'fails if certificate file is not provided' do
227
+ stub_options(private_key_file: 'foo')
228
+ expect { subject.send(method, 'web') }
229
+ .to raise_error(/missing --certificate-file/im)
230
+ end
231
+
232
+ it 'fails if private key file is not provided' do
233
+ stub_options(certificate_file: 'foo')
234
+ expect { subject.send(method, 'web') }
235
+ .to raise_error(/missing --private-key-file/im)
236
+ end
237
+
238
+ it 'fails if a file is unreadable' do
239
+ Dir.mktmpdir do |d|
240
+ cert, key = %w(cert key).map { |f| File.join(d, f) }
241
+ stub_options(certificate_file: cert, private_key_file: key)
242
+ expect { subject.send(method, 'web') }
243
+ .to raise_error(/failed to read certificate or private key/im)
244
+ end
245
+ end
246
+
247
+ it 'creates an Endpoint with an existing Certificate (exact match)' do
248
+ c = Fabricate(:certificate, account: a1)
249
+ stub_options(certificate_fingerprint: c.sha256_fingerprint)
250
+
251
+ expect_create_vhost(
252
+ service,
253
+ certificate: c,
254
+ acme: false,
255
+ default: false
256
+ )
257
+
258
+ subject.send(method, 'web')
259
+ end
260
+
261
+ it 'creates an Endpoint with an existing Certificate (one match)' do
262
+ c = Fabricate(:certificate, account: a1)
263
+ Fabricate(:certificate, account: a1)
264
+
265
+ stub_options(certificate_fingerprint: c.sha256_fingerprint[0..5])
266
+ expect_create_vhost(service, certificate: c)
267
+ subject.send(method, 'web')
268
+ end
269
+
270
+ it 'creates an Endpoint with an existing Certificate (dupe matches)' do
271
+ c1 = Fabricate(:certificate, account: a1)
272
+ Fabricate(
273
+ :certificate,
274
+ account: a1,
275
+ sha256_fingerprint: c1.sha256_fingerprint
276
+ )
277
+
278
+ stub_options(certificate_fingerprint: c1.sha256_fingerprint[0..5])
279
+ expect_create_vhost(service, certificate: c1)
280
+ subject.send(method, 'web')
281
+ end
282
+
283
+ it 'creates an Endpoint with Managed TLS' do
284
+ expect_create_vhost(
285
+ service,
286
+ acme: true,
287
+ user_domain: 'foo.io'
288
+ )
289
+
290
+ stub_options(managed_tls: true, managed_tls_domain: 'foo.io')
291
+ subject.send(method, 'web')
292
+ end
293
+
294
+ it 'requires a domain for Managed TLS' do
295
+ stub_options(managed_tls: true)
296
+ expect { subject.send(method, 'web') }
297
+ .to raise_error(/--managed-tls-domain/im)
298
+ end
299
+
300
+ it 'fails if the certificate does not exist' do
301
+ Fabricate(:certificate, account: a1)
302
+ c2 = Fabricate(:certificate, account: a2)
303
+
304
+ stub_options(certificate_fingerprint: c2.sha256_fingerprint)
305
+ expect { subject.send(method, 'web') }
306
+ .to raise_error(/no certificate matches fingerprint/im)
307
+ end
308
+
309
+ it 'fails if too many certificates match' do
310
+ Fabricate(:certificate, account: a1, sha256_fingerprint: 'fooA')
311
+ Fabricate(:certificate, account: a1, sha256_fingerprint: 'fooB')
312
+ stub_options(certificate_fingerprint: 'foo')
313
+ expect { subject.send(method, 'web') }
314
+ .to raise_error(/too many certificates match fingerprint/im)
315
+ end
316
+
317
+ it 'fails if conflicting options are given (ACME, Cert)' do
318
+ stub_options(certificate_file: 'foo', managed_tls: true)
319
+ expect { subject.send(method, 'web') }
320
+ .to raise_error(/conflicting options.*file.*managed/im)
321
+ end
322
+
323
+ it 'fails if conflicting options are given (ACME Domain, Cert)' do
324
+ stub_options(certificate_file: 'foo', managed_tls_domain: 'bar')
325
+ expect { subject.send(method, 'web') }
326
+ .to raise_error(/conflicting options.*file.*managed/im)
327
+ end
328
+
329
+ it 'fails if conflicting options are given (ACME, Fingerprint)' do
330
+ stub_options(certificate_fingerprint: 'foo', managed_tls: true)
331
+ expect { subject.send(method, 'web') }
332
+ .to raise_error(/conflicting options.*finger.*managed/im)
333
+ end
334
+
335
+ it 'fails if conflicting options are given (Cert, Fingerprint)' do
336
+ stub_options(certificate_file: 'foo', certificate_fingerprint: 'foo')
337
+ expect { subject.send(method, 'web') }
338
+ .to raise_error(/conflicting options.*file.*finger/im)
339
+ end
340
+
341
+ it 'fails if conflicting options are given (ACME, Default)' do
342
+ stub_options(managed_tls: true, default_domain: true)
343
+ expect { subject.send(method, 'web') }
344
+ .to raise_error(/conflicting options.*managed.*default/im)
345
+ end
346
+ end
347
+ end
348
+
349
+ describe 'endpoints:tcp:create' do
350
+ m = 'endpoints:tcp:create'
351
+ include_examples 'shared create app vhost examples', m
352
+ include_examples 'shared create tcp vhost examples', m
353
+
354
+ it 'creates a TCP Endpoint' do
355
+ expect_create_vhost(
356
+ service,
357
+ type: 'tcp',
358
+ platform: 'elb',
359
+ internal: false,
360
+ default: false,
361
+ ip_whitelist: [],
362
+ container_ports: []
363
+ )
364
+
365
+ subject.send(m, 'web')
366
+ end
367
+ end
368
+
369
+ describe 'endpoints:tls:create' do
370
+ m = 'endpoints:tls:create'
371
+ include_examples 'shared create app vhost examples', m
372
+ include_examples 'shared create tcp vhost examples', m
373
+ include_examples 'shared create tls vhost examples', m
374
+
375
+ it 'creates a TLS Endpoint' do
376
+ expect_create_vhost(
377
+ service,
378
+ type: 'tls',
379
+ platform: 'elb',
380
+ internal: false,
381
+ default: false,
382
+ ip_whitelist: [],
383
+ container_ports: []
384
+ )
385
+ subject.send(m, 'web')
386
+ end
387
+ end
388
+
389
+ describe 'endpoints:https:create' do
390
+ m = 'endpoints:https:create'
391
+ include_examples 'shared create app vhost examples', m
392
+ include_examples 'shared create tls vhost examples', m
393
+
394
+ it 'creates a HTTP Endpoint' do
395
+ expect_create_vhost(
396
+ service,
397
+ type: 'http',
398
+ platform: 'alb',
399
+ internal: false,
400
+ default: false,
401
+ ip_whitelist: []
402
+ )
403
+ subject.send(m, 'web')
404
+ end
405
+
406
+ it 'creates an Endpoint with a container Port' do
407
+ expect_create_vhost(service, container_port: 10)
408
+ stub_options(port: 10)
409
+ subject.send(m, 'web')
410
+ end
411
+ end
412
+
413
+ shared_examples 'shared modify app vhost examples' do |m|
414
+ it 'does not change anything if no options are passed' do
415
+ v = Fabricate(:vhost, service: service)
416
+ expect_modify_vhost(v, {})
417
+ subject.send(m, v.external_host)
418
+ end
419
+
420
+ it 'adds an IP whitelist' do
421
+ v = Fabricate(:vhost, service: service)
422
+ expect_modify_vhost(v, ip_whitelist: %w(1.1.1.1))
423
+
424
+ stub_options(ip_whitelist: %w(1.1.1.1))
425
+ subject.send(m, v.external_host)
426
+ end
427
+
428
+ it 'removes an IP whitelist' do
429
+ v = Fabricate(:vhost, service: service)
430
+ expect_modify_vhost(v, ip_whitelist: [])
431
+
432
+ stub_options(:'no-ip_whitelist' => true)
433
+ subject.send(m, v.external_host)
434
+ end
435
+
436
+ it 'does not allow disabling and adding an IP whitelist' do
437
+ v = Fabricate(:vhost, service: service)
438
+ stub_options(ip_whitelist: %w(1.1.1.1), :'no-ip_whitelist' => true)
439
+ expect { subject.send(m, v.external_host) }
440
+ .to raise_error(/conflicting.*no-ip-whitelist.*ip-whitelist/im)
441
+ end
442
+ end
443
+
444
+ shared_examples 'shared modify tcp vhost examples' do |m|
445
+ it 'allows updating Container Ports' do
446
+ v = Fabricate(:vhost, service: service)
447
+ expect_modify_vhost(v, container_ports: [10, 20])
448
+
449
+ stub_options(ports: %w(10 20))
450
+ subject.send(m, v.external_host)
451
+ end
452
+ end
453
+
454
+ shared_examples 'shared modify tls vhost examples' do |m|
455
+ it 'allows enabling Managed TLS' do
456
+ # NOTE: As-is, this will typically fail in the backend since the
457
+ # Managed TLS Hostname is required as well.
458
+ v = Fabricate(:vhost, service: service)
459
+ expect_modify_vhost(v, acme: true)
460
+
461
+ stub_options(managed_tls: true)
462
+ subject.send(m, v.external_host)
463
+ end
464
+
465
+ it 'allows disabling Managed TLS' do
466
+ v = Fabricate(:vhost, service: service)
467
+ expect_modify_vhost(v, acme: false)
468
+
469
+ stub_options(managed_tls: false)
470
+ subject.send(m, v.external_host)
471
+ end
472
+
473
+ it 'allows updating the Managed TLS Domain' do
474
+ # NOTE: This will usually fail in the backend due to API validations on
475
+ # the cert / domain matching.
476
+ v = Fabricate(:vhost, service: service)
477
+ expect_modify_vhost(v, user_domain: 'foobar.io')
478
+
479
+ stub_options(managed_tls_domain: 'foobar.io')
480
+ subject.send(m, v.external_host)
481
+ end
482
+
483
+ it 'updates the Endpoint with a new Certificate' do
484
+ v = Fabricate(:vhost, service: service)
485
+
486
+ expect_create_certificate(
487
+ a1, certificate_body: 'the cert', private_key: 'the key'
488
+ )
489
+
490
+ expect_modify_vhost(v, certificate: an_instance_of(StubCertificate))
491
+
492
+ Dir.mktmpdir do |d|
493
+ cert, key = %w(cert key).map { |f| File.join(d, f) }
494
+ File.write(cert, 'the cert')
495
+ File.write(key, 'the key')
496
+ stub_options(certificate_file: cert, private_key_file: key)
497
+
498
+ subject.send(m, v.external_host)
499
+ end
500
+ end
501
+
502
+ it 'updates an Endpoint with an existing Certificate (exact match)' do
503
+ v = Fabricate(:vhost, service: service)
504
+ c = Fabricate(:certificate, account: a1)
505
+ stub_options(certificate_fingerprint: c.sha256_fingerprint)
506
+
507
+ expect_modify_vhost(v, certificate: c)
508
+
509
+ subject.send(m, v.external_host)
510
+ end
511
+ end
512
+
513
+ describe 'endpoints:tcp:modify' do
514
+ m = 'endpoints:tcp:modify'
515
+ include_examples 'shared modify app vhost examples', m
516
+ include_examples 'shared modify tcp vhost examples', m
517
+ end
518
+
519
+ describe 'endpoints:tls:modify' do
520
+ m = 'endpoints:tls:modify'
521
+ include_examples 'shared modify app vhost examples', m
522
+ include_examples 'shared modify tcp vhost examples', m
523
+ include_examples 'shared modify tls vhost examples', m
524
+ end
525
+
526
+ describe 'endpoints:https:modify' do
527
+ m = 'endpoints:https:modify'
528
+ include_examples 'shared modify app vhost examples', m
529
+ include_examples 'shared modify tls vhost examples', m
530
+
531
+ it 'allows updating the Container Port' do
532
+ v = Fabricate(:vhost, service: service)
533
+ expect_modify_vhost(v, container_port: 10)
534
+
535
+ stub_options(port: 10)
536
+ subject.send(m, v.external_host)
537
+ end
538
+ end
539
+
540
+ describe 'endpoints:list' do
541
+ it 'lists Endpoints across services' do
542
+ lines = []
543
+ allow(subject).to receive(:say) { |m| lines << m }
544
+
545
+ s1 = Fabricate(:service, app: app)
546
+ v1 = Fabricate(:vhost, service: s1)
547
+
548
+ s2 = Fabricate(:service, app: app)
549
+ v2 = Fabricate(:vhost, service: s2)
550
+ v3 = Fabricate(:vhost, service: s2)
551
+
552
+ subject.send('endpoints:list')
553
+
554
+ expect(lines).to include("Hostname: #{v1.external_host}")
555
+ expect(lines).to include("Hostname: #{v2.external_host}")
556
+ expect(lines).to include("Hostname: #{v3.external_host}")
557
+
558
+ expect(lines[0]).not_to eq("\n")
559
+ expect(lines[-1]).not_to eq("\n")
560
+ end
561
+ end
562
+
563
+ describe 'endpoints:deprovison' do
564
+ it 'deprovisions an Endpoint' do
565
+ s1 = Fabricate(:service, app: app)
566
+ Fabricate(:vhost, service: s1)
567
+
568
+ s2 = Fabricate(:service, app: app)
569
+ v2 = Fabricate(:vhost, service: s2)
570
+ Fabricate(:vhost, service: s2)
571
+
572
+ expect_operation(v2, 'deprovision')
573
+ subject.send('endpoints:deprovision', v2.external_host)
574
+ end
575
+
576
+ it 'fails if the Endpoint does not exist' do
577
+ s1 = Fabricate(:service, app: app)
578
+ Fabricate(:vhost, service: s1, external_host: 'qux.io')
579
+
580
+ expect { subject.send('endpoints:deprovision', 'foo.io') }
581
+ .to raise_error(/endpoint.*foo\.io.*does not exist.*qux\.io/im)
582
+ end
583
+ end
584
+
585
+ describe 'endpoints:renew' do
586
+ it 'renews an Endpoint' do
587
+ s1 = Fabricate(:service, app: app)
588
+ Fabricate(:vhost, service: s1)
589
+
590
+ s2 = Fabricate(:service, app: app)
591
+ v2 = Fabricate(:vhost, service: s2)
592
+ Fabricate(:vhost, service: s2)
593
+
594
+ expect_operation(v2, 'renew')
595
+ subject.send('endpoints:renew', v2.external_host)
596
+ end
597
+
598
+ it 'fails if the Endpoint does not exist' do
599
+ expect { subject.send('endpoints:deprovision', 'foo.io') }
600
+ .to raise_error(/endpoint.*foo\.io.*does not exist/im)
601
+ end
602
+ end
603
+ end
604
+ end