nvoi 0.1.8 → 0.2.1

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.
Files changed (122) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +1 -5
  3. data/Gemfile.lock +17 -8
  4. data/Rakefile +1 -1
  5. data/_TODO-rails-example.md +816 -0
  6. data/_TODO-rails-optimization.md +433 -0
  7. data/doc/config-schema.yaml +12 -0
  8. data/examples/apex-wildcard/deploy.yml +1 -0
  9. data/examples/golang-postgres-multi/deploy.yml +1 -0
  10. data/examples/postgres-multi/deploy.yml +1 -0
  11. data/examples/postgres-single/deploy.yml +1 -0
  12. data/examples/rails-single/deploy.yml +1 -0
  13. data/lib/nvoi/cli/config/command.rb +46 -41
  14. data/lib/nvoi/cli/credentials/edit/command.rb +24 -20
  15. data/lib/nvoi/cli/credentials/show/command.rb +1 -1
  16. data/lib/nvoi/cli/db/command.rb +10 -10
  17. data/lib/nvoi/cli/delete/command.rb +2 -2
  18. data/lib/nvoi/cli/deploy/command.rb +2 -2
  19. data/lib/nvoi/cli/deploy/steps/build_image.rb +2 -1
  20. data/lib/nvoi/cli/deploy/steps/configure_tunnel.rb +2 -2
  21. data/lib/nvoi/cli/deploy/steps/deploy_service.rb +7 -4
  22. data/lib/nvoi/cli/deploy/steps/provision_server.rb +1 -1
  23. data/lib/nvoi/cli/deploy/steps/provision_volume.rb +1 -1
  24. data/lib/nvoi/cli/exec/command.rb +3 -3
  25. data/lib/nvoi/cli/logs/command.rb +2 -2
  26. data/lib/nvoi/cli/onboard/command.rb +176 -622
  27. data/lib/nvoi/cli/onboard/steps/app.rb +108 -0
  28. data/lib/nvoi/cli/onboard/steps/app_name.rb +26 -0
  29. data/lib/nvoi/cli/onboard/steps/compute.rb +186 -0
  30. data/lib/nvoi/cli/onboard/steps/database.rb +97 -0
  31. data/lib/nvoi/cli/onboard/steps/domain.rb +48 -0
  32. data/lib/nvoi/cli/onboard/steps/env.rb +67 -0
  33. data/lib/nvoi/cli/onboard/ui.rb +84 -0
  34. data/lib/nvoi/cli/unlock/command.rb +2 -2
  35. data/lib/nvoi/cli.rb +2 -33
  36. data/lib/nvoi/configuration/app_service.rb +54 -0
  37. data/lib/nvoi/configuration/application.rb +44 -0
  38. data/lib/nvoi/configuration/builder.rb +420 -0
  39. data/lib/nvoi/configuration/database.rb +56 -0
  40. data/lib/nvoi/configuration/deploy.rb +15 -0
  41. data/lib/nvoi/{objects/service_spec.rb → configuration/deployment.rb} +4 -3
  42. data/lib/nvoi/{objects/config_override.rb → configuration/override.rb} +4 -4
  43. data/lib/nvoi/configuration/providers.rb +81 -0
  44. data/lib/nvoi/configuration/result.rb +43 -0
  45. data/lib/nvoi/configuration/root.rb +252 -0
  46. data/lib/nvoi/configuration/server.rb +39 -0
  47. data/lib/nvoi/configuration/service.rb +51 -0
  48. data/lib/nvoi/configuration/ssh_key.rb +16 -0
  49. data/lib/nvoi/external/cloud/aws.rb +26 -16
  50. data/lib/nvoi/external/cloud/hetzner.rb +40 -25
  51. data/lib/nvoi/external/cloud/scaleway.rb +10 -8
  52. data/lib/nvoi/external/cloud/types.rb +42 -0
  53. data/lib/nvoi/external/database/mysql.rb +1 -1
  54. data/lib/nvoi/external/database/postgres.rb +1 -1
  55. data/lib/nvoi/external/database/provider.rb +1 -1
  56. data/lib/nvoi/external/database/sqlite.rb +1 -1
  57. data/lib/nvoi/external/database/types.rb +55 -0
  58. data/lib/nvoi/external/dns/cloudflare.rb +11 -11
  59. data/lib/nvoi/external/dns/types.rb +24 -0
  60. data/lib/nvoi/utils/config_loader.rb +12 -12
  61. data/lib/nvoi/utils/credential_store.rb +4 -4
  62. data/lib/nvoi/utils/env_resolver.rb +3 -3
  63. data/lib/nvoi/utils/namer.rb +8 -3
  64. data/lib/nvoi/utils/presence.rb +23 -0
  65. data/lib/nvoi/version.rb +1 -1
  66. data/lib/nvoi.rb +2 -17
  67. metadata +98 -59
  68. data/.claude/todo/refactor/00-overview.md +0 -171
  69. data/.claude/todo/refactor/01-objects.md +0 -96
  70. data/.claude/todo/refactor/02-utils.md +0 -143
  71. data/.claude/todo/refactor/03-external-cloud.md +0 -164
  72. data/.claude/todo/refactor/04-external-dns.md +0 -104
  73. data/.claude/todo/refactor/05-external.md +0 -133
  74. data/.claude/todo/refactor/06-cli.md +0 -123
  75. data/.claude/todo/refactor/07-cli-deploy-command.md +0 -177
  76. data/.claude/todo/refactor/08-cli-deploy-steps.md +0 -201
  77. data/.claude/todo/refactor/09-cli-delete-command.md +0 -169
  78. data/.claude/todo/refactor/10-cli-exec-command.md +0 -157
  79. data/.claude/todo/refactor/11-cli-credentials-command.md +0 -190
  80. data/.claude/todo/refactor/12-cli-db-command.md +0 -128
  81. data/.claude/todo/refactor/_target.md +0 -79
  82. data/.claude/todo/refactor-execution/00-entrypoint.md +0 -49
  83. data/.claude/todo/refactor-execution/01-objects.md +0 -42
  84. data/.claude/todo/refactor-execution/02-utils.md +0 -41
  85. data/.claude/todo/refactor-execution/03-external-cloud.md +0 -38
  86. data/.claude/todo/refactor-execution/04-external-dns.md +0 -35
  87. data/.claude/todo/refactor-execution/05-external-other.md +0 -46
  88. data/.claude/todo/refactor-execution/06-cli-deploy.md +0 -45
  89. data/.claude/todo/refactor-execution/07-cli-delete.md +0 -43
  90. data/.claude/todo/refactor-execution/08-cli-exec.md +0 -30
  91. data/.claude/todo/refactor-execution/09-cli-credentials.md +0 -34
  92. data/.claude/todo/refactor-execution/10-cli-db.md +0 -31
  93. data/.claude/todo/refactor-execution/11-cli-router.md +0 -44
  94. data/.claude/todo/refactor-execution/12-cleanup.md +0 -120
  95. data/.claude/todo/refactor-execution/_monitoring-strategy.md +0 -126
  96. data/.claude/todo/scaleway.impl.md +0 -644
  97. data/.claude/todo/scaleway.reference.md +0 -520
  98. data/.claude/todos/buckets.md +0 -41
  99. data/.claude/todos.md +0 -550
  100. data/Makefile +0 -26
  101. data/ingest +0 -0
  102. data/lib/nvoi/config_api/actions/app.rb +0 -53
  103. data/lib/nvoi/config_api/actions/compute_provider.rb +0 -55
  104. data/lib/nvoi/config_api/actions/database.rb +0 -70
  105. data/lib/nvoi/config_api/actions/domain_provider.rb +0 -40
  106. data/lib/nvoi/config_api/actions/env.rb +0 -32
  107. data/lib/nvoi/config_api/actions/init.rb +0 -67
  108. data/lib/nvoi/config_api/actions/secret.rb +0 -32
  109. data/lib/nvoi/config_api/actions/server.rb +0 -66
  110. data/lib/nvoi/config_api/actions/service.rb +0 -52
  111. data/lib/nvoi/config_api/actions/volume.rb +0 -40
  112. data/lib/nvoi/config_api/base.rb +0 -38
  113. data/lib/nvoi/config_api/result.rb +0 -26
  114. data/lib/nvoi/config_api.rb +0 -93
  115. data/lib/nvoi/objects/configuration.rb +0 -483
  116. data/lib/nvoi/objects/database.rb +0 -56
  117. data/lib/nvoi/objects/dns.rb +0 -14
  118. data/lib/nvoi/objects/firewall.rb +0 -11
  119. data/lib/nvoi/objects/network.rb +0 -11
  120. data/lib/nvoi/objects/server.rb +0 -14
  121. data/lib/nvoi/objects/tunnel.rb +0 -14
  122. data/lib/nvoi/objects/volume.rb +0 -17
