boring_services 0.8.1 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d26c6e6770c20727d68f773f48f1505b9eaeedef20b72d73cd9af76668773bc4
4
- data.tar.gz: b6d7d7ca8c099f958c51f542ff0f7eb64d6b23ae1241ca62e27041c044867f1b
3
+ metadata.gz: e67745f3e72ccf6dce8646fc1606dda6f080f7634f58e5e614e21d732a24baef
4
+ data.tar.gz: 6d16d20e74f47f17c855d217ceb59799a4c67e390f695050dd73ba09f949befd
5
5
  SHA512:
6
- metadata.gz: eab7cdbc819793e3f49ebb6a2212f37cd5d4c2019d8d96b15e70ca9431732a823ec51a689c450a9f8fd9b2fa8e3e7db849d716428044c988e697701bb36b45f6
7
- data.tar.gz: 23d2c7f46e5e6a14a1f6adf143771a1869e5dbb66401f7ac175ba70797b0c70ba0a4b114ac5cede972f2dd68c6ace88a46ecd2258033a1226533c0579b461519
6
+ metadata.gz: 1201bdcc2598275c86c3ceceae12193c398c4135e6e8cbe675b6f5dce5fa2896b1b27fd776ecfc5fd34c2af8456a092ca968734e52d6ceac58c46c7f055c454d
7
+ data.tar.gz: 9bc9c48947ffe1805a73accab2fa699c8c007d744722d785fe51b5cd555595bd14eb4ee33c0deb0ad6ce982c6c7d6b5e8d18c9f61127a7f45d5c2dbb4cb927bf
data/CHANGELOG.md CHANGED
@@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
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
+
10
22
  ## [0.8.1] - 2026-08-20
11
23
 
12
24
  ### 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
@@ -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
 
@@ -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...'
@@ -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
@@ -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
@@ -1,3 +1,3 @@
1
1
  module BoringServices
2
- VERSION = '0.8.1'.freeze
2
+ VERSION = '0.9.0'.freeze
3
3
  end
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.8.1
4
+ version: 0.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - BoringCache
@@ -100,7 +100,7 @@ homepage: https://github.com/boringcache/services
100
100
  licenses:
101
101
  - MIT
102
102
  metadata:
103
- source_code_uri: https://github.com/boringcache/services
103
+ source_code_uri: https://github.com/boringcache/services/tree/v0.9.0
104
104
  documentation_uri: https://github.com/boringcache/services/blob/main/README.md
105
105
  changelog_uri: https://github.com/boringcache/services/blob/main/CHANGELOG.md
106
106
  bug_tracker_uri: https://github.com/boringcache/services/issues