console_kit 1.3.0 → 1.5.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/lib/console_kit/configuration.rb +15 -21
- data/lib/console_kit/configuration_validator.rb +179 -0
- data/lib/console_kit/connections/base_connection_handler.rb +139 -25
- data/lib/console_kit/connections/connection_manager.rb +42 -8
- data/lib/console_kit/connections/dashboard.rb +3 -11
- data/lib/console_kit/connections/diagnostic_helpers.rb +24 -5
- data/lib/console_kit/connections/elasticsearch_connection_handler.rb +87 -32
- data/lib/console_kit/connections/elasticsearch_prefix_registry.rb +70 -0
- data/lib/console_kit/connections/mongo_connection_handler.rb +51 -46
- data/lib/console_kit/connections/mongoid_support.rb +56 -0
- data/lib/console_kit/connections/redis_client_adapter.rb +92 -0
- data/lib/console_kit/connections/redis_connection_handler.rb +97 -41
- data/lib/console_kit/connections/sql_connection_handler.rb +66 -30
- data/lib/console_kit/connections/sql_strategy.rb +248 -0
- data/lib/console_kit/connections/table_renderer.rb +30 -20
- data/lib/console_kit/console_helpers.rb +17 -31
- data/lib/console_kit/diagnostics.rb +143 -0
- data/lib/console_kit/errors.rb +93 -0
- data/lib/console_kit/instrumentation.rb +57 -0
- data/lib/console_kit/output.rb +5 -14
- data/lib/console_kit/prompt.rb +16 -29
- data/lib/console_kit/railtie.rb +2 -3
- data/lib/console_kit/setup.rb +66 -13
- data/lib/console_kit/setup_ui.rb +1 -4
- data/lib/console_kit/tenant_configurator/context_wrapper.rb +51 -23
- data/lib/console_kit/tenant_configurator.rb +40 -78
- data/lib/console_kit/tenant_orchestrator.rb +31 -27
- data/lib/console_kit/tenant_plan.rb +68 -0
- data/lib/console_kit/tenant_rollback.rb +77 -0
- data/lib/console_kit/tenant_selector.rb +3 -6
- data/lib/console_kit/tenant_state.rb +73 -0
- data/lib/console_kit/tenant_switch.rb +124 -0
- data/lib/console_kit/version.rb +1 -1
- data/lib/console_kit.rb +51 -24
- data/lib/generators/console_kit/install_generator.rb +1 -12
- data/lib/generators/console_kit/templates/console_kit.rb +4 -0
- metadata +15 -6
- data/.DS_Store +0 -0
- data/.reek.yml.new +0 -0
- data/lib/console_kit/connections/table_formatter.rb +0 -37
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 88e21758bbea2b210aedef584fabb6da0bd68084adfc5a00a831b756bd923b1c
|
|
4
|
+
data.tar.gz: 0fc14b5091c42181416f065a9513875daa59847b4f919f293d5ed548e494c83b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8f04ee40c2c17bfea636739a4de3d8578fc26da679dbc638bb606931f09ab951852c5b2f212132eaf85f61b411bd09a3a88fc48dca7caa48dc40106587c8fb4a
|
|
7
|
+
data.tar.gz: 16be45ab2a923ffe6c7a4e772175cb614787faf3de5bbc6d20b53f6dcf2b25c77a3b61e0da80c814cd430fabe3a85f7b2c54844b1121810f062ccf03f5844088
|
|
@@ -1,39 +1,33 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative 'configuration_validator'
|
|
4
|
+
|
|
3
5
|
module ConsoleKit
|
|
4
|
-
# Stores ConsoleKit configurations such as tenant map and context behavior
|
|
5
6
|
class Configuration
|
|
6
|
-
|
|
7
|
-
|
|
7
|
+
attr_accessor :pretty_output, :tenants, :sql_base_class, :show_dashboard
|
|
8
|
+
attr_writer :context_class
|
|
8
9
|
|
|
9
10
|
def initialize
|
|
10
|
-
@
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
define_method("#{attr}=") { |val| @settings.send("#{attr}=", val) }
|
|
11
|
+
@pretty_output = true
|
|
12
|
+
@context_class = nil
|
|
13
|
+
@tenants = nil
|
|
14
|
+
@sql_base_class = 'ApplicationRecord'
|
|
15
|
+
@show_dashboard = false
|
|
16
16
|
end
|
|
17
17
|
|
|
18
18
|
def context_class
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
else val
|
|
19
|
+
case @context_class
|
|
20
|
+
when String, Symbol then resolve_context_class(@context_class)
|
|
21
|
+
else @context_class
|
|
23
22
|
end
|
|
24
23
|
end
|
|
25
24
|
|
|
26
|
-
def validate
|
|
27
|
-
validate!
|
|
28
|
-
true
|
|
29
|
-
rescue Error
|
|
30
|
-
false
|
|
31
|
-
end
|
|
32
|
-
|
|
33
25
|
def validate!
|
|
34
26
|
raise Error, 'ConsoleKit: `tenants` is not configured.' if tenants.blank?
|
|
35
27
|
raise Error, 'ConsoleKit: `tenants` must be a Hash.' unless tenants.is_a?(Hash)
|
|
36
|
-
raise Error, 'ConsoleKit: `context_class` is not configured.' if @
|
|
28
|
+
raise Error, 'ConsoleKit: `context_class` is not configured.' if @context_class.blank?
|
|
29
|
+
|
|
30
|
+
ConfigurationValidator.new(self).validate!
|
|
37
31
|
end
|
|
38
32
|
|
|
39
33
|
private
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'errors'
|
|
4
|
+
require_relative 'output'
|
|
5
|
+
require_relative 'connections/diagnostic_helpers'
|
|
6
|
+
|
|
7
|
+
module ConsoleKit
|
|
8
|
+
class TenantEntry
|
|
9
|
+
EXTRA_CONSTANTS_KEYS = [:environment].freeze
|
|
10
|
+
|
|
11
|
+
attr_reader :key, :errors, :warnings
|
|
12
|
+
|
|
13
|
+
def initialize(key, entry)
|
|
14
|
+
@key = key
|
|
15
|
+
@entry = entry
|
|
16
|
+
@errors = []
|
|
17
|
+
@warnings = []
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def constants
|
|
21
|
+
constants = @entry[:constants] if @entry.is_a?(Hash)
|
|
22
|
+
constants if constants.is_a?(Hash)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def validate
|
|
26
|
+
check_identifier
|
|
27
|
+
return @errors << "ConsoleKit: tenant #{@key.inspect} configuration must be a Hash, got #{@entry.class}." \
|
|
28
|
+
unless @entry.is_a?(Hash)
|
|
29
|
+
|
|
30
|
+
validate_constants
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def validate_constants
|
|
36
|
+
declared = @entry[:constants]
|
|
37
|
+
tenant = @key.inspect
|
|
38
|
+
return @errors << "ConsoleKit: tenant #{tenant} is missing a `:constants` Hash." if declared.nil?
|
|
39
|
+
unless declared.is_a?(Hash)
|
|
40
|
+
return @errors << "ConsoleKit: tenant #{tenant} `:constants` must be a Hash, got #{declared.class}."
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
check_required_keys
|
|
44
|
+
check_constants_values
|
|
45
|
+
warn_unknown_keys
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def check_identifier
|
|
49
|
+
return if (@key.is_a?(Symbol) || @key.is_a?(String)) && @key.to_s.strip.present?
|
|
50
|
+
|
|
51
|
+
@errors << "ConsoleKit: tenant identifier #{@key.inspect} must be a non-blank Symbol or String."
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def check_required_keys
|
|
55
|
+
required = TenantPlan::REQUIRED_KEYS
|
|
56
|
+
missing = required - constants.keys
|
|
57
|
+
return if missing.empty?
|
|
58
|
+
|
|
59
|
+
@errors << "ConsoleKit: tenant #{@key.inspect} constants missing required keys: #{missing.join(', ')} " \
|
|
60
|
+
"(expected: #{required.join(', ')})."
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def check_constants_values
|
|
64
|
+
values = constants
|
|
65
|
+
Connections::BaseConnectionHandler.registry.each do |handler_class|
|
|
66
|
+
field = handler_class.constants_key
|
|
67
|
+
next unless values.key?(field)
|
|
68
|
+
|
|
69
|
+
check_backend_value(field, values[field], handler_class)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def check_backend_value(field, value, handler_class)
|
|
74
|
+
reason = handler_class.target_error(value)
|
|
75
|
+
return unless reason
|
|
76
|
+
|
|
77
|
+
scrubbed = Connections::DiagnosticHelpers.scrub(value.inspect)
|
|
78
|
+
@errors << "ConsoleKit: tenant #{@key.inspect} #{field} #{scrubbed} is invalid: #{reason}"
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def warn_unknown_keys
|
|
82
|
+
recognised = TenantConfigurator.context_mapping.values + EXTRA_CONSTANTS_KEYS
|
|
83
|
+
extra = constants.keys - recognised
|
|
84
|
+
return if extra.empty?
|
|
85
|
+
|
|
86
|
+
@warnings << "tenant #{@key.inspect} constants has unrecognised keys: #{extra.map(&:inspect).join(', ')} " \
|
|
87
|
+
"(recognised: #{recognised.map(&:inspect).join(', ')}). Check for typos."
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
class BackendCoverage
|
|
92
|
+
WARNING = 'tenant %<tenant>p does not name %<omitted>s, which other tenants do. Switching to it RESETS those ' \
|
|
93
|
+
'backends to their defaults rather than leaving them on the tenant before it.'
|
|
94
|
+
|
|
95
|
+
def initialize(entries)
|
|
96
|
+
@entries = entries.select(&:constants)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def warnings
|
|
100
|
+
@entries.filter_map do |entry|
|
|
101
|
+
omitted = named - entry.constants.keys
|
|
102
|
+
next if omitted.empty?
|
|
103
|
+
|
|
104
|
+
format(WARNING, tenant: entry.key, omitted: omitted.map(&:inspect).join(', '))
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
private
|
|
109
|
+
|
|
110
|
+
def named
|
|
111
|
+
@named ||= @entries.flat_map { |entry| entry.constants.keys }
|
|
112
|
+
.intersection(Connections::BaseConnectionHandler.registry.map(&:constants_key))
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
class ConfigurationValidator
|
|
117
|
+
def initialize(configuration)
|
|
118
|
+
@configuration = configuration
|
|
119
|
+
@errors = []
|
|
120
|
+
@warnings = []
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def validate!
|
|
124
|
+
entries.each do |entry|
|
|
125
|
+
entry.validate
|
|
126
|
+
errors.concat(entry.errors)
|
|
127
|
+
warnings.concat(entry.warnings)
|
|
128
|
+
end
|
|
129
|
+
check_across_tenants
|
|
130
|
+
raise ConfigurationError, errors.join("\n") if errors.any?
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
private
|
|
134
|
+
|
|
135
|
+
attr_reader :configuration, :errors, :warnings
|
|
136
|
+
|
|
137
|
+
def entries = @entries ||= configuration.tenants.map { |key, entry| TenantEntry.new(key, entry) }
|
|
138
|
+
|
|
139
|
+
def check_across_tenants
|
|
140
|
+
check_duplicate_identifiers
|
|
141
|
+
warnings.concat(BackendCoverage.new(entries).warnings)
|
|
142
|
+
check_context_writers
|
|
143
|
+
warnings.each { |message| Output.print_warning("ConsoleKit: #{message}") }
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def check_duplicate_identifiers
|
|
147
|
+
configuration.tenants.keys.group_by { |key| key.to_s.downcase }.each_value do |keys|
|
|
148
|
+
next if keys.size < 2
|
|
149
|
+
|
|
150
|
+
errors << "ConsoleKit: tenant identifiers #{keys.map(&:inspect).join(', ')} are duplicates once " \
|
|
151
|
+
'normalized (differ only by type or case). Tenant lookup uses an exact match, so only one ' \
|
|
152
|
+
'of these is ever reachable.'
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def check_context_writers
|
|
157
|
+
klass = resolve_context_class
|
|
158
|
+
missing = klass && configured_attributes.reject { |attr| writer?(klass, :"#{attr}=") }
|
|
159
|
+
return if missing.blank?
|
|
160
|
+
|
|
161
|
+
warnings << "context_class #{klass} has no writer for: #{missing.join(', ')}. ConsoleKit will silently " \
|
|
162
|
+
'never configure that backend during a tenant switch.'
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def configured_attributes
|
|
166
|
+
named = entries.filter_map { |entry| entry.constants&.keys }.flatten.uniq
|
|
167
|
+
TenantConfigurator.context_mapping.select { |_attr, field| named.include?(field) }.keys
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def writer?(klass, writer) = klass.respond_to?(writer) || klass.method_defined?(writer)
|
|
171
|
+
|
|
172
|
+
def resolve_context_class
|
|
173
|
+
configuration.context_class
|
|
174
|
+
rescue Error => e
|
|
175
|
+
errors << e.message
|
|
176
|
+
nil
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
end
|
|
@@ -1,50 +1,161 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require 'active_support/core_ext/class/subclasses'
|
|
4
|
-
require 'active_support/core_ext/string/inflections'
|
|
5
|
-
require 'active_support/core_ext/string/filters'
|
|
6
3
|
require_relative 'diagnostic_helpers'
|
|
4
|
+
require_relative '../errors'
|
|
5
|
+
require_relative '../output'
|
|
6
|
+
require_relative '../instrumentation'
|
|
7
|
+
require_relative '../diagnostics'
|
|
7
8
|
|
|
8
9
|
module ConsoleKit
|
|
9
10
|
module Connections
|
|
10
|
-
|
|
11
|
+
module HandlerRegistry
|
|
12
|
+
COLLISION_WARNING = 'ConsoleKit: %<previous>s and %<current>s both claim the backend key %<key>p. Only ' \
|
|
13
|
+
'%<current>s will be switched, verified and rolled back.'
|
|
14
|
+
DUPLICATE_ATTRIBUTE = 'ConsoleKit: %<current>s claims the context attribute %<attribute>p, which %<previous>s ' \
|
|
15
|
+
'already claims for the %<key>p backend. One attribute is one slot on the context ' \
|
|
16
|
+
'object, so each backend would write the other values there and the context would ' \
|
|
17
|
+
'disagree with the live connection. Give %<current>s a context attribute of its own.'
|
|
18
|
+
|
|
19
|
+
class << self
|
|
20
|
+
def all = entries.values.freeze
|
|
21
|
+
|
|
22
|
+
def add(handler_class)
|
|
23
|
+
key = handler_class.backend_key
|
|
24
|
+
reject_duplicate_attribute(handler_class, key)
|
|
25
|
+
drop_other_keys(handler_class, key)
|
|
26
|
+
report_collision(entries[key], handler_class)
|
|
27
|
+
entries[key] = handler_class
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def remove(handler_class)
|
|
31
|
+
key = handler_class.backend_key
|
|
32
|
+
entries.delete(key) if entries[key].equal?(handler_class)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def entries = @entries ||= {}
|
|
38
|
+
|
|
39
|
+
def drop_other_keys(handler_class, key)
|
|
40
|
+
entries.delete_if { |existing, klass| existing != key && klass.equal?(handler_class) }
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def reject_duplicate_attribute(handler_class, key)
|
|
44
|
+
attribute = handler_class.context_attribute
|
|
45
|
+
previous_key, previous_class =
|
|
46
|
+
entries.find { |other, klass| other != key && klass.context_attribute == attribute }
|
|
47
|
+
return if !previous_class || same_declaration?(previous_class, handler_class)
|
|
48
|
+
|
|
49
|
+
raise ConfigurationError, format(DUPLICATE_ATTRIBUTE, current: handler_class, previous: previous_class,
|
|
50
|
+
attribute: attribute, key: previous_key)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def same_declaration?(previous, current)
|
|
54
|
+
previous_name = previous.name
|
|
55
|
+
previous.equal?(current) || (previous_name && previous_name == current.name)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def report_collision(previous, current)
|
|
59
|
+
previous_name = previous&.name
|
|
60
|
+
current_name = current.name
|
|
61
|
+
return unless previous_name && current_name && previous_name != current_name
|
|
62
|
+
|
|
63
|
+
Instrumentation.increment('console_kit.handler_collision')
|
|
64
|
+
Output.print_warning(format(COLLISION_WARNING, previous: previous, current: current,
|
|
65
|
+
key: current.backend_key))
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
11
70
|
class BaseConnectionHandler
|
|
12
71
|
include DiagnosticHelpers
|
|
13
72
|
|
|
14
73
|
class << self
|
|
15
|
-
def
|
|
74
|
+
def backend_key = declaration[:backend_key] || superclass.try(:backend_key)
|
|
75
|
+
def display_name = declaration[:display_name] || superclass.try(:display_name)
|
|
76
|
+
def context_attribute = declaration[:context_attribute] || superclass.try(:context_attribute)
|
|
77
|
+
def constants_key = declaration[:constants_key] || superclass.try(:constants_key)
|
|
78
|
+
def detail_label = declaration[:detail_label] || superclass.try(:detail_label)
|
|
79
|
+
|
|
80
|
+
def backend(key, display_name:, context_attribute:, constants_key:, detail_label:)
|
|
81
|
+
@declaration = { backend_key: key, display_name: display_name, context_attribute: context_attribute,
|
|
82
|
+
constants_key: constants_key, detail_label: detail_label }
|
|
83
|
+
HandlerRegistry.add(self)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def declaration = @declaration ||= {}
|
|
87
|
+
|
|
88
|
+
def registry = HandlerRegistry.all
|
|
89
|
+
def unregister(handler_class) = HandlerRegistry.remove(handler_class)
|
|
90
|
+
|
|
91
|
+
def target_error(_value) = nil
|
|
92
|
+
|
|
93
|
+
def identifier_error(value)
|
|
94
|
+
return if value.nil? || ((value.is_a?(String) || value.is_a?(Symbol)) && value.to_s.strip.present?)
|
|
95
|
+
|
|
96
|
+
'expected a non-blank String or Symbol'
|
|
97
|
+
end
|
|
16
98
|
end
|
|
17
99
|
|
|
18
100
|
attr_reader :context
|
|
19
101
|
|
|
20
102
|
def initialize(context) = @context = context
|
|
21
|
-
|
|
103
|
+
|
|
104
|
+
def backend_key = self.class.backend_key
|
|
105
|
+
def display_name = self.class.display_name
|
|
106
|
+
|
|
107
|
+
def target
|
|
108
|
+
attr_name = self.class.context_attribute
|
|
109
|
+
attr_name && context_attribute(attr_name).presence
|
|
110
|
+
end
|
|
111
|
+
|
|
22
112
|
def available? = raise NotImplementedError, "#{self.class} must implement #available?"
|
|
23
|
-
def
|
|
113
|
+
def snapshot = raise NotImplementedError, "#{self.class} must implement #snapshot"
|
|
114
|
+
def connect!(_target) = raise NotImplementedError, "#{self.class} must implement #connect!"
|
|
115
|
+
def restore(_snapshot) = raise NotImplementedError, "#{self.class} must implement #restore"
|
|
24
116
|
|
|
25
|
-
def
|
|
26
|
-
handler_name = self.class.name.demodulize.delete_suffix('ConnectionHandler')
|
|
27
|
-
thread, result_wrapper = spawn_diagnostic_thread(handler_name)
|
|
117
|
+
def unavailable_reason = nil
|
|
28
118
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
119
|
+
def prepare(_target) = nil
|
|
120
|
+
|
|
121
|
+
def verify!(_target) = nil
|
|
122
|
+
|
|
123
|
+
def diagnostics(level: :basic)
|
|
124
|
+
return unavailable_diagnostics unless available?
|
|
125
|
+
|
|
126
|
+
diagnostics_at(level)
|
|
127
|
+
rescue StandardError => e
|
|
128
|
+
raise e if ConsoleKit.programming_error?(e)
|
|
129
|
+
|
|
130
|
+
error_diagnostics(display_name, e)
|
|
34
131
|
end
|
|
35
132
|
|
|
36
|
-
|
|
133
|
+
def diagnostic_identity = nil
|
|
37
134
|
|
|
38
|
-
def
|
|
39
|
-
|
|
40
|
-
thread = Thread.new { wrapper[:value] = run_diagnostics_safely(handler_name) { diagnostics } }
|
|
41
|
-
[thread, wrapper]
|
|
135
|
+
def safe_diagnostics(timeout: Diagnostics::DEFAULT_TIMEOUT, level: :basic)
|
|
136
|
+
Diagnostics::Runner.call(self, timeout: timeout, level: level)
|
|
42
137
|
end
|
|
43
138
|
|
|
44
|
-
def
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
139
|
+
def verification_error(expected, actual)
|
|
140
|
+
ConnectionVerificationError.new(
|
|
141
|
+
nil, backend: display_name, tenant: nil, expected: expected, actual: actual
|
|
142
|
+
)
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
private
|
|
146
|
+
|
|
147
|
+
def diagnostics_at(level) = level == :full ? full_diagnostics : basic_diagnostics
|
|
148
|
+
|
|
149
|
+
def basic_diagnostics = raise NotImplementedError, "#{self.class} must implement #basic_diagnostics"
|
|
150
|
+
def full_diagnostics = raise NotImplementedError, "#{self.class} must implement #full_diagnostics"
|
|
151
|
+
|
|
152
|
+
def validate_target!(target)
|
|
153
|
+
handler_class = self.class
|
|
154
|
+
reason = handler_class.target_error(target)
|
|
155
|
+
return unless reason
|
|
156
|
+
|
|
157
|
+
raise ConfigurationError,
|
|
158
|
+
"ConsoleKit: #{handler_class.constants_key} #{scrub(target.inspect)} is invalid: #{reason}."
|
|
48
159
|
end
|
|
49
160
|
|
|
50
161
|
def measure_latency
|
|
@@ -54,7 +165,10 @@ module ConsoleKit
|
|
|
54
165
|
end
|
|
55
166
|
|
|
56
167
|
def context_attribute(name) = @context.try(name)
|
|
57
|
-
|
|
168
|
+
|
|
169
|
+
def unavailable_diagnostics
|
|
170
|
+
{ name: display_name, status: :unavailable, latency_ms: nil, details: {} }
|
|
171
|
+
end
|
|
58
172
|
end
|
|
59
173
|
end
|
|
60
174
|
end
|
|
@@ -4,23 +4,57 @@ require_relative 'sql_connection_handler'
|
|
|
4
4
|
require_relative 'mongo_connection_handler'
|
|
5
5
|
require_relative 'redis_connection_handler'
|
|
6
6
|
require_relative 'elasticsearch_connection_handler'
|
|
7
|
+
require_relative '../output'
|
|
8
|
+
require_relative '../instrumentation'
|
|
7
9
|
|
|
8
10
|
module ConsoleKit
|
|
9
11
|
module Connections
|
|
10
|
-
# Manages available connection handlers
|
|
11
12
|
class ConnectionManager
|
|
13
|
+
DROPPED_WARNING = 'was skipped because %<reason>s. That backend will NOT be switched, verified or rolled ' \
|
|
14
|
+
'back; it is recorded on the tenant state so the switch cannot report itself as fully ' \
|
|
15
|
+
'verified.'
|
|
16
|
+
HALF_IMPLEMENTED = 'it is only half-implemented (%<message>s)'
|
|
17
|
+
REPORTED_KEY = :console_kit_dropped_reported
|
|
18
|
+
|
|
12
19
|
class << self
|
|
13
|
-
def available_handlers(context)
|
|
14
|
-
handler_classes.filter_map
|
|
15
|
-
handler = klass.new(context)
|
|
16
|
-
handler if handler.available?
|
|
17
|
-
rescue NotImplementedError
|
|
18
|
-
nil
|
|
19
|
-
end
|
|
20
|
+
def available_handlers(context, dropped = nil)
|
|
21
|
+
handler_classes.filter_map { |klass| resolve(klass, context, dropped) }
|
|
20
22
|
end
|
|
21
23
|
|
|
22
24
|
private
|
|
23
25
|
|
|
26
|
+
def resolve(klass, context, dropped)
|
|
27
|
+
handler = klass.new(context)
|
|
28
|
+
return handler if handler.available?
|
|
29
|
+
|
|
30
|
+
drop(klass, dropped, handler.try(:unavailable_reason))
|
|
31
|
+
rescue NotImplementedError => e
|
|
32
|
+
drop(klass, dropped, format(HALF_IMPLEMENTED, message: e.message))
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def drop(klass, dropped, reason)
|
|
36
|
+
return nil unless reason
|
|
37
|
+
|
|
38
|
+
report_dropped(klass, reason)
|
|
39
|
+
dropped << klass.backend_key if dropped
|
|
40
|
+
nil
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def report_dropped(klass, reason)
|
|
44
|
+
Instrumentation.increment('console_kit.handler_dropped')
|
|
45
|
+
return unless unreported?(klass.backend_key, reason)
|
|
46
|
+
|
|
47
|
+
Output.print_warning("#{klass} #{format(DROPPED_WARNING, reason: reason)}")
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def unreported?(key, reason)
|
|
51
|
+
reported = Thread.current[REPORTED_KEY] ||= {}
|
|
52
|
+
return false if reported[key] == reason
|
|
53
|
+
|
|
54
|
+
reported[key] = reason
|
|
55
|
+
true
|
|
56
|
+
end
|
|
57
|
+
|
|
24
58
|
def handler_classes = BaseConnectionHandler.registry
|
|
25
59
|
end
|
|
26
60
|
end
|
|
@@ -1,27 +1,19 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative 'table_renderer'
|
|
4
|
+
require_relative '../diagnostics'
|
|
4
5
|
|
|
5
6
|
module ConsoleKit
|
|
6
7
|
module Connections
|
|
7
|
-
# Displays connection diagnostics as a Unicode table
|
|
8
8
|
module Dashboard
|
|
9
9
|
class << self
|
|
10
|
-
def display
|
|
11
|
-
rows =
|
|
10
|
+
def display(level: :basic)
|
|
11
|
+
rows = ConsoleKit::Diagnostics.run(level: level)
|
|
12
12
|
return Output.print_warning('No connections available') if rows.empty?
|
|
13
13
|
|
|
14
14
|
Output.print_header('Connection Dashboard')
|
|
15
15
|
Output.print_raw(TableRenderer.render(rows))
|
|
16
16
|
end
|
|
17
|
-
|
|
18
|
-
private
|
|
19
|
-
|
|
20
|
-
def fetch_diagnostics
|
|
21
|
-
ctx = ConsoleKit.configuration.context_class
|
|
22
|
-
handlers = ConnectionManager.available_handlers(ctx)
|
|
23
|
-
handlers.map(&:safe_diagnostics)
|
|
24
|
-
end
|
|
25
17
|
end
|
|
26
18
|
end
|
|
27
19
|
end
|
|
@@ -1,21 +1,40 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'active_support/core_ext/string/filters'
|
|
4
|
+
|
|
3
5
|
module ConsoleKit
|
|
4
6
|
module Connections
|
|
5
|
-
# Shared helper methods for connection diagnostics
|
|
6
7
|
module DiagnosticHelpers
|
|
8
|
+
CREDENTIAL_PATTERN = begin
|
|
9
|
+
url = %r{\b[a-z][a-z0-9+.-]*://\S*}i
|
|
10
|
+
secret_key = /password|passwd|pwd|secret|token|api[_-]?key|access[_-]?key|auth(?:orization)?/i
|
|
11
|
+
auth_scheme = /(?:Bearer|Basic|Token|Digest|ApiKey)\s+/i
|
|
12
|
+
assignment = /["']?\b#{secret_key}\b["']?\s*(?:=>|=|:(?!:))\s*["']?#{auth_scheme}?\S+/
|
|
13
|
+
noise = /
|
|
14
|
+
authentication|auth|is|was|for|error|required|mismatch|incorrect|invalid|failed|missing|expired|
|
|
15
|
+
limit|type|count|name|header|value|must|cannot|not|and|or|in|of|to|exceeded|denied|unsupported|
|
|
16
|
+
supported|rejected|refused|revoked|length|format|scheme|provider|store|file|path|key
|
|
17
|
+
/xi
|
|
18
|
+
phrase = /\b#{secret_key}\s+(?!#{noise}\b)["']?\S+/i
|
|
19
|
+
principal = /\b(?:for user|username|user name)\b\s+["']?\S+/i
|
|
20
|
+
principal_at = /\buser\s+["']?\S+@\S+/i
|
|
21
|
+
Regexp.union(url, assignment, phrase, principal, principal_at).freeze
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
REDACTED = '[redacted]'
|
|
25
|
+
|
|
7
26
|
module_function
|
|
8
27
|
|
|
9
28
|
def clock_time
|
|
10
29
|
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
11
30
|
end
|
|
12
31
|
|
|
13
|
-
def
|
|
14
|
-
|
|
32
|
+
def scrub(message)
|
|
33
|
+
message.to_s.scrub.gsub(CREDENTIAL_PATTERN, REDACTED)
|
|
15
34
|
end
|
|
16
35
|
|
|
17
|
-
def
|
|
18
|
-
{ name: name, status: :
|
|
36
|
+
def error_diagnostics(name, error)
|
|
37
|
+
{ name: name, status: :error, latency_ms: nil, details: { error: scrub(error.message).truncate(60) } }
|
|
19
38
|
end
|
|
20
39
|
end
|
|
21
40
|
end
|