@@ -0,0 +1,433 @@
1
+ # NVOI Gem - Rails Optimization TODO
2
+
3
+ Changes needed in the `nvoi` gem to support **optional** dashboard integration.
4
+
5
+ ---
6
+
7
+ ## Modes
8
+
9
+ ### Standalone Mode (default)
10
+
11
+ No `callback_url` in config. Gem works exactly as today:
12
+
13
+ ```
14
+ nvoi deploy
15
+
16
+ logs to stdout
17
+
18
+ done
19
+ ```
20
+
21
+ ### Dashboard Mode (optional)
22
+
23
+ When `callback_url` is set in config:
24
+
25
+ ```
26
+ nvoi deploy
27
+
28
+ logs to stdout AND POSTs to callback_url
29
+
30
+ Rails dashboard receives logs in real-time
31
+ ```
32
+
33
+ **The gem remains 100% standalone.** Dashboard integration is opt-in via config.
34
+
35
+ ---
36
+
37
+ ## Architecture (Dashboard Mode)
38
+
39
+ ```
40
+ CI (GitHub Actions, GitLab CI, etc.)
41
+
42
+ gem install nvoi && nvoi deploy
43
+
44
+ gem decrypts deploy.enc with DEPLOY_KEY
45
+
46
+ gem POSTs logs to callback_url (signed with DEPLOY_KEY)
47
+
48
+ Rails receives, stores, broadcasts via Turbo
49
+ ```
50
+
51
+ **One key, two purposes (when dashboard enabled):**
52
+
53
+ 1. Decrypt `deploy.enc`
54
+ 2. HMAC-sign API callbacks to Rails
55
+
56
+ ---
57
+
58
+ ## 1. Callback Configuration
59
+
60
+ Add optional callback URL to deploy config schema:
61
+
62
+ ```ruby
63
+ # lib/nvoi/configuration/deploy.rb
64
+ class Deploy
65
+ # Existing fields...
66
+
67
+ # New: callback URL for log streaming
68
+ def callback_url
69
+ @data["callback_url"]
70
+ end
71
+
72
+ def callback_enabled?
73
+ callback_url.present?
74
+ end
75
+ end
76
+ ```
77
+
78
+ Config example:
79
+
80
+ ```yaml
81
+ # In decrypted deploy config
82
+ callback_url: "https://myapp.com/api/deploys"
83
+ ```
84
+
85
+ ---
86
+
87
+ ## 2. HTTP Logger Adapter
88
+
89
+ New adapter that POSTs logs to Rails:
90
+
91
+ ```ruby
92
+ # lib/nvoi/adapters/logger/http.rb
93
+ module Nvoi
94
+ module Adapters
95
+ module Logger
96
+ class Http < Base
97
+ def initialize(url:, key:, deploy_id:, fallback: nil)
98
+ @url = url
99
+ @key = key
100
+ @deploy_id = deploy_id
101
+ @fallback = fallback || Stdout.new
102
+ @buffer = []
103
+ @flush_thread = start_flush_thread
104
+ end
105
+
106
+ def info(message, *args)
107
+ log(:info, format_message(message, args))
108
+ end
109
+
110
+ def success(message, *args)
111
+ log(:success, format_message(message, args))
112
+ end
113
+
114
+ def error(message, *args)
115
+ log(:error, format_message(message, args))
116
+ end
117
+
118
+ def warning(message, *args)
119
+ log(:warning, format_message(message, args))
120
+ end
121
+
122
+ def step(message, *args)
123
+ log(:step, format_message(message, args))
124
+ end
125
+
126
+ def ok(message, *args)
127
+ log(:ok, format_message(message, args))
128
+ end
129
+
130
+ def flush
131
+ return if @buffer.empty?
132
+
133
+ logs = @buffer.dup
134
+ @buffer.clear
135
+
136
+ send_logs(logs)
137
+ end
138
+
139
+ def close
140
+ @flush_thread&.kill
141
+ flush
142
+ send_status(:completed)
143
+ end
144
+
145
+ def fail!(error_message)
146
+ flush
147
+ send_status(:failed, error: error_message)
148
+ end
149
+
150
+ private
151
+
152
+ def log(level, message)
153
+ @fallback&.public_send(level, message)
154
+
155
+ @buffer << {
156
+ level: level,
157
+ message: message,
158
+ logged_at: Time.now.iso8601(3)
159
+ }
160
+ end
161
+
162
+ def start_flush_thread
163
+ Thread.new do
164
+ loop do
165
+ sleep 1
166
+ flush
167
+ end
168
+ end
169
+ end
170
+
171
+ def send_logs(logs)
172
+ payload = { logs: logs }
173
+ post("#{@url}/#{@deploy_id}/logs", payload)
174
+ rescue => e
175
+ @fallback&.warning("Callback failed: #{e.message}")
176
+ end
177
+
178
+ def send_status(status, error: nil)
179
+ payload = { status: status, error: error }
180
+ post("#{@url}/#{@deploy_id}/status", payload)
181
+ rescue => e
182
+ @fallback&.warning("Status callback failed: #{e.message}")
183
+ end
184
+
185
+ def post(url, payload)
186
+ body = payload.to_json
187
+ signature = sign(body)
188
+
189
+ uri = URI(url)
190
+ http = Net::HTTP.new(uri.host, uri.port)
191
+ http.use_ssl = uri.scheme == "https"
192
+
193
+ request = Net::HTTP::Post.new(uri.path)
194
+ request["Content-Type"] = "application/json"
195
+ request["X-Nvoi-Signature"] = signature
196
+ request["X-Nvoi-Deploy-Id"] = @deploy_id
197
+ request.body = body
198
+
199
+ response = http.request(request)
200
+
201
+ unless response.is_a?(Net::HTTPSuccess)
202
+ raise "HTTP #{response.code}: #{response.body}"
203
+ end
204
+ end
205
+
206
+ def sign(body)
207
+ "sha256=" + OpenSSL::HMAC.hexdigest("SHA256", @key, body)
208
+ end
209
+ end
210
+ end
211
+ end
212
+ end
213
+ ```
214
+
215
+ ---
216
+
217
+ ## 3. CLI Integration
218
+
219
+ Update deploy command to use HTTP logger when callback configured:
220
+
221
+ ```ruby
222
+ # lib/nvoi/cli/deploy/command.rb
223
+ def run
224
+ @log = build_logger
225
+
226
+ # ... existing deploy logic ...
227
+
228
+ @log.close # flush remaining logs + send completed status
229
+ rescue => e
230
+ @log.fail!(e.message)
231
+ raise
232
+ end
233
+
234
+ private
235
+
236
+ def build_logger
237
+ if @config.callback_enabled?
238
+ deploy_id = ENV["NVOI_DEPLOY_ID"] || ENV["GITHUB_RUN_ID"] || SecureRandom.uuid
239
+
240
+ Adapters::Logger::Http.new(
241
+ url: @config.callback_url,
242
+ key: load_deploy_key,
243
+ deploy_id: deploy_id,
244
+ fallback: Utils::Logger.new # still print to stdout
245
+ )
246
+ else
247
+ Utils::Logger.new
248
+ end
249
+ end
250
+
251
+ def load_deploy_key
252
+ key_path = resolve_key_path
253
+ File.read(key_path).strip
254
+ end
255
+ ```
256
+
257
+ ---
258
+
259
+ ## 4. Status Callbacks
260
+
261
+ Send deploy lifecycle events:
262
+
263
+ ```ruby
264
+ # Callback payloads:
265
+
266
+ # POST /api/deploys/:id/logs
267
+ {
268
+ "logs": [
269
+ { "level": "info", "message": "Starting deploy...", "logged_at": "2024-01-15T10:30:00.123Z" },
270
+ { "level": "success", "message": "Server provisioned", "logged_at": "2024-01-15T10:30:05.456Z" }
271
+ ]
272
+ }
273
+
274
+ # POST /api/deploys/:id/status
275
+ {
276
+ "status": "started",
277
+ "git_sha": "abc123",
278
+ "git_ref": "main",
279
+ "ci_provider": "github_actions",
280
+ "ci_run_url": "https://github.com/user/repo/actions/runs/12345"
281
+ }
282
+
283
+ # POST /api/deploys/:id/status (on complete)
284
+ {
285
+ "status": "completed",
286
+ "tunnels": [
287
+ { "service_name": "web", "hostname": "www.example.com" }
288
+ ],
289
+ "duration_seconds": 120
290
+ }
291
+
292
+ # POST /api/deploys/:id/status (on failure)
293
+ {
294
+ "status": "failed",
295
+ "error": "SSH connection failed"
296
+ }
297
+ ```
298
+
299
+ ---
300
+
301
+ ## 5. Delete Command Callbacks
302
+
303
+ Same pattern for delete:
304
+
305
+ ```ruby
306
+ # lib/nvoi/cli/delete/command.rb
307
+ def run
308
+ @log = build_logger
309
+ send_status(:started)
310
+
311
+ # ... existing delete logic ...
312
+
313
+ send_status(:completed)
314
+ @log.close
315
+ rescue => e
316
+ send_status(:failed, error: e.message)
317
+ @log.close
318
+ raise
319
+ end
320
+ ```
321
+
322
+ ---
323
+
324
+ ## 6. Environment Variables
325
+
326
+ CI provides context:
327
+
328
+ | Variable | Source | Purpose |
329
+ | ------------------- | --------------------------- | ------------------------ |
330
+ | `NVOI_DEPLOY_ID` | Set by CI or auto-generated | Unique deploy identifier |
331
+ | `GITHUB_RUN_ID` | GitHub Actions | Fallback deploy ID |
332
+ | `GITHUB_SHA` | GitHub Actions | Git commit SHA |
333
+ | `GITHUB_REF_NAME` | GitHub Actions | Branch name |
334
+ | `GITHUB_SERVER_URL` | GitHub Actions | Build CI run URL |
335
+ | `GITHUB_REPOSITORY` | GitHub Actions | repo owner/name |
336
+ | `GITHUB_RUN_ID` | GitHub Actions | Run ID for URL |
337
+
338
+ ---
339
+
340
+ ## 7. File Structure
341
+
342
+ ```
343
+ lib/nvoi/
344
+ adapters/
345
+ logger/
346
+ base.rb
347
+ http.rb # NEW
348
+ stdout.rb # renamed from utils/logger.rb
349
+ null.rb
350
+ cli/
351
+ deploy/
352
+ command.rb # MODIFIED - use callback logger
353
+ delete/
354
+ command.rb # MODIFIED - use callback logger
355
+ ```
356
+
357
+ ---
358
+
359
+ ## 8. Onboard Wizard Enhancement
360
+
361
+ Add callback URL step:
362
+
363
+ ```ruby
364
+ # lib/nvoi/cli/onboard/steps/callback_step.rb
365
+ class CallbackStep
366
+ def run(state)
367
+ use_callback = prompt.yes?("Stream deploy logs to a dashboard?")
368
+ return state unless use_callback
369
+
370
+ url = prompt.ask("Callback URL:", required: true)
371
+
372
+ state.merge(callback_url: url)
373
+ end
374
+ end
375
+ ```
376
+
377
+ ---
378
+
379
+ ## 9. Testing
380
+
381
+ ```ruby
382
+ # test/nvoi/adapters/logger/http_test.rb
383
+ class HttpLoggerTest < Minitest::Test
384
+ def setup
385
+ @url = "https://example.com/api/deploys"
386
+ @key = "test-key-123"
387
+ @deploy_id = "run-456"
388
+ end
389
+
390
+ def test_signs_requests_with_hmac
391
+ stub_request(:post, "#{@url}/#{@deploy_id}/logs")
392
+ .with { |req|
393
+ signature = req.headers["X-Nvoi-Signature"]
394
+ expected = "sha256=" + OpenSSL::HMAC.hexdigest("SHA256", @key, req.body)
395
+ signature == expected
396
+ }
397
+ .to_return(status: 200)
398
+
399
+ logger = Nvoi::Adapters::Logger::Http.new(
400
+ url: @url, key: @key, deploy_id: @deploy_id
401
+ )
402
+ logger.info("test")
403
+ logger.flush
404
+ end
405
+
406
+ def test_buffers_and_flushes
407
+ stub = stub_request(:post, "#{@url}/#{@deploy_id}/logs")
408
+ .to_return(status: 200)
409
+
410
+ logger = Nvoi::Adapters::Logger::Http.new(
411
+ url: @url, key: @key, deploy_id: @deploy_id
412
+ )
413
+
414
+ logger.info("one")
415
+ logger.info("two")
416
+ logger.flush
417
+
418
+ assert_requested(stub, times: 1)
419
+ end
420
+ end
421
+ ```
422
+
423
+ ---
424
+
425
+ ## 10. Migration Path
426
+
427
+ 1. Add HTTP logger adapter (non-breaking)
428
+ 2. Add callback_url config option (non-breaking)
429
+ 3. Update CLI to use callback logger when configured (non-breaking)
430
+ 4. Update onboard wizard (non-breaking)
431
+ 5. Release as 0.3.0
432
+
433
+ No breaking changes for existing CLI users.
@@ -127,6 +127,7 @@ components:
127
127
  description: Hetzner Cloud provider configuration
