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.
@@ -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