boring_services 0.2.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: afc3d44acfb6c7fe4d741a643dc87d08b2b9ae371daa24a054dea6c25f6c6429
4
+ data.tar.gz: d2c240bdbfe39bf716ac2025e4f8249b01e3b5dceda5fa6766601aeca308abb0
5
+ SHA512:
6
+ metadata.gz: 924f46cbdd506ecd7212bdd0836aa557f8e397986861f21f684f9d0e7a02d1ede2a627e1aca67adb11f4a7faf94ef207ca1dc4476c2e5768ee80ddc6420cd714
7
+ data.tar.gz: 180534ff00c723563bd8e09bd022d94e100cf9f2e864cfe561db2021d288cd2a818957fe9baef5436af267d69b5876df7c1e67560dc974ccb3036eea1891c65c
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Gaurav
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
data/README.md ADDED
@@ -0,0 +1,461 @@
1
+ # BoringServices
2
+
3
+ Deploy infrastructure services (Memcached, Redis, HAProxy, Nginx) to servers with Ruby and SSH. No Ansible, no Python, no Kubernetes.
4
+
5
+ Works standalone with any Ruby application or integrates seamlessly with Rails.
6
+
7
+ ## Features
8
+
9
+ - **Memcached** - In-memory caching with configurable parameters
10
+ - **Redis** - Key-value store and cache
11
+ - **HAProxy** - Load balancer with SSL/TLS support
12
+ - **Nginx** - Web server and reverse proxy
13
+ - **Simple Configuration** - YAML files with environment support
14
+ - **Flexible Overrides** - Custom config templates or parameter overrides
15
+ - **Secret Management** - Environment variables or command execution
16
+ - **SSH Deployment** - Direct deployment to Ubuntu servers
17
+ - **Health Checks** - Monitor service status across hosts
18
+ - **VPN Support** - Optional private_ip field for VPN/WireGuard deployments
19
+ - **Rails Integration** - Generators, rake tasks, and seamless integration
20
+ - **Standalone Support** - Works with any Ruby application
21
+
22
+ ## Quick Start
23
+
24
+ ### Standalone
25
+
26
+ ```bash
27
+ gem install boring_services
28
+ boringservices setup
29
+ boringservices status
30
+
31
+ # Target a different config/environment
32
+ BORING_SERVICES_CONFIG=config/services.staging.yml \
33
+ BORING_SERVICES_ENV=staging boringservices setup
34
+ ```
35
+
36
+ ### Rails Integration
37
+
38
+ ```ruby
39
+ # Gemfile
40
+ gem 'boring_services'
41
+ ```
42
+
43
+ ```bash
44
+ # Generate config
45
+ rails generate boring_services:install
46
+
47
+ # Deploy services
48
+ rails boring_services:setup
49
+
50
+ # Deploy using a different config/environment
51
+ BORING_SERVICES_CONFIG=config/services.staging.yml BORING_SERVICES_ENV=staging \
52
+ rails boring_services:setup
53
+
54
+ # Check health
55
+ rails boring_services:health
56
+ ```
57
+
58
+ ## Configuration
59
+
60
+ Create `config/services.yml`:
61
+
62
+ ```yaml
63
+ production:
64
+ user: ubuntu
65
+ ssh_key: ~/.ssh/id_rsa
66
+ forward_agent: true
67
+ use_ssh_agent: false
68
+ ssh_auth_methods:
69
+ - publickey
70
+
71
+ services:
72
+ - name: memcached
73
+ enabled: true
74
+ hosts:
75
+ - host: 10.8.0.20
76
+ label: cache-a
77
+ - host: 10.8.0.21
78
+ label: cache-b
79
+ port: 11211
80
+ memory_mb: 256
81
+
82
+ - name: redis
83
+ enabled: true
84
+ host: 10.8.0.22
85
+ port: 6379
86
+ memory_mb: 1024
87
+
88
+ - name: haproxy
89
+ enabled: true
90
+ hosts:
91
+ - 10.8.0.30
92
+ port: 80
93
+ stats_port: 8404
94
+ backends:
95
+ - host: 10.8.0.10
96
+ port: 3000
97
+ - host: 10.8.0.11
98
+ port: 3000
99
+
100
+ secrets:
101
+ redis_password: $REDIS_PASSWORD
102
+ ```
103
+
104
+ Use `host:` for a single target or `hosts:` for multiple VMs. When using `hosts`, each entry can be a simple hostname/IP or a hash with per-host overrides (e.g., `label`, `port`, or `memory_mb`).
105
+
106
+ ### Service Options
107
+
108
+ #### Memcached
109
+
110
+ **Basic Configuration:**
111
+
112
+ ```yaml
113
+ - name: memcached
114
+ enabled: true
115
+ hosts:
116
+ - 10.8.0.20
117
+ port: 11211 # Default: 11211
118
+ memory_mb: 256 # Memory allocation in MB
119
+ ```
120
+
121
+ **With Custom Parameters:**
122
+
123
+ ```yaml
124
+ - name: memcached
125
+ enabled: true
126
+ host: 10.8.0.20
127
+ port: 11211
128
+ memory_mb: 512
129
+ custom_params:
130
+ listen_address: 0.0.0.0 # Default: 0.0.0.0
131
+ max_connections: 2048 # Default: 1024
132
+ max_item_size: 2m # Optional: max item size
133
+ verbosity: 2 # Optional: 0-3 for debug output
134
+ ```
135
+
136
+ **With Custom Config Template:**
137
+
138
+ ```yaml
139
+ - name: memcached
140
+ enabled: true
141
+ host: 10.8.0.20
142
+ custom_config_template: config/memcached.custom.conf # Path to custom config file
143
+ ```
144
+
145
+ #### Redis
146
+
147
+ ```yaml
148
+ - name: redis
149
+ enabled: true
150
+ hosts:
151
+ - 10.8.0.21
152
+ port: 6379 # Default: 6379
153
+ memory_mb: 512 # Memory allocation
154
+ ```
155
+
156
+ Optional password protection via secrets:
157
+
158
+ ```yaml
159
+ secrets:
160
+ redis_password: $REDIS_PASSWORD
161
+ ```
162
+
163
+ #### HAProxy
164
+
165
+ **Basic HTTP Load Balancer:**
166
+
167
+ ```yaml
168
+ - name: haproxy
169
+ enabled: true
170
+ host: 10.8.0.30
171
+ port: 80 # Frontend port
172
+ stats_port: 8404 # Stats dashboard port
173
+ backends:
174
+ - host: 10.8.0.10
175
+ port: 3000
176
+ - host: 10.8.0.11
177
+ port: 3000
178
+ ```
179
+
180
+ **HTTPS with SSL Termination:**
181
+
182
+ ```yaml
183
+ - name: haproxy
184
+ enabled: true
185
+ host: 34.123.78.90
186
+ port: 80 # Redirects to HTTPS
187
+ https_port: 443 # SSL/TLS port
188
+ stats_port: 8404
189
+ ssl: true
190
+ ssl_cert: $(op read "op://MyVault/haproxy-ssl/certificate")
191
+ ssl_key: $(op read "op://MyVault/haproxy-ssl/private_key")
192
+ backends:
193
+ - host: 192.168.1.10
194
+ port: 3000
195
+ - host: 192.168.1.11
196
+ port: 3000
197
+ ```
198
+
199
+ **With Custom Parameters:**
200
+
201
+ ```yaml
202
+ - name: haproxy
203
+ enabled: true
204
+ host: 10.8.0.30
205
+ port: 80
206
+ stats_port: 8404
207
+ custom_params:
208
+ timeout_connect: 10000 # Default: 5000ms
209
+ timeout_client: 60000 # Default: 50000ms
210
+ timeout_server: 60000 # Default: 50000ms
211
+ balance: leastconn # Default: roundrobin
212
+ health_check_path: /healthz # Default: /health
213
+ ssl_ciphers: CUSTOM_CIPHERS # Custom SSL cipher suite
214
+ ssl_options: no-sslv3 # Custom SSL options
215
+ backends:
216
+ - host: 10.8.0.10
217
+ port: 3000
218
+ ```
219
+
220
+ **With Custom Config Template:**
221
+
222
+ ```yaml
223
+ - name: haproxy
224
+ enabled: true
225
+ host: 10.8.0.30
226
+ custom_config_template: config/haproxy.custom.cfg # Path to custom HAProxy config
227
+ # Note: When using custom template, most other options are ignored
228
+ ```
229
+
230
+ **SSL Certificate Options:**
231
+
232
+ ```yaml
233
+ # Option 1: 1Password CLI
234
+ ssl_cert: $(op read "op://MyVault/haproxy-ssl/certificate")
235
+ ssl_key: $(op read "op://MyVault/haproxy-ssl/private_key")
236
+
237
+ # Option 2: Environment variables
238
+ ssl_cert: $HAPROXY_SSL_CERT
239
+ ssl_key: $HAPROXY_SSL_KEY
240
+
241
+ # Option 3: Rails credentials (Rails only)
242
+ ssl_cert: $(rails credentials:show | yq .haproxy.ssl.cert)
243
+ ssl_key: $(rails credentials:show | yq .haproxy.ssl.key)
244
+
245
+ # Option 4: Local files
246
+ ssl_cert: $(cat /path/to/cert.pem)
247
+ ssl_key: $(cat /path/to/key.pem)
248
+ ```
249
+
250
+ **HAProxy Features:**
251
+ - āœ… **Config Validation** - Automatically validates HAProxy config before applying
252
+ - āœ… **SSL/TLS Support** - Automatic HTTPS setup with certificate management
253
+ - āœ… **Self-Signed Fallback** - Generates self-signed cert if none provided
254
+ - āœ… **HTTP → HTTPS Redirect** - Automatic redirect when SSL is enabled
255
+ - āœ… **Health Checks** - Configurable health check endpoint (default: GET /health)
256
+ - āœ… **Stats Dashboard** - Access at `http://your-host:8404/`
257
+ - āœ… **Custom Overrides** - Override timeouts, balance algorithms, SSL ciphers
258
+ - āœ… **Custom Templates** - Bring your own HAProxy config file
259
+
260
+ #### Nginx
261
+
262
+ ```yaml
263
+ - name: nginx
264
+ enabled: true
265
+ hosts:
266
+ - 10.8.0.40
267
+ port: 80
268
+ ssl: true # Enable HTTPS on port 443
269
+ backends:
270
+ - host: 10.8.0.10
271
+ port: 3000
272
+ ```
273
+
274
+ ## CLI Commands
275
+
276
+ ### Install Services
277
+
278
+ ```bash
279
+ boringservices install
280
+
281
+ boringservices install redis
282
+
283
+ boringservices install -e staging
284
+ ```
285
+
286
+ ### Check Status
287
+
288
+ ```bash
289
+ boringservices status
290
+
291
+ boringservices status -e production
292
+ ```
293
+
294
+ ### Restart Services
295
+
296
+ ```bash
297
+ boringservices restart redis
298
+
299
+ boringservices restart haproxy -e staging
300
+ ```
301
+
302
+ ### Uninstall Services
303
+
304
+ ```bash
305
+ boringservices uninstall memcached
306
+ ```
307
+
308
+ ## Rails Integration
309
+
310
+ Add to your `Gemfile`:
311
+
312
+ ```ruby
313
+ gem 'boring_services'
314
+ ```
315
+
316
+ Generate configuration:
317
+
318
+ ```bash
319
+ rails generate boring_services:install
320
+ # Creates config/services.yml
321
+
322
+ rails boring_services:setup
323
+ # Deploys all enabled services
324
+ ```
325
+
326
+ Use services in your Rails app:
327
+
328
+ ```ruby
329
+ Rails.application.configure do
330
+ # Connect to Memcached
331
+ config.cache_store = :mem_cache_store, '10.8.0.20:11211', '10.8.0.21:11211'
332
+
333
+ # Connect to Redis for sessions
334
+ config.session_store :redis_store,
335
+ servers: ['redis://10.8.0.22:6379/0/session'],
336
+ expire_after: 90.minutes
337
+ end
338
+ ```
339
+
340
+ **Available Rake Tasks:**
341
+
342
+ ```bash
343
+ rails boring_services:setup # Deploy all services
344
+ rails boring_services:health # Check service health
345
+ rails boring_services:status # Show service status
346
+ rails boring_services:restart # Restart all services
347
+ ```
348
+
349
+ ## Secrets Management
350
+
351
+ Use environment variables or command execution:
352
+
353
+ ```yaml
354
+ secrets:
355
+ # Option 1: Environment variable
356
+ redis_password: $REDIS_PASSWORD
357
+
358
+ # Option 2: 1Password CLI
359
+ redis_password: $(op read "op://MyVault/redis/password")
360
+
361
+ # Option 3: Rails credentials (Rails only)
362
+ redis_password: $(rails credentials:show | yq .redis.password)
363
+
364
+ # Option 4: File-based secrets
365
+ redis_password: $(cat .secrets/redis_password)
366
+ ```
367
+
368
+ ## VPN/Private Network Support
369
+
370
+ BoringServices supports deploying services over VPN or private networks (e.g., WireGuard, Tailscale):
371
+
372
+ - **Public IPs** (`host`) for SSH deployment access
373
+ - **Private IPs** (`private_ip`) for service communication
374
+ - Services listen on all interfaces (0.0.0.0) by default
375
+ - Accessible via private network IPs
376
+ - No public exposure of services
377
+
378
+ **Example with Private IPs:**
379
+
380
+ ```yaml
381
+ services:
382
+ - name: redis
383
+ host: 18.234.67.89 # ← Public IP for SSH deployment
384
+ private_ip: 10.8.0.32 # ← Private VPN IP for connections
385
+ label: us-east-1
386
+ port: 6379
387
+ memory_mb: 1024
388
+
389
+ - name: memcached
390
+ host: 51.15.214.103 # ← Public IP for SSH
391
+ private_ip: 10.8.0.61 # ← Private VPN IP
392
+ label: cache-eu
393
+ port: 11211
394
+ memory_mb: 256
395
+ ```
396
+
397
+ Your application connects via the private IP:
398
+
399
+ ```ruby
400
+ # Ruby/Rails app
401
+ Redis.new(url: 'redis://10.8.0.32:6379')
402
+
403
+ # Memcached
404
+ Dalli::Client.new('10.8.0.61:11211')
405
+ ```
406
+
407
+ The `private_ip` field is optional and purely for documentation - use it to track which IPs your application should connect to.
408
+
409
+ ## Health Monitoring
410
+
411
+ ```bash
412
+ boringservices status
413
+ ```
414
+
415
+ Output:
416
+
417
+ ```
418
+ memcached: healthy
419
+ āœ“ 10.8.0.20: running
420
+ āœ“ 10.8.0.21: running
421
+
422
+ redis: healthy
423
+ āœ“ 10.8.0.22: running
424
+
425
+ haproxy: healthy
426
+ āœ“ 10.8.0.30: running
427
+ ```
428
+
429
+ ## Requirements
430
+
431
+ - Ruby 3.0+
432
+ - Ubuntu 20.04+ servers (Debian-based distributions)
433
+ - SSH key-based authentication
434
+ - Optional: VPN solution (WireGuard, Tailscale, etc.) for private networking
435
+
436
+ ## Installation
437
+
438
+ **Standalone Ruby:**
439
+
440
+ ```bash
441
+ gem install boring_services
442
+ ```
443
+
444
+ **With Bundler:**
445
+
446
+ ```ruby
447
+ # Gemfile
448
+ gem 'boring_services'
449
+ ```
450
+
451
+ Then run:
452
+
453
+ ```bash
454
+ bundle install
455
+ ```
456
+
457
+ **For Rails projects**, the gem will automatically integrate with Rails and provide generators and rake tasks.
458
+
459
+ ## License
460
+
461
+ MIT
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require_relative '../lib/boring_services'
4
+
5
+ BoringServices::CLI.start(ARGV)
@@ -0,0 +1,100 @@
1
+ require 'thor'
2
+
3
+ module BoringServices
4
+ class CLI < Thor
5
+ class_option :config, aliases: '-c', default: ENV['BORING_SERVICES_CONFIG'] || 'config/services.yml',
6
+ desc: 'Path to services.yml'
7
+ class_option :environment, aliases: '-e',
8
+ default: ENV['BORING_SERVICES_ENV'] || ENV['BORING_ENVIRONMENT'] ||
9
+ ENV['RAILS_ENV'] || 'production',
10
+ desc: 'Environment (production, staging, development)'
11
+
12
+ def self.exit_on_failure?
13
+ true
14
+ end
15
+
16
+ desc 'setup', 'Setup/install all services (alias for install)'
17
+ def setup
18
+ config = Configuration.load(options[:config], options[:environment])
19
+ installer = Installer.new(config)
20
+ installer.install_all
21
+ rescue Error => e
22
+ puts "Error: #{e.message}"
23
+ exit 1
24
+ end
25
+
26
+ desc 'install [SERVICE]', 'Install service(s) - all services or specific service'
27
+ def install(service_name = nil)
28
+ config = Configuration.load(options[:config], options[:environment])
29
+ installer = Installer.new(config)
30
+
31
+ if service_name
32
+ installer.install_service(service_name)
33
+ else
34
+ installer.install_all
35
+ end
36
+ rescue Error => e
37
+ puts "Error: #{e.message}"
38
+ exit 1
39
+ end
40
+
41
+ desc 'uninstall SERVICE', 'Uninstall a specific service'
42
+ def uninstall(service_name)
43
+ config = Configuration.load(options[:config], options[:environment])
44
+ installer = Installer.new(config)
45
+ installer.uninstall_service(service_name)
46
+ rescue Error => e
47
+ puts "Error: #{e.message}"
48
+ exit 1
49
+ end
50
+
51
+ desc 'restart SERVICE', 'Restart a specific service'
52
+ def restart(service_name)
53
+ config = Configuration.load(options[:config], options[:environment])
54
+ installer = Installer.new(config)
55
+ installer.restart_service(service_name)
56
+ rescue Error => e
57
+ puts "Error: #{e.message}"
58
+ exit 1
59
+ end
60
+
61
+ desc 'reconfigure [SERVICE]', 'Reconfigure service(s) - skips package installation, only updates config and restarts'
62
+ def reconfigure(service_name = nil)
63
+ config = Configuration.load(options[:config], options[:environment])
64
+ installer = Installer.new(config)
65
+
66
+ if service_name
67
+ installer.reconfigure_service(service_name)
68
+ else
69
+ installer.reconfigure_all
70
+ end
71
+ rescue Error => e
72
+ puts "Error: #{e.message}"
73
+ exit 1
74
+ end
75
+
76
+ desc 'status', 'Check health status of all services'
77
+ def status
78
+ Configuration.load(options[:config], options[:environment])
79
+ results = BoringServices.status
80
+
81
+ results.each do |service_name, result|
82
+ puts "\n#{service_name}: #{result[:status]}"
83
+ next unless result[:hosts]
84
+
85
+ result[:hosts].each do |host, host_result|
86
+ status_icon = host_result[:running] ? 'āœ“' : 'āœ—'
87
+ puts " #{status_icon} #{host}: #{host_result[:running] ? 'running' : 'stopped'}"
88
+ end
89
+ end
90
+ rescue Error => e
91
+ puts "Error: #{e.message}"
92
+ exit 1
93
+ end
94
+
95
+ desc 'version', 'Show version'
96
+ def version
97
+ puts "boring_services #{VERSION}"
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,80 @@
1
+ require 'yaml'
2
+ require 'erb'
3
+
4
+ module BoringServices
5
+ class Configuration
6
+ attr_reader :config, :environment
7
+
8
+ def self.load(config_path = 'config/services.yml', environment = nil)
9
+ env = environment || ENV['BORING_SERVICES_ENV'] || ENV['BORING_ENVIRONMENT'] || ENV['RAILS_ENV'] || 'production'
10
+ new(config_path, env)
11
+ end
12
+
13
+ def initialize(config_path, environment)
14
+ @config_path = config_path
15
+ @environment = environment.to_s
16
+ @config = load_config
17
+ end
18
+
19
+ def service_config(service_name)
20
+ services = @config['services'] || []
21
+ services.find { |s| s['name'] == service_name.to_s }
22
+ end
23
+
24
+ def service_enabled?(service_name)
25
+ service = service_config(service_name)
26
+ service && service['enabled'] != false
27
+ end
28
+
29
+ def services
30
+ @config['services'] || []
31
+ end
32
+
33
+ def enabled_services
34
+ services.reject { |s| s['enabled'] == false }
35
+ end
36
+
37
+ def user
38
+ @config['user'] || 'ubuntu'
39
+ end
40
+
41
+ def ssh_key
42
+ @config['ssh_key'] || '~/.ssh/id_rsa'
43
+ end
44
+
45
+ def forward_agent
46
+ return @config['forward_agent'] unless @config['forward_agent'].nil?
47
+
48
+ true
49
+ end
50
+
51
+ def use_ssh_agent
52
+ return @config['use_ssh_agent'] unless @config['use_ssh_agent'].nil?
53
+
54
+ false
55
+ end
56
+
57
+ def ssh_auth_methods
58
+ (@config['ssh_auth_methods'] || ['publickey']).map(&:to_s)
59
+ end
60
+
61
+ def secrets
62
+ @config['secrets'] || {}
63
+ end
64
+
65
+ private
66
+
67
+ def load_config
68
+ raise Error, "Config file not found: #{@config_path}" unless File.exist?(@config_path)
69
+
70
+ content = File.read(@config_path)
71
+ erb_result = ERB.new(content).result
72
+ full_config = YAML.safe_load(erb_result, permitted_classes: [Symbol], aliases: true)
73
+
74
+ env_config = full_config[@environment]
75
+ raise Error, "Environment '#{@environment}' not found in config" unless env_config
76
+
77
+ env_config
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,25 @@
1
+ require 'rails/generators'
2
+
3
+ module BoringServices
4
+ module Generators
5
+ class InstallGenerator < ::Rails::Generators::Base
6
+ source_root File.expand_path('templates', __dir__)
7
+
8
+ def create_config_file
9
+ template 'services.yml.erb', 'config/services.yml'
10
+ end
11
+
12
+ def show_instructions
13
+ puts "\nāœ… BoringServices installed!"
14
+ puts "\nšŸ“ Next steps:"
15
+ puts ' 1. Either:'
16
+ puts ' a) Edit config/services.yml with your service hosts, OR'
17
+ puts ' b) Use Terraform to auto-generate config/services.yml'
18
+ puts ' 2. Deploy services: rails boring_services:setup'
19
+ puts ' 3. Check status: rails boring_services:status'
20
+ puts "\nšŸ“š Available: Memcached, Redis, HAProxy (SSL), Nginx"
21
+ puts ' See config/services.example.yml in gem for examples'
22
+ end
23
+ end
24
+ end
25
+ end