boring_services 0.8.0 → 0.9.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/CHANGELOG.md +68 -0
- data/README.md +9 -3
- data/SECURITY.md +22 -0
- data/lib/boring_services/cli.rb +7 -4
- data/lib/boring_services/configuration.rb +59 -0
- data/lib/boring_services/installer.rb +5 -5
- data/lib/boring_services/secrets.rb +21 -12
- data/lib/boring_services/service_locator.rb +1 -1
- data/lib/boring_services/services/haproxy.rb +3 -4
- data/lib/boring_services/services/redis.rb +2 -4
- data/lib/boring_services/ssh_executor.rb +39 -6
- data/lib/boring_services/version.rb +1 -1
- data/lib/boring_services.rb +1 -1
- metadata +16 -56
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e67745f3e72ccf6dce8646fc1606dda6f080f7634f58e5e614e21d732a24baef
|
|
4
|
+
data.tar.gz: 6d16d20e74f47f17c855d217ceb59799a4c67e390f695050dd73ba09f949befd
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1201bdcc2598275c86c3ceceae12193c398c4135e6e8cbe675b6f5dce5fa2896b1b27fd776ecfc5fd34c2af8456a092ca968734e52d6ceac58c46c7f055c454d
|
|
7
|
+
data.tar.gz: 9bc9c48947ffe1805a73accab2fa699c8c007d744722d785fe51b5cd555595bd14eb4ee33c0deb0ad6ce982c6c7d6b5e8d18c9f61127a7f45d5c2dbb4cb927bf
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [0.9.0] - 2026-10-02
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- Update development dependencies.
|
|
15
|
+
- Link gem source metadata to the matching release tag.
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
|
|
19
|
+
- Per-host jump routing, strict known-hosts configuration, and a targeted
|
|
20
|
+
`install --host LABEL` path for safely adding services to a new origin.
|
|
21
|
+
|
|
22
|
+
## [0.8.1] - 2026-08-20
|
|
23
|
+
|
|
24
|
+
### Changed
|
|
25
|
+
|
|
26
|
+
- Require Ruby 4.0 or newer.
|
|
27
|
+
- Align runtime and development dependencies with the maintained toolchain.
|
|
28
|
+
- Add blocking test, lint, dependency-audit, and package-build CI checks.
|
|
29
|
+
- Add automated Bundler and GitHub Actions dependency updates.
|
|
30
|
+
- Clarify that private IP metadata does not configure bind addresses or
|
|
31
|
+
firewalls.
|
|
32
|
+
|
|
33
|
+
## [0.1.0] - 2025-01-21
|
|
34
|
+
|
|
35
|
+
### Added
|
|
36
|
+
- Initial release of BoringServices
|
|
37
|
+
- Deploy infrastructure services (Memcached, Redis, HAProxy, Nginx) via SSH
|
|
38
|
+
- Rails integration with generators and rake tasks
|
|
39
|
+
- Standalone Ruby support (no Rails dependency required)
|
|
40
|
+
- Configuration override support for HAProxy via `custom_params`:
|
|
41
|
+
- `timeout_connect`, `timeout_client`, `timeout_server`
|
|
42
|
+
- `balance` algorithm (roundrobin, leastconn, etc.)
|
|
43
|
+
- `health_check_path` customization
|
|
44
|
+
- `ssl_ciphers` and `ssl_options` for SSL/TLS configuration
|
|
45
|
+
- Configuration override support for Memcached via `custom_params`:
|
|
46
|
+
- `listen_address`, `max_connections`
|
|
47
|
+
- `max_item_size`, `verbosity`
|
|
48
|
+
- Custom config template support:
|
|
49
|
+
- HAProxy: `custom_config_template` option to provide your own config file
|
|
50
|
+
- Memcached: `custom_config_template` option to provide your own config file
|
|
51
|
+
- VPN/private network support with `private_ip` field for tracking internal IPs
|
|
52
|
+
- Multi-host deployment support for services
|
|
53
|
+
- Environment-based configuration (development, staging, production)
|
|
54
|
+
- Secret management via environment variables or command execution
|
|
55
|
+
- SSL/TLS support for HAProxy with automatic certificate management
|
|
56
|
+
- Health checking and status monitoring
|
|
57
|
+
- SSH key-based authentication with configurable options
|
|
58
|
+
|
|
59
|
+
### Changed
|
|
60
|
+
- Made railties dependency optional for standalone Ruby usage
|
|
61
|
+
- Improved documentation with comprehensive examples
|
|
62
|
+
- Updated Ruby requirement to >= 3.0.0 (from >= 3.4.0)
|
|
63
|
+
|
|
64
|
+
### Technical Details
|
|
65
|
+
- Uses SSHKit for SSH operations
|
|
66
|
+
- Supports Ubuntu 20.04+ servers (Debian-based distributions)
|
|
67
|
+
- YAML-based configuration with ERB support
|
|
68
|
+
- Thor-based CLI interface
|
data/README.md
CHANGED
|
@@ -371,9 +371,15 @@ BoringServices supports deploying services over VPN or private networks (e.g., W
|
|
|
371
371
|
|
|
372
372
|
- **Public IPs** (`host`) for SSH deployment access
|
|
373
373
|
- **Private IPs** (`private_ip`) for service communication
|
|
374
|
-
-
|
|
374
|
+
- Some generated service configurations listen on all interfaces (`0.0.0.0`)
|
|
375
|
+
by default
|
|
375
376
|
- Accessible via private network IPs
|
|
376
|
-
-
|
|
377
|
+
- `private_ip` documents the address clients should use; it does not change the
|
|
378
|
+
service bind address or configure a firewall
|
|
379
|
+
|
|
380
|
+
Before deploying, restrict each service port to the intended private CIDRs with
|
|
381
|
+
the provider firewall and the host firewall, or set a supported service-specific
|
|
382
|
+
listen address. A public SSH address does not make a service endpoint private.
|
|
377
383
|
|
|
378
384
|
**Example with Private IPs:**
|
|
379
385
|
|
|
@@ -428,7 +434,7 @@ haproxy: healthy
|
|
|
428
434
|
|
|
429
435
|
## Requirements
|
|
430
436
|
|
|
431
|
-
- Ruby
|
|
437
|
+
- Ruby 4.0+
|
|
432
438
|
- Ubuntu 20.04+ servers (Debian-based distributions)
|
|
433
439
|
- SSH key-based authentication
|
|
434
440
|
- Optional: VPN solution (WireGuard, Tailscale, etc.) for private networking
|
data/SECURITY.md
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Security Policy
|
|
2
|
+
|
|
3
|
+
## Reporting a Vulnerability
|
|
4
|
+
|
|
5
|
+
Please do not open a public issue for a potential vulnerability. Email
|
|
6
|
+
[security@boringcache.com](mailto:security@boringcache.com) or use
|
|
7
|
+
[GitHub private vulnerability reporting](https://github.com/boringcache/services/security/advisories/new).
|
|
8
|
+
|
|
9
|
+
Include the affected version, service, expected impact, and enough reproduction
|
|
10
|
+
detail for us to investigate. Do not include live credentials, private keys,
|
|
11
|
+
certificates, customer data, or production configuration.
|
|
12
|
+
|
|
13
|
+
## Supported Versions
|
|
14
|
+
|
|
15
|
+
Only the latest published release is supported. Security fixes are normally
|
|
16
|
+
shipped in a new release rather than backported.
|
|
17
|
+
|
|
18
|
+
## Scope
|
|
19
|
+
|
|
20
|
+
Reports about SSH host verification, network exposure, secret handling,
|
|
21
|
+
generated service configuration, command execution, or release integrity are
|
|
22
|
+
welcome.
|
data/lib/boring_services/cli.rb
CHANGED
|
@@ -25,8 +25,10 @@ module BoringServices
|
|
|
25
25
|
end
|
|
26
26
|
|
|
27
27
|
desc 'install [SERVICE]', 'Install service(s) - all services or specific service'
|
|
28
|
+
option :host, type: :string, desc: 'Install only the host matching this label or address'
|
|
28
29
|
def install(service_name = nil)
|
|
29
30
|
config = Configuration.load(options[:config], options[:environment])
|
|
31
|
+
config.only_host!(options[:host]) if options[:host]
|
|
30
32
|
installer = Installer.new(config)
|
|
31
33
|
service_name ? installer.install_service(service_name) : installer.install_all
|
|
32
34
|
rescue Error => e
|
|
@@ -52,7 +54,8 @@ module BoringServices
|
|
|
52
54
|
exit 1
|
|
53
55
|
end
|
|
54
56
|
|
|
55
|
-
desc 'reconfigure [SERVICE]',
|
|
57
|
+
desc 'reconfigure [SERVICE]',
|
|
58
|
+
'Reconfigure service(s) - skips package installation, only updates config and restarts'
|
|
56
59
|
def reconfigure(service_name = nil)
|
|
57
60
|
config = Configuration.load(options[:config], options[:environment])
|
|
58
61
|
installer = Installer.new(config)
|
|
@@ -108,7 +111,7 @@ module BoringServices
|
|
|
108
111
|
end
|
|
109
112
|
|
|
110
113
|
def print_memcached_stats(stats)
|
|
111
|
-
bytes = format_bytes(stats['bytes']
|
|
114
|
+
bytes = format_bytes(stats['bytes'].to_i)
|
|
112
115
|
conns = stats['curr_connections'] || '0'
|
|
113
116
|
items = stats['curr_items'] || '0'
|
|
114
117
|
hit_rate = calculate_hit_rate(stats['get_hits'], stats['get_misses'])
|
|
@@ -116,8 +119,8 @@ module BoringServices
|
|
|
116
119
|
end
|
|
117
120
|
|
|
118
121
|
def calculate_hit_rate(hits, misses)
|
|
119
|
-
hits = hits
|
|
120
|
-
misses = misses
|
|
122
|
+
hits = hits.to_i
|
|
123
|
+
misses = misses.to_i
|
|
121
124
|
total = hits + misses
|
|
122
125
|
total.positive? ? ((hits.to_f / total) * 100).round(1) : 0
|
|
123
126
|
end
|
|
@@ -14,6 +14,7 @@ module BoringServices
|
|
|
14
14
|
@config_path = config_path
|
|
15
15
|
@environment = environment.to_s
|
|
16
16
|
@config = load_config
|
|
17
|
+
@all_hosts = host_entries
|
|
17
18
|
end
|
|
18
19
|
|
|
19
20
|
def service_config(service_name)
|
|
@@ -42,6 +43,54 @@ module BoringServices
|
|
|
42
43
|
@config['ssh_key'] || '~/.ssh/id_rsa'
|
|
43
44
|
end
|
|
44
45
|
|
|
46
|
+
def ssh_known_hosts_file
|
|
47
|
+
path = @config['ssh_known_hosts_file']
|
|
48
|
+
File.expand_path(path) if path
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def verify_host_key_mode
|
|
52
|
+
case @config.fetch('verify_host_key', true)
|
|
53
|
+
when true, 'always', :always then :always
|
|
54
|
+
when 'accept_new', :accept_new then :accept_new
|
|
55
|
+
when false, 'never', :never then :never
|
|
56
|
+
else :always
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def host_config(reference)
|
|
61
|
+
reference = reference.to_s
|
|
62
|
+
@all_hosts.find do |host|
|
|
63
|
+
next unless host.is_a?(Hash)
|
|
64
|
+
|
|
65
|
+
host['host'].to_s == reference ||
|
|
66
|
+
host['label'].to_s.casecmp?(reference) ||
|
|
67
|
+
host['name'].to_s.casecmp?(reference)
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def jump_host_config(host)
|
|
72
|
+
return unless host.is_a?(Hash)
|
|
73
|
+
|
|
74
|
+
reference = host['jump_host'] || host[:jump_host]
|
|
75
|
+
return unless reference
|
|
76
|
+
|
|
77
|
+
host_config(reference) || raise(Error, "Jump host not found: #{reference}")
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def only_host!(reference)
|
|
81
|
+
selected = host_config(reference)
|
|
82
|
+
raise Error, "Host not found: #{reference}" unless selected
|
|
83
|
+
|
|
84
|
+
@config['services'] = services.filter_map do |service|
|
|
85
|
+
hosts = Array(service['hosts'] || service['host']).compact
|
|
86
|
+
matches = hosts.select { |host| same_host?(host, selected) }
|
|
87
|
+
next if matches.empty?
|
|
88
|
+
|
|
89
|
+
service.merge('hosts' => matches).tap { |entry| entry.delete('host') }
|
|
90
|
+
end
|
|
91
|
+
self
|
|
92
|
+
end
|
|
93
|
+
|
|
45
94
|
def forward_agent
|
|
46
95
|
return @config['forward_agent'] unless @config['forward_agent'].nil?
|
|
47
96
|
|
|
@@ -64,6 +113,16 @@ module BoringServices
|
|
|
64
113
|
|
|
65
114
|
private
|
|
66
115
|
|
|
116
|
+
def host_entries
|
|
117
|
+
services.flat_map { |service| Array(service['hosts'] || service['host']).compact }
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def same_host?(candidate, selected)
|
|
121
|
+
return candidate.to_s == selected['host'].to_s unless candidate.is_a?(Hash)
|
|
122
|
+
|
|
123
|
+
candidate['host'].to_s == selected['host'].to_s
|
|
124
|
+
end
|
|
125
|
+
|
|
67
126
|
def load_config
|
|
68
127
|
raise Error, "Config file not found: #{@config_path}" unless File.exist?(@config_path)
|
|
69
128
|
|
|
@@ -115,9 +115,9 @@ module BoringServices
|
|
|
115
115
|
end
|
|
116
116
|
|
|
117
117
|
def print_credentials_summary(hints)
|
|
118
|
-
puts "\n
|
|
119
|
-
puts
|
|
120
|
-
puts
|
|
118
|
+
puts "\n#{'=' * 50}"
|
|
119
|
+
puts '📋 Add to Rails credentials:'
|
|
120
|
+
puts '=' * 50
|
|
121
121
|
|
|
122
122
|
# Group by service type
|
|
123
123
|
grouped = hints.group_by { |h| h[:type] }
|
|
@@ -129,14 +129,14 @@ module BoringServices
|
|
|
129
129
|
by_region = entries.group_by { |e| e[:region] }
|
|
130
130
|
by_region.each do |region, region_entries|
|
|
131
131
|
puts " #{region}:"
|
|
132
|
-
puts
|
|
132
|
+
puts ' servers:'
|
|
133
133
|
region_entries.each do |entry|
|
|
134
134
|
puts " - #{entry[:server]} # #{entry[:label]}"
|
|
135
135
|
end
|
|
136
136
|
end
|
|
137
137
|
end
|
|
138
138
|
|
|
139
|
-
puts "\n
|
|
139
|
+
puts "\n#{'=' * 50}"
|
|
140
140
|
end
|
|
141
141
|
end
|
|
142
142
|
end
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
require 'English'
|
|
2
|
+
require 'open3'
|
|
3
|
+
require 'shellwords'
|
|
4
|
+
|
|
2
5
|
module BoringServices
|
|
3
6
|
class Secrets
|
|
4
7
|
def self.resolve(value)
|
|
@@ -8,10 +11,10 @@ module BoringServices
|
|
|
8
11
|
|
|
9
12
|
if value_str.start_with?('credentials:')
|
|
10
13
|
resolve_rails_credentials(value_str)
|
|
11
|
-
elsif value_str.start_with?('$')
|
|
12
|
-
resolve_env_var(value_str)
|
|
13
14
|
elsif value_str.start_with?('$(') && value_str.end_with?(')')
|
|
14
15
|
resolve_command(value_str[2..-2])
|
|
16
|
+
elsif value_str.start_with?('$')
|
|
17
|
+
resolve_env_var(value_str)
|
|
15
18
|
else
|
|
16
19
|
value_str
|
|
17
20
|
end
|
|
@@ -24,16 +27,19 @@ module BoringServices
|
|
|
24
27
|
|
|
25
28
|
# Use rails credentials:show to fetch the value
|
|
26
29
|
# This requires being run from the Rails root directory
|
|
27
|
-
|
|
28
|
-
result =
|
|
30
|
+
runner = "puts Rails.application.credentials.dig(#{keys.map { |key| key.to_sym.inspect }.join(', ')})"
|
|
31
|
+
result, stderr, status = Open3.capture3(
|
|
32
|
+
{ 'RAILS_ENV' => ENV.fetch('RAILS_ENV', 'production') },
|
|
33
|
+
'bundle', 'exec', 'rails', 'runner', runner
|
|
34
|
+
)
|
|
29
35
|
|
|
30
|
-
unless
|
|
31
|
-
|
|
36
|
+
unless status.success?
|
|
37
|
+
detail = stderr.strip.empty? ? 'Make sure you are running from the Rails root directory.' : stderr.strip
|
|
38
|
+
raise Error, "Failed to resolve Rails credentials: #{key_path}. #{detail}"
|
|
32
39
|
end
|
|
33
40
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
end
|
|
41
|
+
result = result.strip
|
|
42
|
+
raise Error, "Rails credentials key not found: #{key_path}" if result.empty?
|
|
37
43
|
|
|
38
44
|
result
|
|
39
45
|
end
|
|
@@ -46,10 +52,13 @@ module BoringServices
|
|
|
46
52
|
end
|
|
47
53
|
|
|
48
54
|
def self.resolve_command(command)
|
|
49
|
-
|
|
50
|
-
raise Error,
|
|
55
|
+
argv = Shellwords.split(command)
|
|
56
|
+
raise Error, 'Command secret is empty' if argv.empty?
|
|
51
57
|
|
|
52
|
-
result
|
|
58
|
+
result, stderr, status = Open3.capture3(*argv)
|
|
59
|
+
raise Error, "Command failed: #{argv.first}: #{stderr.strip}" unless status.success?
|
|
60
|
+
|
|
61
|
+
result.strip
|
|
53
62
|
end
|
|
54
63
|
end
|
|
55
64
|
end
|
|
@@ -9,7 +9,7 @@ module BoringServices
|
|
|
9
9
|
configure_haproxy
|
|
10
10
|
validate_config
|
|
11
11
|
ssh_executor.systemd_enable('haproxy')
|
|
12
|
-
ssh_executor.systemd_restart('haproxy')
|
|
12
|
+
ssh_executor.systemd_restart('haproxy') # Use restart instead of start to reload config
|
|
13
13
|
verify_listening_ports
|
|
14
14
|
end
|
|
15
15
|
end
|
|
@@ -124,7 +124,7 @@ module BoringServices
|
|
|
124
124
|
end
|
|
125
125
|
end
|
|
126
126
|
|
|
127
|
-
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
127
|
+
# rubocop:disable-next Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
128
128
|
def configure_haproxy
|
|
129
129
|
# Check if custom config template is provided
|
|
130
130
|
if service_config['custom_config_template'] && File.exist?(service_config['custom_config_template'])
|
|
@@ -239,7 +239,6 @@ module BoringServices
|
|
|
239
239
|
execute :sudo, :chown, 'root:root', '/etc/haproxy/haproxy.cfg'
|
|
240
240
|
execute :sudo, :chmod, '644', '/etc/haproxy/haproxy.cfg'
|
|
241
241
|
end
|
|
242
|
-
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
243
242
|
|
|
244
243
|
def verify_listening_ports
|
|
245
244
|
puts ' Verifying HAProxy is listening on ports...'
|
|
@@ -257,7 +256,7 @@ module BoringServices
|
|
|
257
256
|
ports_to_check << stats_port
|
|
258
257
|
|
|
259
258
|
ports_to_check.each do |check_port|
|
|
260
|
-
result = execute :sudo, :ss, '-tlnp', '|', :grep,
|
|
259
|
+
result = execute :sudo, :ss, '-tlnp', '|', :grep, '-E', "':#{check_port} '", raise_on_error: false
|
|
261
260
|
if result
|
|
262
261
|
puts " ✓ Port #{check_port} is listening"
|
|
263
262
|
else
|
|
@@ -42,7 +42,7 @@ module BoringServices
|
|
|
42
42
|
listen_port = port || 6379
|
|
43
43
|
password = resolve_secret('redis_password') if config.secrets['redis_password']
|
|
44
44
|
has_private_ip = !private_ip.to_s.strip.empty?
|
|
45
|
-
bind_address = has_private_ip ? "127.0.0.1 #{private_ip}" :
|
|
45
|
+
bind_address = has_private_ip ? "127.0.0.1 #{private_ip}" : '0.0.0.0'
|
|
46
46
|
|
|
47
47
|
config_content = <<~REDIS
|
|
48
48
|
bind #{bind_address}
|
|
@@ -62,9 +62,7 @@ module BoringServices
|
|
|
62
62
|
execute :sudo, :chmod, '640', '/etc/redis/redis.conf'
|
|
63
63
|
|
|
64
64
|
# If using private IP (WireGuard), ensure Redis starts after WireGuard
|
|
65
|
-
if has_private_ip
|
|
66
|
-
configure_wireguard_dependency
|
|
67
|
-
end
|
|
65
|
+
configure_wireguard_dependency if has_private_ip
|
|
68
66
|
|
|
69
67
|
# Set vm.overcommit_memory for Redis background saves
|
|
70
68
|
execute :sudo, :sysctl, '-w', 'vm.overcommit_memory=1'
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
require 'sshkit'
|
|
4
4
|
require 'sshkit/dsl'
|
|
5
|
+
require 'net/ssh/proxy/command'
|
|
6
|
+
require 'shellwords'
|
|
5
7
|
|
|
6
8
|
module BoringServices
|
|
7
9
|
class SSHExecutor
|
|
@@ -152,10 +154,10 @@ module BoringServices
|
|
|
152
154
|
end
|
|
153
155
|
end
|
|
154
156
|
|
|
155
|
-
def capture_on_host(host, *
|
|
157
|
+
def capture_on_host(host, *)
|
|
156
158
|
output = nil
|
|
157
159
|
execute_on_host(host) do
|
|
158
|
-
output = capture(
|
|
160
|
+
output = capture(*, raise_on_non_zero_exit: false)
|
|
159
161
|
end
|
|
160
162
|
output
|
|
161
163
|
end
|
|
@@ -174,8 +176,10 @@ module BoringServices
|
|
|
174
176
|
forward_agent: config.forward_agent,
|
|
175
177
|
auth_methods: config.ssh_auth_methods,
|
|
176
178
|
keys_only: !config.use_ssh_agent,
|
|
177
|
-
use_agent: config.use_ssh_agent
|
|
179
|
+
use_agent: config.use_ssh_agent,
|
|
180
|
+
verify_host_key: config.verify_host_key_mode
|
|
178
181
|
}
|
|
182
|
+
ssh.ssh_options[:user_known_hosts_file] = [config.ssh_known_hosts_file] if config.ssh_known_hosts_file
|
|
179
183
|
end
|
|
180
184
|
end
|
|
181
185
|
|
|
@@ -185,19 +189,48 @@ module BoringServices
|
|
|
185
189
|
target_host = host['host'] || host[:host]
|
|
186
190
|
raise Error, 'Host entry missing host field' unless target_host
|
|
187
191
|
|
|
188
|
-
build_sshkit_host(target_host, host['user'] || host[:user])
|
|
192
|
+
build_sshkit_host(target_host, host['user'] || host[:user], host)
|
|
189
193
|
else
|
|
190
194
|
host_string = host.to_s
|
|
191
195
|
return SSHKit::Host.new(host_string) if host_string.include?('@')
|
|
192
196
|
|
|
193
|
-
build_sshkit_host(host_string, nil)
|
|
197
|
+
build_sshkit_host(host_string, nil, nil)
|
|
194
198
|
end
|
|
195
199
|
end
|
|
196
200
|
|
|
197
|
-
def build_sshkit_host(hostname, user)
|
|
201
|
+
def build_sshkit_host(hostname, user, host_config)
|
|
198
202
|
sshkit_host = SSHKit::Host.new(hostname)
|
|
199
203
|
sshkit_host.user = user || config.user
|
|
204
|
+
options = { verify_host_key: config.verify_host_key_mode }
|
|
205
|
+
options[:user_known_hosts_file] = [config.ssh_known_hosts_file] if config.ssh_known_hosts_file
|
|
206
|
+
if (jump_host = config.jump_host_config(host_config))
|
|
207
|
+
options[:proxy] = jump_proxy(jump_host)
|
|
208
|
+
end
|
|
209
|
+
sshkit_host.ssh_options = options
|
|
200
210
|
sshkit_host
|
|
201
211
|
end
|
|
212
|
+
|
|
213
|
+
def jump_proxy(jump_host)
|
|
214
|
+
user = jump_host['user'] || config.user
|
|
215
|
+
command = [
|
|
216
|
+
'ssh', '-F', '/dev/null',
|
|
217
|
+
'-o', 'BatchMode=yes',
|
|
218
|
+
'-o', 'ForwardAgent=no',
|
|
219
|
+
'-o', "StrictHostKeyChecking=#{open_ssh_host_key_mode}"
|
|
220
|
+
]
|
|
221
|
+
command.push('-o', "UserKnownHostsFile=#{config.ssh_known_hosts_file}") if config.ssh_known_hosts_file
|
|
222
|
+
command.push('-i', File.expand_path(config.ssh_key), '-o', 'IdentitiesOnly=yes')
|
|
223
|
+
command.push('-W', '%h:%p', "#{user}@#{jump_host.fetch('host')}")
|
|
224
|
+
command_line = Shellwords.join(command).gsub('\\%h', '%h').gsub('\\%p', '%p')
|
|
225
|
+
Net::SSH::Proxy::Command.new(command_line)
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def open_ssh_host_key_mode
|
|
229
|
+
case config.verify_host_key_mode
|
|
230
|
+
when :accept_new then 'accept-new'
|
|
231
|
+
when :never then 'no'
|
|
232
|
+
else 'yes'
|
|
233
|
+
end
|
|
234
|
+
end
|
|
202
235
|
end
|
|
203
236
|
end
|
data/lib/boring_services.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: boring_services
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.9.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- BoringCache
|
|
@@ -29,84 +29,42 @@ dependencies:
|
|
|
29
29
|
requirements:
|
|
30
30
|
- - "~>"
|
|
31
31
|
- !ruby/object:Gem::Version
|
|
32
|
-
version: '1.
|
|
32
|
+
version: '1.4'
|
|
33
33
|
type: :runtime
|
|
34
34
|
prerelease: false
|
|
35
35
|
version_requirements: !ruby/object:Gem::Requirement
|
|
36
36
|
requirements:
|
|
37
37
|
- - "~>"
|
|
38
38
|
- !ruby/object:Gem::Version
|
|
39
|
-
version: '1.
|
|
39
|
+
version: '1.4'
|
|
40
40
|
- !ruby/object:Gem::Dependency
|
|
41
41
|
name: sshkit
|
|
42
42
|
requirement: !ruby/object:Gem::Requirement
|
|
43
43
|
requirements:
|
|
44
44
|
- - "~>"
|
|
45
45
|
- !ruby/object:Gem::Version
|
|
46
|
-
version: '1.
|
|
46
|
+
version: '1.25'
|
|
47
47
|
type: :runtime
|
|
48
48
|
prerelease: false
|
|
49
49
|
version_requirements: !ruby/object:Gem::Requirement
|
|
50
50
|
requirements:
|
|
51
51
|
- - "~>"
|
|
52
52
|
- !ruby/object:Gem::Version
|
|
53
|
-
version: '1.
|
|
53
|
+
version: '1.25'
|
|
54
54
|
- !ruby/object:Gem::Dependency
|
|
55
55
|
name: thor
|
|
56
56
|
requirement: !ruby/object:Gem::Requirement
|
|
57
57
|
requirements:
|
|
58
58
|
- - "~>"
|
|
59
59
|
- !ruby/object:Gem::Version
|
|
60
|
-
version: '1.
|
|
60
|
+
version: '1.5'
|
|
61
61
|
type: :runtime
|
|
62
62
|
prerelease: false
|
|
63
63
|
version_requirements: !ruby/object:Gem::Requirement
|
|
64
64
|
requirements:
|
|
65
65
|
- - "~>"
|
|
66
66
|
- !ruby/object:Gem::Version
|
|
67
|
-
version: '1.
|
|
68
|
-
- !ruby/object:Gem::Dependency
|
|
69
|
-
name: minitest
|
|
70
|
-
requirement: !ruby/object:Gem::Requirement
|
|
71
|
-
requirements:
|
|
72
|
-
- - "~>"
|
|
73
|
-
- !ruby/object:Gem::Version
|
|
74
|
-
version: '5.0'
|
|
75
|
-
type: :development
|
|
76
|
-
prerelease: false
|
|
77
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
78
|
-
requirements:
|
|
79
|
-
- - "~>"
|
|
80
|
-
- !ruby/object:Gem::Version
|
|
81
|
-
version: '5.0'
|
|
82
|
-
- !ruby/object:Gem::Dependency
|
|
83
|
-
name: rubocop
|
|
84
|
-
requirement: !ruby/object:Gem::Requirement
|
|
85
|
-
requirements:
|
|
86
|
-
- - "~>"
|
|
87
|
-
- !ruby/object:Gem::Version
|
|
88
|
-
version: '1.50'
|
|
89
|
-
type: :development
|
|
90
|
-
prerelease: false
|
|
91
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
92
|
-
requirements:
|
|
93
|
-
- - "~>"
|
|
94
|
-
- !ruby/object:Gem::Version
|
|
95
|
-
version: '1.50'
|
|
96
|
-
- !ruby/object:Gem::Dependency
|
|
97
|
-
name: simplecov
|
|
98
|
-
requirement: !ruby/object:Gem::Requirement
|
|
99
|
-
requirements:
|
|
100
|
-
- - "~>"
|
|
101
|
-
- !ruby/object:Gem::Version
|
|
102
|
-
version: '0.22'
|
|
103
|
-
type: :development
|
|
104
|
-
prerelease: false
|
|
105
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
106
|
-
requirements:
|
|
107
|
-
- - "~>"
|
|
108
|
-
- !ruby/object:Gem::Version
|
|
109
|
-
version: '0.22'
|
|
67
|
+
version: '1.5'
|
|
110
68
|
description: Simple deployment and management of infrastructure services like Memcached,
|
|
111
69
|
Redis, HAProxy, and Nginx. Works standalone or with Rails.
|
|
112
70
|
email:
|
|
@@ -116,8 +74,10 @@ executables:
|
|
|
116
74
|
extensions: []
|
|
117
75
|
extra_rdoc_files: []
|
|
118
76
|
files:
|
|
77
|
+
- CHANGELOG.md
|
|
119
78
|
- LICENSE
|
|
120
79
|
- README.md
|
|
80
|
+
- SECURITY.md
|
|
121
81
|
- exe/boringservices
|
|
122
82
|
- lib/boring_services.rb
|
|
123
83
|
- lib/boring_services/cli.rb
|
|
@@ -136,14 +96,14 @@ files:
|
|
|
136
96
|
- lib/boring_services/ssh_executor.rb
|
|
137
97
|
- lib/boring_services/version.rb
|
|
138
98
|
- lib/tasks/services.rake
|
|
139
|
-
homepage: https://github.com/boringcache/
|
|
99
|
+
homepage: https://github.com/boringcache/services
|
|
140
100
|
licenses:
|
|
141
101
|
- MIT
|
|
142
102
|
metadata:
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
103
|
+
source_code_uri: https://github.com/boringcache/services/tree/v0.9.0
|
|
104
|
+
documentation_uri: https://github.com/boringcache/services/blob/main/README.md
|
|
105
|
+
changelog_uri: https://github.com/boringcache/services/blob/main/CHANGELOG.md
|
|
106
|
+
bug_tracker_uri: https://github.com/boringcache/services/issues
|
|
147
107
|
rubygems_mfa_required: 'true'
|
|
148
108
|
rdoc_options: []
|
|
149
109
|
require_paths:
|
|
@@ -152,14 +112,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
152
112
|
requirements:
|
|
153
113
|
- - ">="
|
|
154
114
|
- !ruby/object:Gem::Version
|
|
155
|
-
version:
|
|
115
|
+
version: 4.0.0
|
|
156
116
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
157
117
|
requirements:
|
|
158
118
|
- - ">="
|
|
159
119
|
- !ruby/object:Gem::Version
|
|
160
120
|
version: '0'
|
|
161
121
|
requirements: []
|
|
162
|
-
rubygems_version:
|
|
122
|
+
rubygems_version: 4.0.16
|
|
163
123
|
specification_version: 4
|
|
164
124
|
summary: Deploy infrastructure services for Ruby & Rails apps
|
|
165
125
|
test_files: []
|