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 +4 -4
- data/README.md +12 -2
- data/aptible-cli.gemspec +4 -3
- data/lib/aptible/cli/agent.rb +9 -2
- data/lib/aptible/cli/helpers/app.rb +19 -0
- data/lib/aptible/cli/helpers/app_or_database.rb +34 -0
- data/lib/aptible/cli/helpers/vhost.rb +83 -0
- data/lib/aptible/cli/helpers/vhost/option_set_builder.rb +292 -0
- data/lib/aptible/cli/subcommands/apps.rb +1 -13
- data/lib/aptible/cli/subcommands/domains.rb +3 -1
- data/lib/aptible/cli/subcommands/endpoints.rb +183 -0
- data/lib/aptible/cli/subcommands/logs.rb +5 -16
- data/lib/aptible/cli/version.rb +1 -1
- data/spec/aptible/cli/helpers/vhost_spec.rb +105 -0
- data/spec/aptible/cli/subcommands/apps_spec.rb +1 -1
- data/spec/aptible/cli/subcommands/endpoints_spec.rb +604 -0
- data/spec/fabricators/account_fabricator.rb +7 -1
- data/spec/fabricators/app_fabricator.rb +5 -0
- data/spec/fabricators/certificate_fabricator.rb +11 -0
- data/spec/fabricators/database_fabricator.rb +10 -1
- data/spec/fabricators/service_fabricator.rb +25 -3
- data/spec/fabricators/vhost_fabricator.rb +5 -2
- metadata +38 -8
@@ -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
|
-
|
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',
|
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::
|
11
|
-
include Helpers::Database
|
10
|
+
include Helpers::AppOrDatabase
|
12
11
|
|
13
|
-
desc 'logs
|
14
|
-
|
15
|
-
|
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
|
-
|
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. ' \
|
data/lib/aptible/cli/version.rb
CHANGED
@@ -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
|