aptible-cli 0.14.1 → 0.15.0

Sign up to get free protection for your applications and to get access to all the features.
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