monq 0.1.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 +7 -0
- data/CODE_OF_CONDUCT.md +10 -0
- data/LICENSE.txt +21 -0
- data/README.md +108 -0
- data/Rakefile +8 -0
- data/docs/superpowers/plans/2026-10-24-monq.md +1384 -0
- data/docs/superpowers/specs/2026-10-24-monq-design.md +401 -0
- data/exe/monq +6 -0
- data/lib/monq/app.rb +86 -0
- data/lib/monq/cli.rb +141 -0
- data/lib/monq/commands/info.rb +45 -0
- data/lib/monq/commands/models.rb +95 -0
- data/lib/monq/commands/reconnect.rb +45 -0
- data/lib/monq/commands/redefine.rb +25 -0
- data/lib/monq/commands/sandbox.rb +57 -0
- data/lib/monq/commands/show_sql.rb +92 -0
- data/lib/monq/commands.rb +25 -0
- data/lib/monq/config.rb +216 -0
- data/lib/monq/environment.rb +218 -0
- data/lib/monq/errors.rb +5 -0
- data/lib/monq/ext/array.rb +57 -0
- data/lib/monq/ext/criteria.rb +31 -0
- data/lib/monq/ext/document.rb +29 -0
- data/lib/monq/ext/kernel.rb +70 -0
- data/lib/monq/ext/object.rb +14 -0
- data/lib/monq/ext/view.rb +40 -0
- data/lib/monq/ext.rb +42 -0
- data/lib/monq/multi_io.rb +49 -0
- data/lib/monq/presenter.rb +54 -0
- data/lib/monq/repl.rb +45 -0
- data/lib/monq/ssh_proxy.rb +43 -0
- data/lib/monq/table.rb +50 -0
- data/lib/monq/version.rb +5 -0
- data/lib/monq.rb +23 -0
- data/sig/monq.rbs +4 -0
- metadata +193 -0
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "monq/table"
|
|
4
|
+
|
|
5
|
+
module Monq
|
|
6
|
+
module Commands
|
|
7
|
+
module Models
|
|
8
|
+
HEADERS = ["Table Name", "Model Class", "Abbr"].freeze
|
|
9
|
+
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
def render(envs, env_regexp = nil, collection_regexp = nil, format: "terminal")
|
|
13
|
+
envs.filter_map { |env| render_env(env, collection_regexp, format) if match_env?(env, env_regexp) }.join("\n")
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def render_env(env, collection_regexp, format)
|
|
17
|
+
Table.new("Environment: #{env.name}") do |table|
|
|
18
|
+
table.headers = HEADERS
|
|
19
|
+
Array(env.models).each do |klass|
|
|
20
|
+
collection = collection_name(klass)
|
|
21
|
+
next unless match_collection?(klass, collection, collection_regexp)
|
|
22
|
+
|
|
23
|
+
table.body << [collection, klass.name, abbreviation(env, klass)]
|
|
24
|
+
end
|
|
25
|
+
end.to_terminal(format)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def match_collection?(klass, collection, collection_regexp)
|
|
29
|
+
regexp = coerce_regexp(collection_regexp)
|
|
30
|
+
return true unless regexp
|
|
31
|
+
|
|
32
|
+
collection.to_s =~ regexp || klass.name.to_s =~ regexp
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def coerce_regexp(value)
|
|
36
|
+
return nil if value.nil? || value.to_s.empty?
|
|
37
|
+
return value if value.is_a?(Regexp)
|
|
38
|
+
|
|
39
|
+
source = value.to_s
|
|
40
|
+
if source.start_with?("/") && source.end_with?("/") && source.length > 1
|
|
41
|
+
Regexp.new(source[1..-2])
|
|
42
|
+
else
|
|
43
|
+
Regexp.new(source, Regexp::IGNORECASE)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def match_env?(env, env_regexp)
|
|
48
|
+
return true if env_regexp.nil? || env_regexp.to_s.empty?
|
|
49
|
+
|
|
50
|
+
env.name =~ coerce_regexp(env_regexp)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def collection_name(klass)
|
|
54
|
+
options = klass.storage_options
|
|
55
|
+
options[:collection] || options["collection"]
|
|
56
|
+
rescue StandardError
|
|
57
|
+
nil
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def abbreviation(env, klass)
|
|
61
|
+
mod = env.namespace_module
|
|
62
|
+
mod.constants.each do |name|
|
|
63
|
+
next if name.to_s == klass.name.to_s.split("::").last
|
|
64
|
+
|
|
65
|
+
return name.to_s if mod.const_get(name) == klass
|
|
66
|
+
end
|
|
67
|
+
""
|
|
68
|
+
rescue StandardError
|
|
69
|
+
""
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def register_pry_command!
|
|
73
|
+
return if Pry.commands["m"]
|
|
74
|
+
|
|
75
|
+
Pry.commands.create_command("m", "List loaded models: m [-e env] [-f format] [collection_regexp]") do
|
|
76
|
+
def options(opt)
|
|
77
|
+
opt.on "-e", "--env", "Environment name regexp", argument: true, as: String
|
|
78
|
+
opt.on "-f", "--format", "Table format: terminal, md, org", argument: true, as: String, default: "terminal"
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def process
|
|
82
|
+
output.puts Monq::Commands::Models.render(
|
|
83
|
+
Monq::App.instance.environments,
|
|
84
|
+
opts[:env],
|
|
85
|
+
args.first,
|
|
86
|
+
format: opts[:format] || "terminal"
|
|
87
|
+
)
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
Pry.commands.alias_command "l", "m"
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Monq
|
|
4
|
+
module Commands
|
|
5
|
+
module Reconnect
|
|
6
|
+
module_function
|
|
7
|
+
|
|
8
|
+
def run(envs, force: false)
|
|
9
|
+
raise Error, "sandbox is active; run sandbox-quit first" if envs.any?(&:sandbox?)
|
|
10
|
+
|
|
11
|
+
envs.each do |env|
|
|
12
|
+
reconnect_ssh(env, force: force)
|
|
13
|
+
if force
|
|
14
|
+
env.reconnect!
|
|
15
|
+
else
|
|
16
|
+
env.reconnect! unless env.ping?
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def reconnect_ssh(env, force:)
|
|
22
|
+
proxy = env.ssh_proxy
|
|
23
|
+
return unless proxy
|
|
24
|
+
|
|
25
|
+
if force
|
|
26
|
+
proxy.reconnect! if proxy.respond_to?(:reconnect!)
|
|
27
|
+
elsif proxy.respond_to?(:reconnect)
|
|
28
|
+
proxy.reconnect
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def register_pry_command!
|
|
33
|
+
return if Pry.commands["reconnect"]
|
|
34
|
+
|
|
35
|
+
Pry.commands.block_command("reconnect", "Reconnect dead clients") do
|
|
36
|
+
Monq::Commands::Reconnect.run(Monq::App.instance.environments, force: false)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
Pry.commands.block_command("reconnect!", "Force reconnect clients") do
|
|
40
|
+
Monq::Commands::Reconnect.run(Monq::App.instance.environments, force: true)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Monq
|
|
4
|
+
module Commands
|
|
5
|
+
module Redefine
|
|
6
|
+
module_function
|
|
7
|
+
|
|
8
|
+
def run(envs)
|
|
9
|
+
raise Error, "sandbox is active; run sandbox-quit first" if envs.any?(&:sandbox?)
|
|
10
|
+
|
|
11
|
+
envs.each(&:redefine!)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def register_pry_command!
|
|
15
|
+
return if Pry.commands["redefine"]
|
|
16
|
+
|
|
17
|
+
Pry.commands.block_command("redefine", "Reload model files") do
|
|
18
|
+
Monq::Commands::Redefine.run(Monq::App.instance.environments)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
Pry.commands.alias_command "redef", "redefine"
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Monq
|
|
4
|
+
module Commands
|
|
5
|
+
module Sandbox
|
|
6
|
+
module_function
|
|
7
|
+
|
|
8
|
+
def enter(envs)
|
|
9
|
+
return warn("already in sandbox") if envs.any?(&:sandbox?)
|
|
10
|
+
|
|
11
|
+
hellos = envs.map { |env| env.client.database.command(hello: 1).documents.first }
|
|
12
|
+
if hellos.any? { |doc| !transaction_capable?(doc) }
|
|
13
|
+
raise Error, "current deployment does not support transactions (need replica set or mongos); not entering sandbox"
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
started = []
|
|
17
|
+
envs.each do |env|
|
|
18
|
+
session = env.client.start_session
|
|
19
|
+
session.start_transaction
|
|
20
|
+
env.bind_session(session)
|
|
21
|
+
started << [env, session]
|
|
22
|
+
rescue StandardError
|
|
23
|
+
started.reverse_each do |e, s|
|
|
24
|
+
s.abort_transaction
|
|
25
|
+
s.end_session
|
|
26
|
+
e.clear_session
|
|
27
|
+
end
|
|
28
|
+
raise
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def quit(envs)
|
|
33
|
+
envs.each(&:clear_session)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def transaction_capable?(doc)
|
|
37
|
+
return false if doc.nil?
|
|
38
|
+
|
|
39
|
+
set_name = doc["setName"] || doc[:setName]
|
|
40
|
+
msg = doc["msg"] || doc[:msg]
|
|
41
|
+
!set_name.nil? || msg == "isdbgrid"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def register_pry_command!
|
|
45
|
+
return if Pry.commands["sandbox-enter"]
|
|
46
|
+
|
|
47
|
+
Pry.commands.block_command("sandbox-enter", "Start an all-or-nothing transaction on every environment") do
|
|
48
|
+
Monq::Commands::Sandbox.enter(Monq::App.instance.environments)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
Pry.commands.block_command("sandbox-quit", "Abort sandbox transactions and end their sessions") do
|
|
52
|
+
Monq::Commands::Sandbox.quit(Monq::App.instance.environments)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "logger"
|
|
4
|
+
require "json"
|
|
5
|
+
require "monq/multi_io"
|
|
6
|
+
|
|
7
|
+
module Monq
|
|
8
|
+
module Commands
|
|
9
|
+
module ShowSql
|
|
10
|
+
module Subscriber
|
|
11
|
+
module_function
|
|
12
|
+
|
|
13
|
+
def started(event)
|
|
14
|
+
write("started", event)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def succeeded(event)
|
|
18
|
+
write("succeeded", event)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def failed(event)
|
|
22
|
+
write("failed", event)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def write(phase, event)
|
|
26
|
+
io = App.log_io
|
|
27
|
+
return unless io
|
|
28
|
+
|
|
29
|
+
payload = {
|
|
30
|
+
phase: phase,
|
|
31
|
+
command_name: event.respond_to?(:command_name) ? event.command_name : nil,
|
|
32
|
+
database: event.respond_to?(:database_name) ? event.database_name : nil,
|
|
33
|
+
command: jsonable(event.respond_to?(:command) ? event.command : nil),
|
|
34
|
+
duration: event.respond_to?(:duration) ? event.duration : nil
|
|
35
|
+
}
|
|
36
|
+
io.write("#{JSON.generate(payload)}\n")
|
|
37
|
+
rescue StandardError
|
|
38
|
+
nil
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def jsonable(value)
|
|
42
|
+
JSON.parse(JSON.generate(value))
|
|
43
|
+
rescue StandardError
|
|
44
|
+
value.to_s
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
class << self
|
|
49
|
+
attr_accessor :subscribed
|
|
50
|
+
|
|
51
|
+
def enable
|
|
52
|
+
App.log_io ||= MultiIO.new
|
|
53
|
+
App.log_io << $stdout unless App.log_io.include?($stdout)
|
|
54
|
+
subscribe!
|
|
55
|
+
mirror_logger!
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def disable
|
|
59
|
+
return unless App.log_io.is_a?(MultiIO) && App.log_io.include?($stdout)
|
|
60
|
+
|
|
61
|
+
App.log_io.delete($stdout)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def subscribe!
|
|
65
|
+
return if subscribed
|
|
66
|
+
return unless defined?(Mongo::Monitoring::COMMAND) && Mongo::Monitoring::Global.respond_to?(:subscribe)
|
|
67
|
+
|
|
68
|
+
Mongo::Monitoring::Global.subscribe(Mongo::Monitoring::COMMAND, Subscriber)
|
|
69
|
+
self.subscribed = true
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def mirror_logger!
|
|
73
|
+
return unless defined?(Mongo::Logger)
|
|
74
|
+
|
|
75
|
+
Mongo::Logger.logger = Logger.new(App.log_io)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def register_pry_command!
|
|
79
|
+
return if Pry.commands["show-sql"]
|
|
80
|
+
|
|
81
|
+
Pry.commands.block_command("show-sql", "Print Mongo commands") do
|
|
82
|
+
Monq::Commands::ShowSql.enable
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
Pry.commands.block_command("hide-sql", "Stop printing Mongo commands") do
|
|
86
|
+
Monq::Commands::ShowSql.disable
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "commands/info"
|
|
4
|
+
require_relative "commands/models"
|
|
5
|
+
require_relative "commands/show_sql"
|
|
6
|
+
require_relative "commands/reconnect"
|
|
7
|
+
require_relative "commands/redefine"
|
|
8
|
+
require_relative "commands/sandbox"
|
|
9
|
+
|
|
10
|
+
module Monq
|
|
11
|
+
module Commands
|
|
12
|
+
module_function
|
|
13
|
+
|
|
14
|
+
def register_pry_commands!
|
|
15
|
+
return unless defined?(Pry)
|
|
16
|
+
|
|
17
|
+
Info.register_pry_command!
|
|
18
|
+
Models.register_pry_command!
|
|
19
|
+
ShowSql.register_pry_command!
|
|
20
|
+
Reconnect.register_pry_command!
|
|
21
|
+
Redefine.register_pry_command!
|
|
22
|
+
Sandbox.register_pry_command!
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
data/lib/monq/config.rb
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "yaml"
|
|
4
|
+
require "ostruct"
|
|
5
|
+
|
|
6
|
+
module Monq
|
|
7
|
+
module Config
|
|
8
|
+
RESERVED = %w[default arql].freeze
|
|
9
|
+
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
def load_file(path)
|
|
13
|
+
raw = YAML.safe_load(File.read(path), aliases: true)
|
|
14
|
+
(raw || {}).transform_keys(&:to_s)
|
|
15
|
+
rescue ArgumentError
|
|
16
|
+
raw = YAML.safe_load(File.read(path))
|
|
17
|
+
(raw || {}).transform_keys(&:to_s)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def select_environments(yaml, names, options)
|
|
21
|
+
yaml = stringify(yaml)
|
|
22
|
+
if names.nil? || names.empty?
|
|
23
|
+
return { "default" => cli_env(options) } if sufficient_cli?(options)
|
|
24
|
+
|
|
25
|
+
raise Error, "No -e given and CLI has no connection. Pass -e or --uri, or create ~/.monq.d/init.yml"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
names.each do |name|
|
|
29
|
+
raise Error, "#{name} is a reserved environment name" if RESERVED.include?(name)
|
|
30
|
+
raise Error, "Environment #{name} not in config. Known: #{known(yaml).join(', ')}" unless yaml.key?(name)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
selected = names.to_h { |n| [n, deep_sym(yaml[n])] }
|
|
34
|
+
apply_cli!(selected, options)
|
|
35
|
+
assign_namespaces!(selected)
|
|
36
|
+
assert_unique_namespaces!(selected)
|
|
37
|
+
selected
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def known(yaml)
|
|
41
|
+
yaml.keys - RESERVED
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def sufficient_cli?(options)
|
|
45
|
+
opt(options, :uri).to_s != "" || (opt(options, :host).to_s != "" && opt(options, :database).to_s != "")
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def cli_env(options)
|
|
49
|
+
{
|
|
50
|
+
uri: opt(options, :uri),
|
|
51
|
+
host: opt(options, :host),
|
|
52
|
+
port: opt(options, :port),
|
|
53
|
+
database: opt(options, :database),
|
|
54
|
+
username: opt(options, :username),
|
|
55
|
+
password: opt(options, :password),
|
|
56
|
+
auth_source: opt(options, :auth_source)
|
|
57
|
+
}.compact
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def apply_cli!(selected, options)
|
|
61
|
+
overrides = connection_overrides(options)
|
|
62
|
+
if selected.size > 1 && overrides.any?
|
|
63
|
+
raise Error, "Connection CLI flags are not allowed with multiple -e environments"
|
|
64
|
+
end
|
|
65
|
+
return if selected.size != 1
|
|
66
|
+
|
|
67
|
+
env = selected.values.first
|
|
68
|
+
overrides.each { |k, v| env[k] = v }
|
|
69
|
+
merge_ssh!(env, options)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def connection_overrides(options)
|
|
73
|
+
{
|
|
74
|
+
uri: opt(options, :uri),
|
|
75
|
+
host: opt(options, :host),
|
|
76
|
+
port: opt(options, :port),
|
|
77
|
+
database: opt(options, :database),
|
|
78
|
+
username: opt(options, :username),
|
|
79
|
+
password: opt(options, :password),
|
|
80
|
+
auth_source: opt(options, :auth_source)
|
|
81
|
+
}.compact.reject { |_, v| v.respond_to?(:empty?) && v.empty? }
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def merge_ssh!(env, options)
|
|
85
|
+
ssh = opt(options, :ssh)
|
|
86
|
+
return if ssh.nil? || ssh.empty?
|
|
87
|
+
|
|
88
|
+
env[:ssh] = (env[:ssh] || {}).merge(ssh.transform_keys(&:to_sym))
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def assign_namespaces!(selected)
|
|
92
|
+
selected.each do |name, env|
|
|
93
|
+
env[:namespace] ||= camelize(name)
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def assert_unique_namespaces!(selected)
|
|
98
|
+
groups = selected.group_by { |_, env| env[:namespace] }
|
|
99
|
+
dup = groups.find { |_, pairs| pairs.size > 1 }
|
|
100
|
+
return unless dup
|
|
101
|
+
|
|
102
|
+
raise Error, "Environments share namespace #{dup.first}: #{dup.last.map(&:first).join(', ')}"
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def camelize(name)
|
|
106
|
+
name.split(/[^A-Za-z0-9]+/).reject(&:empty?).map { |p| p[0].upcase + p[1..] }.join
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def stringify(hash)
|
|
110
|
+
hash.to_h.transform_keys(&:to_s).transform_values { |v| v.is_a?(Hash) ? stringify(v) : v }
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def deep_sym(obj)
|
|
114
|
+
case obj
|
|
115
|
+
when Hash then obj.to_h { |k, v| [k.to_sym, deep_sym(v)] }
|
|
116
|
+
else obj
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def prefix_for(env_name)
|
|
121
|
+
env_name.split(/[._-]/, 2).first
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def client_name_for(env_name)
|
|
125
|
+
env_name.gsub(/[^A-Za-z0-9]/, "_").to_sym
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def opt(options, key)
|
|
129
|
+
if options.respond_to?(key)
|
|
130
|
+
options.public_send(key)
|
|
131
|
+
elsif options.respond_to?(:[])
|
|
132
|
+
options[key] || options[key.to_s]
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def client_options(env)
|
|
137
|
+
env = deep_sym(env)
|
|
138
|
+
ssh = env[:ssh]
|
|
139
|
+
if ssh && env[:uri].to_s.start_with?("mongodb+srv:")
|
|
140
|
+
raise Error, "mongodb+srv with ssh is not supported; use a direct host"
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
built = env[:uri].to_s != "" ? from_uri(env) : from_fields(env)
|
|
144
|
+
return built unless ssh && ssh[:local_port]
|
|
145
|
+
|
|
146
|
+
{
|
|
147
|
+
hosts: ["127.0.0.1:#{ssh[:local_port]}"],
|
|
148
|
+
database: built[:database],
|
|
149
|
+
options: without_connect(without_replica_set(built[:options].merge(direct_connection: true)))
|
|
150
|
+
}
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def from_uri(env)
|
|
154
|
+
require "mongo"
|
|
155
|
+
parsed = Mongo::URI.get(env[:uri])
|
|
156
|
+
database = env[:database].to_s
|
|
157
|
+
database = parsed.client_options["database"].to_s if database.empty?
|
|
158
|
+
raise Error, "database is required (URI path is empty)" if database.empty?
|
|
159
|
+
|
|
160
|
+
options = symbolize_options(parsed.uri_options)
|
|
161
|
+
creds = parsed.credentials
|
|
162
|
+
options[:user] = creds[:user] if creds[:user]
|
|
163
|
+
options[:password] = creds[:password] if creds[:password]
|
|
164
|
+
options = without_replica_set(options) if env[:ssh] && env[:ssh][:local_port]
|
|
165
|
+
{
|
|
166
|
+
hosts: parsed.servers,
|
|
167
|
+
database: database,
|
|
168
|
+
options: options
|
|
169
|
+
}
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def from_fields(env)
|
|
173
|
+
raise Error, "host and database are required" if env[:host].to_s.empty? || env[:database].to_s.empty?
|
|
174
|
+
|
|
175
|
+
port = env[:port] || 27017
|
|
176
|
+
options = {}
|
|
177
|
+
options[:user] = env[:username] if env[:username]
|
|
178
|
+
options[:password] = env[:password] if env[:password]
|
|
179
|
+
options[:auth_source] = env[:auth_source] if env[:auth_source]
|
|
180
|
+
options[:replica_set] = env[:replica_set] if env[:replica_set] && !(env[:ssh] && env[:ssh][:local_port])
|
|
181
|
+
options[:ssl] = env[:ssl] unless env[:ssl].nil?
|
|
182
|
+
{ hosts: ["#{env[:host]}:#{port}"], database: env[:database], options: options }
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def mask_uri(uri)
|
|
186
|
+
uri.to_s.sub(%r{://([^:/@]+):[^@]+@}, '://\1:***@')
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def ssh_forward_target(env)
|
|
190
|
+
env = deep_sym(env)
|
|
191
|
+
if env[:uri].to_s != ""
|
|
192
|
+
require "mongo"
|
|
193
|
+
host, port = Mongo::URI.get(env[:uri]).servers.first.split(":")
|
|
194
|
+
{ host: host, port: (port || 27017).to_i }
|
|
195
|
+
else
|
|
196
|
+
{ host: env[:host], port: (env[:port] || 27017).to_i }
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def symbolize_options(options)
|
|
201
|
+
options.to_h { |key, value| [key.to_sym, value] }
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def without_replica_set(options)
|
|
205
|
+
options.reject { |key, _| replica_set_key?(key) }
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def without_connect(options)
|
|
209
|
+
options.except(:connect, "connect")
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def replica_set_key?(key)
|
|
213
|
+
%w[replica_set replicaset].include?(key.to_s.downcase.tr("-", "_"))
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
end
|