128
128
  required:
129
129
  - api_token
130
+ - architecture
130
131
  properties:
131
132
  api_token:
132
133
  type: string
@@ -141,6 +142,11 @@ components:
141
142
  description: Default datacenter location
142
143
  enum: [fsn1, nbg1, hel1, ash, hil]
143
144
  example: fsn1
145
+ architecture:
146
+ type: string
147
+ description: CPU architecture for Docker builds
148
+ enum: [x86, arm64]
149
+ example: x86
144
150
 
145
151
  AWSConfig:
146
152
  type: object
@@ -149,6 +155,7 @@ components:
149
155
  - access_key_id
150
156
  - secret_access_key
151
157
  - region
158
+ - architecture
152
159
  properties:
153
160
  access_key_id:
154
161
  type: string
@@ -164,6 +171,11 @@ components:
164
171
  type: string
165
172
  description: Default EC2 instance type
166
173
  example: t3.medium
174
+ architecture:
175
+ type: string
176
+ description: CPU architecture for Docker builds
177
+ enum: [x86, arm64]
178
+ example: x86
167
179
 
168
180
  ServerConfig:
169
181
  type: object
@@ -19,6 +19,7 @@ application:
19
19
  api_token: $HETZNER_API_TOKEN
20
20
  server_type: cx22
