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.
Files changed (60) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +10 -1
  3. data/aptible-cli.gemspec +1 -0
  4. data/bin/aptible +9 -5
  5. data/lib/aptible/cli.rb +36 -0
  6. data/lib/aptible/cli/agent.rb +10 -6
  7. data/lib/aptible/cli/error.rb +6 -0
  8. data/lib/aptible/cli/formatter.rb +21 -0
  9. data/lib/aptible/cli/formatter/grouped_keyed_list.rb +54 -0
  10. data/lib/aptible/cli/formatter/keyed_list.rb +25 -0
  11. data/lib/aptible/cli/formatter/keyed_object.rb +16 -0
  12. data/lib/aptible/cli/formatter/list.rb +33 -0
  13. data/lib/aptible/cli/formatter/node.rb +8 -0
  14. data/lib/aptible/cli/formatter/object.rb +38 -0
  15. data/lib/aptible/cli/formatter/root.rb +46 -0
  16. data/lib/aptible/cli/formatter/value.rb +25 -0
  17. data/lib/aptible/cli/helpers/app.rb +1 -0
  18. data/lib/aptible/cli/helpers/database.rb +22 -6
  19. data/lib/aptible/cli/helpers/operation.rb +3 -2
  20. data/lib/aptible/cli/helpers/tunnel.rb +1 -3
  21. data/lib/aptible/cli/helpers/vhost.rb +9 -46
  22. data/lib/aptible/cli/renderer.rb +26 -0
  23. data/lib/aptible/cli/renderer/base.rb +8 -0
  24. data/lib/aptible/cli/renderer/json.rb +26 -0
  25. data/lib/aptible/cli/renderer/text.rb +99 -0
  26. data/lib/aptible/cli/resource_formatter.rb +136 -0
  27. data/lib/aptible/cli/subcommands/apps.rb +26 -14
  28. data/lib/aptible/cli/subcommands/backup.rb +22 -4
  29. data/lib/aptible/cli/subcommands/config.rb +15 -11
  30. data/lib/aptible/cli/subcommands/db.rb +82 -31
  31. data/lib/aptible/cli/subcommands/deploy.rb +1 -1
  32. data/lib/aptible/cli/subcommands/endpoints.rb +11 -8
  33. data/lib/aptible/cli/subcommands/operation.rb +2 -1
  34. data/lib/aptible/cli/subcommands/rebuild.rb +1 -1
  35. data/lib/aptible/cli/subcommands/restart.rb +1 -1
  36. data/lib/aptible/cli/subcommands/services.rb +8 -9
  37. data/lib/aptible/cli/version.rb +1 -1
  38. data/spec/aptible/cli/agent_spec.rb +11 -14
  39. data/spec/aptible/cli/formatter_spec.rb +4 -0
  40. data/spec/aptible/cli/renderer/json_spec.rb +63 -0
  41. data/spec/aptible/cli/renderer/text_spec.rb +150 -0
  42. data/spec/aptible/cli/resource_formatter_spec.rb +113 -0
  43. data/spec/aptible/cli/subcommands/apps_spec.rb +144 -28
  44. data/spec/aptible/cli/subcommands/backup_spec.rb +37 -16
  45. data/spec/aptible/cli/subcommands/config_spec.rb +95 -0
  46. data/spec/aptible/cli/subcommands/db_spec.rb +185 -93
  47. data/spec/aptible/cli/subcommands/endpoints_spec.rb +10 -8
  48. data/spec/aptible/cli/subcommands/operation_spec.rb +0 -1
  49. data/spec/aptible/cli/subcommands/rebuild_spec.rb +17 -0
  50. data/spec/aptible/cli/subcommands/services_spec.rb +8 -12
  51. data/spec/aptible/cli_spec.rb +31 -0
  52. data/spec/fabricators/account_fabricator.rb +11 -0
  53. data/spec/fabricators/app_fabricator.rb +15 -0
  54. data/spec/fabricators/configuration_fabricator.rb +8 -0
  55. data/spec/fabricators/database_image_fabricator.rb +17 -0
  56. data/spec/fabricators/operation_fabricator.rb +1 -0
  57. data/spec/fabricators/service_fabricator.rb +4 -0
  58. data/spec/spec_helper.rb +63 -1
  59. metadata +55 -4
  60. data/spec/aptible/cli/helpers/vhost_spec.rb +0 -105
@@ -152,6 +152,7 @@ module Aptible
152
152
  end
153
153
 
154
154
  def apps_from_handle(handle, environment)
155
+ # TODO: This should probably use each_app for more efficiency.
155
156
  if environment
156
157
  environment.apps
157
158
  else
@@ -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
- puts 'Disconnected from logs, waiting for operation to complete'
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
- puts "Cancelling #{prettify_operation(operation)}..."
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
- e = 'Tunnel did not come up, is something else listening on port ' \
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
- explain_vhost(service, vhost.reload)
50
- # TODO: Instructions if ACME is enabled?
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 each_vhost(resource, &block)
69
- return enum_for(:each_vhost, resource) unless block_given?
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,8 @@
1
+ module Aptible
2
+ module CLI
3
+ module Renderer
4
+ class Base
5
+ end
6
+ end
7
+ end
8
+ 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