aptible-cli 0.14.1 → 0.15.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 +10 -1
- data/aptible-cli.gemspec +1 -0
- data/bin/aptible +9 -5
- data/lib/aptible/cli.rb +36 -0
- data/lib/aptible/cli/agent.rb +10 -6
- data/lib/aptible/cli/error.rb +6 -0
- data/lib/aptible/cli/formatter.rb +21 -0
- data/lib/aptible/cli/formatter/grouped_keyed_list.rb +54 -0
- data/lib/aptible/cli/formatter/keyed_list.rb +25 -0
- data/lib/aptible/cli/formatter/keyed_object.rb +16 -0
- data/lib/aptible/cli/formatter/list.rb +33 -0
- data/lib/aptible/cli/formatter/node.rb +8 -0
- data/lib/aptible/cli/formatter/object.rb +38 -0
- data/lib/aptible/cli/formatter/root.rb +46 -0
- data/lib/aptible/cli/formatter/value.rb +25 -0
- data/lib/aptible/cli/helpers/app.rb +1 -0
- data/lib/aptible/cli/helpers/database.rb +22 -6
- data/lib/aptible/cli/helpers/operation.rb +3 -2
- data/lib/aptible/cli/helpers/tunnel.rb +1 -3
- data/lib/aptible/cli/helpers/vhost.rb +9 -46
- data/lib/aptible/cli/renderer.rb +26 -0
- data/lib/aptible/cli/renderer/base.rb +8 -0
- data/lib/aptible/cli/renderer/json.rb +26 -0
- data/lib/aptible/cli/renderer/text.rb +99 -0
- data/lib/aptible/cli/resource_formatter.rb +136 -0
- data/lib/aptible/cli/subcommands/apps.rb +26 -14
- data/lib/aptible/cli/subcommands/backup.rb +22 -4
- data/lib/aptible/cli/subcommands/config.rb +15 -11
- data/lib/aptible/cli/subcommands/db.rb +82 -31
- data/lib/aptible/cli/subcommands/deploy.rb +1 -1
- data/lib/aptible/cli/subcommands/endpoints.rb +11 -8
- data/lib/aptible/cli/subcommands/operation.rb +2 -1
- data/lib/aptible/cli/subcommands/rebuild.rb +1 -1
- data/lib/aptible/cli/subcommands/restart.rb +1 -1
- data/lib/aptible/cli/subcommands/services.rb +8 -9
- data/lib/aptible/cli/version.rb +1 -1
- data/spec/aptible/cli/agent_spec.rb +11 -14
- data/spec/aptible/cli/formatter_spec.rb +4 -0
- data/spec/aptible/cli/renderer/json_spec.rb +63 -0
- data/spec/aptible/cli/renderer/text_spec.rb +150 -0
- data/spec/aptible/cli/resource_formatter_spec.rb +113 -0
- data/spec/aptible/cli/subcommands/apps_spec.rb +144 -28
- data/spec/aptible/cli/subcommands/backup_spec.rb +37 -16
- data/spec/aptible/cli/subcommands/config_spec.rb +95 -0
- data/spec/aptible/cli/subcommands/db_spec.rb +185 -93
- data/spec/aptible/cli/subcommands/endpoints_spec.rb +10 -8
- data/spec/aptible/cli/subcommands/operation_spec.rb +0 -1
- data/spec/aptible/cli/subcommands/rebuild_spec.rb +17 -0
- data/spec/aptible/cli/subcommands/services_spec.rb +8 -12
- data/spec/aptible/cli_spec.rb +31 -0
- data/spec/fabricators/account_fabricator.rb +11 -0
- data/spec/fabricators/app_fabricator.rb +15 -0
- data/spec/fabricators/configuration_fabricator.rb +8 -0
- data/spec/fabricators/database_image_fabricator.rb +17 -0
- data/spec/fabricators/operation_fabricator.rb +1 -0
- data/spec/fabricators/service_fabricator.rb +4 -0
- data/spec/spec_helper.rb +63 -1
- metadata +55 -4
- data/spec/aptible/cli/helpers/vhost_spec.rb +0 -105
@@ -40,12 +40,6 @@ module Aptible
|
|
40
40
|
databases.select { |a| a.handle == handle }
|
41
41
|
end
|
42
42
|
|
43
|
-
def present_environment_databases(environment)
|
44
|
-
say "=== #{environment.handle}"
|
45
|
-
environment.databases.each { |db| say db.handle }
|
46
|
-
say ''
|
47
|
-
end
|
48
|
-
|
49
43
|
def clone_database(source, dest_handle)
|
50
44
|
op = source.create_operation!(type: 'clone', handle: dest_handle)
|
51
45
|
attach_to_operation_logs(op)
|
@@ -120,6 +114,28 @@ module Aptible
|
|
120
114
|
err = "No credential with type #{type} for database" if type
|
121
115
|
raise Thor::Error, "#{err}, valid credential types: #{valid}"
|
122
116
|
end
|
117
|
+
|
118
|
+
def find_database_image(type, version)
|
119
|
+
available_versions = []
|
120
|
+
|
121
|
+
Aptible::Api::DatabaseImage.all(token: fetch_token).each do |i|
|
122
|
+
next unless i.type == type
|
123
|
+
return i if i.version == version
|
124
|
+
available_versions << i.version
|
125
|
+
end
|
126
|
+
|
127
|
+
err = "No Database Image of type #{type} with version #{version}"
|
128
|
+
err = "#{err}, valid versions: #{available_versions.join(' ')}"
|
129
|
+
raise Thor::Error, err
|
130
|
+
end
|
131
|
+
|
132
|
+
def render_database(database, account)
|
133
|
+
Formatter.render(Renderer.current) do |root|
|
134
|
+
root.keyed_object('connection_url') do |node|
|
135
|
+
ResourceFormatter.inject_database(node, database, account)
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
123
139
|
end
|
124
140
|
end
|
125
141
|
end
|
@@ -37,13 +37,14 @@ module Aptible
|
|
37
37
|
# operation failed, poll_for_success will immediately fall through to
|
38
38
|
# the error message.
|
39
39
|
unless code == 0
|
40
|
-
|
40
|
+
e = 'Disconnected from logs, waiting for operation to complete'
|
41
|
+
CLI.logger.warn e
|
41
42
|
poll_for_success(operation)
|
42
43
|
end
|
43
44
|
end
|
44
45
|
|
45
46
|
def cancel_operation(operation)
|
46
|
-
|
47
|
+
CLI.logger.info "Cancelling #{prettify_operation(operation)}..."
|
47
48
|
operation.update!(cancelled: true)
|
48
49
|
end
|
49
50
|
|
@@ -53,9 +53,7 @@ module Aptible
|
|
53
53
|
out_read.readline
|
54
54
|
rescue EOFError
|
55
55
|
stop
|
56
|
-
|
57
|
-
"#{@local_port}?\n#{err_read.read}"
|
58
|
-
raise e
|
56
|
+
raise UserError, "Tunnel did not come up: #{err_read.read}"
|
59
57
|
ensure
|
60
58
|
[out_read, err_read].map(&:close)
|
61
59
|
end
|
@@ -2,52 +2,15 @@ module Aptible
|
|
2
2
|
module CLI
|
3
3
|
module Helpers
|
4
4
|
module Vhost
|
5
|
-
def explain_vhost(service, vhost)
|
6
|
-
say "Service: #{service.process_type}"
|
7
|
-
say "Hostname: #{vhost.external_host}"
|
8
|
-
say "Status: #{vhost.status}"
|
9
|
-
|
10
|
-
case vhost.type
|
11
|
-
when 'tcp', 'tls'
|
12
|
-
ports = if vhost.container_ports.any?
|
13
|
-
vhost.container_ports.map(&:to_s).join(' ')
|
14
|
-
else
|
15
|
-
'all'
|
16
|
-
end
|
17
|
-
say "Type: #{vhost.type}"
|
18
|
-
say "Ports: #{ports}"
|
19
|
-
when 'http', 'http_proxy_protocol'
|
20
|
-
port = vhost.container_port ? vhost.container_port : 'default'
|
21
|
-
say 'Type: https'
|
22
|
-
say "Port: #{port}"
|
23
|
-
end
|
24
|
-
|
25
|
-
say "Internal: #{vhost.internal}"
|
26
|
-
|
27
|
-
ip_whitelist = if vhost.ip_whitelist.any?
|
28
|
-
vhost.ip_whitelist.join(' ')
|
29
|
-
else
|
30
|
-
'all traffic'
|
31
|
-
end
|
32
|
-
say "IP Whitelist: #{ip_whitelist}"
|
33
|
-
|
34
|
-
say "Default Domain Enabled: #{vhost.default}"
|
35
|
-
say "Default Domain: #{vhost.virtual_domain}" if vhost.default
|
36
|
-
|
37
|
-
say "Managed TLS Enabled: #{vhost.acme}"
|
38
|
-
if vhost.acme
|
39
|
-
say "Managed TLS Domain: #{vhost.user_domain}"
|
40
|
-
say 'Managed TLS DNS Challenge Hostname: ' \
|
41
|
-
"#{vhost.acme_dns_challenge_host}"
|
42
|
-
say "Managed TLS Status: #{vhost.acme_status}"
|
43
|
-
end
|
44
|
-
end
|
45
|
-
|
46
5
|
def provision_vhost_and_explain(service, vhost)
|
47
6
|
op = vhost.create_operation!(type: 'provision')
|
48
7
|
attach_to_operation_logs(op)
|
49
|
-
|
50
|
-
|
8
|
+
|
9
|
+
Formatter.render(Renderer.current) do |root|
|
10
|
+
root.object do |node|
|
11
|
+
ResourceFormatter.inject_vhost(node, vhost.reload, service)
|
12
|
+
end
|
13
|
+
end
|
51
14
|
end
|
52
15
|
|
53
16
|
def find_vhost(service_enumerator, hostname)
|
@@ -65,10 +28,10 @@ module Aptible
|
|
65
28
|
raise Thor::Error, e
|
66
29
|
end
|
67
30
|
|
68
|
-
def
|
69
|
-
return enum_for(:
|
70
|
-
|
31
|
+
def each_service(resource, &block)
|
32
|
+
return enum_for(:each_service, resource) if block.nil?
|
71
33
|
klass = resource.class
|
34
|
+
|
72
35
|
if klass == Aptible::Api::App
|
73
36
|
resource.each_service(&block)
|
74
37
|
elsif klass == Aptible::Api::Database
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'json'
|
2
|
+
|
3
|
+
require_relative 'renderer/base'
|
4
|
+
require_relative 'renderer/json'
|
5
|
+
require_relative 'renderer/text'
|
6
|
+
|
7
|
+
module Aptible
|
8
|
+
module CLI
|
9
|
+
module Renderer
|
10
|
+
FORMAT_VAR = 'APTIBLE_OUTPUT_FORMAT'.freeze
|
11
|
+
|
12
|
+
def self.current
|
13
|
+
case (format = ENV[FORMAT_VAR])
|
14
|
+
when 'json'
|
15
|
+
Json.new
|
16
|
+
when 'text'
|
17
|
+
Text.new
|
18
|
+
when nil
|
19
|
+
Text.new
|
20
|
+
else
|
21
|
+
raise UserError, "Invalid #{FORMAT_VAR}: #{format}"
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module Aptible
|
2
|
+
module CLI
|
3
|
+
module Renderer
|
4
|
+
class Json < Base
|
5
|
+
def visit(node)
|
6
|
+
case node
|
7
|
+
when Formatter::Root
|
8
|
+
visit(node.root)
|
9
|
+
when Formatter::Object
|
10
|
+
Hash[node.children.each_pair.map { |k, c| [k, visit(c)] }]
|
11
|
+
when Formatter::List
|
12
|
+
node.children.map { |c| visit(c) }
|
13
|
+
when Formatter::Value
|
14
|
+
node.value
|
15
|
+
else
|
16
|
+
raise "Unhandled node: #{node.inspect}"
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def render(node)
|
21
|
+
JSON.pretty_generate(visit(node))
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
module Aptible
|
2
|
+
module CLI
|
3
|
+
module Renderer
|
4
|
+
class Text < Base
|
5
|
+
include ActiveSupport::Inflector
|
6
|
+
|
7
|
+
POST_PROCESSED_KEYS = {
|
8
|
+
'Tls' => 'TLS',
|
9
|
+
'Dns' => 'DNS',
|
10
|
+
'Ip' => 'IP'
|
11
|
+
}.freeze
|
12
|
+
|
13
|
+
def visit(node, io)
|
14
|
+
case node
|
15
|
+
when Formatter::Root
|
16
|
+
visit(node.root, io)
|
17
|
+
when Formatter::KeyedObject
|
18
|
+
visit(node.children.fetch(node.key), io)
|
19
|
+
when Formatter::Object
|
20
|
+
# TODO: We should have a way to fail in tests if we're going to
|
21
|
+
# nest an object under another object (or at least handle it
|
22
|
+
# decently).
|
23
|
+
#
|
24
|
+
# Right now, it provides unusable output like this:
|
25
|
+
#
|
26
|
+
# Foo: Bar: bar
|
27
|
+
# Qux: qux
|
28
|
+
#
|
29
|
+
# (when rendering { foo => { bar => bar }, qux => qux })
|
30
|
+
#
|
31
|
+
# The solution to this problem is typically to make sure the
|
32
|
+
# children are KeyedObject instances so they can render properly,
|
33
|
+
# but we need to warn in tests that this is required.
|
34
|
+
node.children.each_pair do |k, c|
|
35
|
+
io.print "#{format_key(k)}: "
|
36
|
+
visit(c, io)
|
37
|
+
end
|
38
|
+
when Formatter::GroupedKeyedList
|
39
|
+
enum = spacer_enumerator
|
40
|
+
node.groups.each_pair.sort_by(&:first).each do |key, group|
|
41
|
+
io.print enum.next
|
42
|
+
io.print '=== '
|
43
|
+
nodes = group.map { |n| n.children.fetch(node.key) }
|
44
|
+
visit(key, io)
|
45
|
+
output_list(nodes, io)
|
46
|
+
end
|
47
|
+
when Formatter::KeyedList
|
48
|
+
nodes = node.children.map { |n| n.children.fetch(node.key) }
|
49
|
+
output_list(nodes, io)
|
50
|
+
when Formatter::List
|
51
|
+
output_list(node.children, io)
|
52
|
+
when Formatter::Value
|
53
|
+
io.puts node.value
|
54
|
+
else
|
55
|
+
raise "Unhandled node: #{node.inspect}"
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def render(node)
|
60
|
+
io = StringIO.new
|
61
|
+
visit(node, io)
|
62
|
+
io.rewind
|
63
|
+
io.read
|
64
|
+
end
|
65
|
+
|
66
|
+
private
|
67
|
+
|
68
|
+
def output_list(nodes, io)
|
69
|
+
if nodes.all? { |v| v.is_a?(Formatter::Value) }
|
70
|
+
# All nodes are single values, so we render one per line.
|
71
|
+
nodes.each { |c| visit(c, io) }
|
72
|
+
else
|
73
|
+
# Nested values. Display each as a block with newlines in between.
|
74
|
+
enum = spacer_enumerator
|
75
|
+
nodes.each do |c|
|
76
|
+
io.print enum.next
|
77
|
+
visit(c, io)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def format_key(key)
|
83
|
+
key = titleize(humanize(key))
|
84
|
+
POST_PROCESSED_KEYS.each_pair do |pk, pv|
|
85
|
+
key = key.gsub(/(^|\W)#{Regexp.escape(pk)}($|\W)/, "\\1#{pv}\\2")
|
86
|
+
end
|
87
|
+
key
|
88
|
+
end
|
89
|
+
|
90
|
+
def spacer_enumerator
|
91
|
+
Enumerator.new do |y|
|
92
|
+
y << ''
|
93
|
+
loop { y << "\n" }
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
@@ -0,0 +1,136 @@
|
|
1
|
+
module Aptible
|
2
|
+
module CLI
|
3
|
+
module ResourceFormatter
|
4
|
+
class << self
|
5
|
+
NO_NESTING = Object.new.freeze
|
6
|
+
|
7
|
+
def inject_account(node, account)
|
8
|
+
node.value('id', account.id)
|
9
|
+
node.value('handle', account.handle)
|
10
|
+
end
|
11
|
+
|
12
|
+
def inject_app(node, app, account)
|
13
|
+
node.value('id', app.id)
|
14
|
+
node.value('handle', app.handle)
|
15
|
+
|
16
|
+
node.value('status', app.status)
|
17
|
+
node.value('git_remote', app.git_repo)
|
18
|
+
|
19
|
+
node.list('services') do |services_list|
|
20
|
+
app.each_service do |service|
|
21
|
+
services_list.object do |n|
|
22
|
+
inject_service(n, service, NO_NESTING)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
attach_account(node, account)
|
28
|
+
end
|
29
|
+
|
30
|
+
def inject_database(node, database, account)
|
31
|
+
node.value('id', database.id)
|
32
|
+
node.value('handle', database.handle)
|
33
|
+
|
34
|
+
node.value('type', database.type)
|
35
|
+
node.value('status', database.status)
|
36
|
+
node.value('connection_url', database.connection_url)
|
37
|
+
|
38
|
+
node.list('credentials') do |creds_list|
|
39
|
+
database.database_credentials.each do |cred|
|
40
|
+
creds_list.object { |n| inject_credential(n, cred) }
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
attach_account(node, account)
|
45
|
+
end
|
46
|
+
|
47
|
+
def inject_credential(node, credential)
|
48
|
+
# TODO: Should this accept a DB for nesting? Maybe if we have any
|
49
|
+
# callers that could benefit from it.
|
50
|
+
node.value('type', credential.type)
|
51
|
+
node.value('connection_url', credential.connection_url)
|
52
|
+
node.value('default', credential.default)
|
53
|
+
end
|
54
|
+
|
55
|
+
def inject_service(node, service, app)
|
56
|
+
node.value('id', service.id)
|
57
|
+
node.value('service', service.process_type)
|
58
|
+
|
59
|
+
node.value('command', service.command || 'CMD')
|
60
|
+
node.value('container_count', service.container_count)
|
61
|
+
node.value('container_size', service.container_memory_limit_mb)
|
62
|
+
|
63
|
+
attach_app(node, app)
|
64
|
+
end
|
65
|
+
|
66
|
+
def inject_vhost(node, vhost, service)
|
67
|
+
node.value('id', vhost.id)
|
68
|
+
node.value('hostname', vhost.external_host)
|
69
|
+
node.value('status', vhost.status)
|
70
|
+
|
71
|
+
case vhost.type
|
72
|
+
when 'tcp', 'tls'
|
73
|
+
ports = if vhost.container_ports.any?
|
74
|
+
vhost.container_ports.map(&:to_s).join(' ')
|
75
|
+
else
|
76
|
+
'all'
|
77
|
+
end
|
78
|
+
node.value('type', vhost.type)
|
79
|
+
node.value('ports', ports)
|
80
|
+
when 'http', 'http_proxy_protocol'
|
81
|
+
port = vhost.container_port ? vhost.container_port : 'default'
|
82
|
+
node.value('type', 'https')
|
83
|
+
node.value('port', port)
|
84
|
+
end
|
85
|
+
|
86
|
+
node.value('internal', vhost.internal)
|
87
|
+
|
88
|
+
ip_whitelist = if vhost.ip_whitelist.any?
|
89
|
+
vhost.ip_whitelist.join(' ')
|
90
|
+
else
|
91
|
+
'all traffic'
|
92
|
+
end
|
93
|
+
node.value('ip_whitelist', ip_whitelist)
|
94
|
+
|
95
|
+
node.value('default_domain_enabled', vhost.default)
|
96
|
+
node.value('default_domain', vhost.virtual_domain) if vhost.default
|
97
|
+
|
98
|
+
node.value('managed_tls_enabled', vhost.acme)
|
99
|
+
if vhost.acme
|
100
|
+
node.value('managed_tls_domain', vhost.user_domain)
|
101
|
+
node.value(
|
102
|
+
'managed_tls_dns_challenge_hostname',
|
103
|
+
vhost.acme_dns_challenge_host
|
104
|
+
)
|
105
|
+
node.value('managed_tls_status', vhost.acme_status)
|
106
|
+
end
|
107
|
+
|
108
|
+
attach_service(node, service)
|
109
|
+
end
|
110
|
+
|
111
|
+
private
|
112
|
+
|
113
|
+
def attach_account(node, account)
|
114
|
+
return if NO_NESTING.eql?(account)
|
115
|
+
node.keyed_object('environment', 'handle') do |n|
|
116
|
+
inject_account(n, account)
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
def attach_app(node, app)
|
121
|
+
return if NO_NESTING.eql?(app)
|
122
|
+
node.keyed_object('app', 'handle') do |n|
|
123
|
+
inject_app(n, app, NO_NESTING)
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
def attach_service(node, service)
|
128
|
+
return if NO_NESTING.eql?(service)
|
129
|
+
node.keyed_object('service', 'service') do |n|
|
130
|
+
inject_service(n, service, NO_NESTING)
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|