21
21
  server_location: fsn1
22
+ architecture: x86
22
23
 
23
24
  servers:
24
25
  master:
@@ -13,6 +13,7 @@ application:
13
13
  api_token: $HETZNER_API_TOKEN
14
14
  server_type: cx22 # Default for all servers
15
15
  server_location: nbg1
16
+ architecture: x86
16
17
 
17
18
  # Multi-instance server configuration: 3 servers total
18
19
  # 1 master (K3s control plane) + 2 workers (app + database)
@@ -13,6 +13,7 @@ application:
13
13
  api_token: $HETZNER_API_TOKEN
14
14
  server_type: cx22 # Default for all servers
15
15
  server_location: nbg1
16
+ architecture: x86
16
17
 
17
18
  # Multi-instance server configuration: 4 servers total
18
19
  servers:
@@ -13,6 +13,7 @@ application:
13
13
  api_token: $HETZNER_API_TOKEN
14
14
  server_type: cx22
15
15
  server_location: fsn1
16
+ architecture: x86
16
17
 
17
18
  # Server configuration (single server, master: true is implicit)
18
19
  servers:
@@ -12,6 +12,7 @@ application:
12
12
  api_token: $HETZNER_API_TOKEN
13
13
  server_type: cx22
14
14
  server_location: fsn1
15
+ architecture: x86
15
16
 
16
17
  servers:
17
18
  master: