nvoi 0.1.5 → 0.1.7

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 (156) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/todo/refactor/00-overview.md +171 -0
  3. data/.claude/todo/refactor/01-objects.md +96 -0
  4. data/.claude/todo/refactor/02-utils.md +143 -0
  5. data/.claude/todo/refactor/03-external-cloud.md +164 -0
  6. data/.claude/todo/refactor/04-external-dns.md +104 -0
  7. data/.claude/todo/refactor/05-external.md +133 -0
  8. data/.claude/todo/refactor/06-cli.md +123 -0
  9. data/.claude/todo/refactor/07-cli-deploy-command.md +177 -0
  10. data/.claude/todo/refactor/08-cli-deploy-steps.md +201 -0
  11. data/.claude/todo/refactor/09-cli-delete-command.md +169 -0
  12. data/.claude/todo/refactor/10-cli-exec-command.md +157 -0
  13. data/.claude/todo/refactor/11-cli-credentials-command.md +190 -0
  14. data/.claude/todo/refactor/12-cli-db-command.md +128 -0
  15. data/.claude/todo/refactor/_target.md +79 -0
  16. data/.claude/todo/refactor-execution/00-entrypoint.md +49 -0
  17. data/.claude/todo/refactor-execution/01-objects.md +42 -0
  18. data/.claude/todo/refactor-execution/02-utils.md +41 -0
  19. data/.claude/todo/refactor-execution/03-external-cloud.md +38 -0
  20. data/.claude/todo/refactor-execution/04-external-dns.md +35 -0
  21. data/.claude/todo/refactor-execution/05-external-other.md +46 -0
  22. data/.claude/todo/refactor-execution/06-cli-deploy.md +45 -0
  23. data/.claude/todo/refactor-execution/07-cli-delete.md +43 -0
  24. data/.claude/todo/refactor-execution/08-cli-exec.md +30 -0
  25. data/.claude/todo/refactor-execution/09-cli-credentials.md +34 -0
  26. data/.claude/todo/refactor-execution/10-cli-db.md +31 -0
  27. data/.claude/todo/refactor-execution/11-cli-router.md +44 -0
  28. data/.claude/todo/refactor-execution/12-cleanup.md +120 -0
  29. data/.claude/todo/refactor-execution/_monitoring-strategy.md +126 -0
  30. data/.claude/todo/scaleway.impl.md +644 -0
  31. data/.claude/todo/scaleway.reference.md +520 -0
  32. data/.claude/todos.md +550 -0
  33. data/Gemfile +6 -0
  34. data/Gemfile.lock +46 -5
  35. data/Rakefile +1 -1
  36. data/doc/config-schema.yaml +44 -11
  37. data/examples/golang/deploy.enc +0 -0
  38. data/examples/golang/main.go +18 -0
  39. data/exe/nvoi +3 -1
  40. data/ingest +0 -0
  41. data/lib/nvoi/cli/config/command.rb +219 -0
  42. data/lib/nvoi/cli/credentials/edit/command.rb +384 -0
  43. data/lib/nvoi/cli/credentials/show/command.rb +35 -0
  44. data/lib/nvoi/cli/db/command.rb +308 -0
  45. data/lib/nvoi/cli/delete/command.rb +75 -0
  46. data/lib/nvoi/cli/delete/steps/detach_volumes.rb +98 -0
  47. data/lib/nvoi/cli/delete/steps/teardown_dns.rb +50 -0
  48. data/lib/nvoi/cli/delete/steps/teardown_firewall.rb +46 -0
  49. data/lib/nvoi/cli/delete/steps/teardown_network.rb +30 -0
  50. data/lib/nvoi/cli/delete/steps/teardown_server.rb +50 -0
  51. data/lib/nvoi/cli/delete/steps/teardown_tunnel.rb +44 -0
  52. data/lib/nvoi/cli/delete/steps/teardown_volume.rb +61 -0
  53. data/lib/nvoi/cli/deploy/command.rb +184 -0
  54. data/lib/nvoi/cli/deploy/steps/build_image.rb +27 -0
  55. data/lib/nvoi/cli/deploy/steps/cleanup_images.rb +42 -0
  56. data/lib/nvoi/cli/deploy/steps/configure_tunnel.rb +102 -0
  57. data/lib/nvoi/cli/deploy/steps/deploy_service.rb +399 -0
  58. data/lib/nvoi/cli/deploy/steps/provision_network.rb +44 -0
  59. data/lib/nvoi/cli/deploy/steps/provision_server.rb +143 -0
  60. data/lib/nvoi/cli/deploy/steps/provision_volume.rb +171 -0
  61. data/lib/nvoi/cli/deploy/steps/setup_k3s.rb +490 -0
  62. data/lib/nvoi/cli/exec/command.rb +173 -0
  63. data/lib/nvoi/cli/logs/command.rb +66 -0
  64. data/lib/nvoi/cli/onboard/command.rb +761 -0
  65. data/lib/nvoi/cli/unlock/command.rb +72 -0
  66. data/lib/nvoi/cli.rb +339 -141
  67. data/lib/nvoi/config_api/actions/app.rb +53 -0
  68. data/lib/nvoi/config_api/actions/compute_provider.rb +55 -0
  69. data/lib/nvoi/config_api/actions/database.rb +70 -0
  70. data/lib/nvoi/config_api/actions/domain_provider.rb +40 -0
  71. data/lib/nvoi/config_api/actions/env.rb +32 -0
  72. data/lib/nvoi/config_api/actions/init.rb +67 -0
  73. data/lib/nvoi/config_api/actions/secret.rb +32 -0
  74. data/lib/nvoi/config_api/actions/server.rb +66 -0
  75. data/lib/nvoi/config_api/actions/service.rb +52 -0
  76. data/lib/nvoi/config_api/actions/volume.rb +40 -0
  77. data/lib/nvoi/config_api/base.rb +38 -0
  78. data/lib/nvoi/config_api/result.rb +26 -0
  79. data/lib/nvoi/config_api.rb +93 -0
  80. data/lib/nvoi/errors.rb +68 -50
  81. data/lib/nvoi/external/cloud/aws.rb +450 -0
  82. data/lib/nvoi/external/cloud/base.rb +99 -0
  83. data/lib/nvoi/external/cloud/factory.rb +48 -0
  84. data/lib/nvoi/external/cloud/hetzner.rb +402 -0
  85. data/lib/nvoi/external/cloud/scaleway.rb +559 -0
  86. data/lib/nvoi/external/cloud.rb +15 -0
  87. data/lib/nvoi/external/containerd.rb +86 -0
  88. data/lib/nvoi/external/database/mysql.rb +84 -0
  89. data/lib/nvoi/external/database/postgres.rb +82 -0
  90. data/lib/nvoi/external/database/provider.rb +65 -0
  91. data/lib/nvoi/external/database/sqlite.rb +72 -0
  92. data/lib/nvoi/external/database.rb +22 -0
  93. data/lib/nvoi/external/dns/cloudflare.rb +310 -0
  94. data/lib/nvoi/external/kubectl.rb +65 -0
  95. data/lib/nvoi/external/ssh.rb +106 -0
  96. data/lib/nvoi/objects/config_override.rb +60 -0
  97. data/lib/nvoi/objects/configuration.rb +483 -0
  98. data/lib/nvoi/objects/database.rb +56 -0
  99. data/lib/nvoi/objects/dns.rb +14 -0
  100. data/lib/nvoi/objects/firewall.rb +11 -0
  101. data/lib/nvoi/objects/network.rb +11 -0
  102. data/lib/nvoi/objects/server.rb +14 -0
  103. data/lib/nvoi/objects/service_spec.rb +26 -0
  104. data/lib/nvoi/objects/tunnel.rb +14 -0
  105. data/lib/nvoi/objects/volume.rb +17 -0
  106. data/lib/nvoi/utils/config_loader.rb +172 -0
  107. data/lib/nvoi/utils/constants.rb +61 -0
  108. data/lib/nvoi/{credentials/manager.rb → utils/credential_store.rb} +16 -16
  109. data/lib/nvoi/{credentials → utils}/crypto.rb +8 -5
  110. data/lib/nvoi/{config → utils}/env_resolver.rb +10 -2
  111. data/lib/nvoi/utils/logger.rb +84 -0
  112. data/lib/nvoi/{config/naming.rb → utils/namer.rb} +37 -25
  113. data/lib/nvoi/{deployer → utils}/retry.rb +23 -3
  114. data/lib/nvoi/utils/templates.rb +62 -0
  115. data/lib/nvoi/version.rb +1 -1
  116. data/lib/nvoi.rb +27 -55
  117. data/templates/app-ingress.yaml.erb +3 -1
  118. data/templates/error-backend.yaml.erb +134 -0
  119. metadata +121 -44
  120. data/examples/golang/deploy.yml +0 -54
  121. data/lib/nvoi/cloudflare/client.rb +0 -287
  122. data/lib/nvoi/config/config.rb +0 -248
  123. data/lib/nvoi/config/loader.rb +0 -102
  124. data/lib/nvoi/config/ssh_keys.rb +0 -82
  125. data/lib/nvoi/config/types.rb +0 -274
  126. data/lib/nvoi/constants.rb +0 -59
  127. data/lib/nvoi/credentials/editor.rb +0 -272
  128. data/lib/nvoi/deployer/cleaner.rb +0 -36
  129. data/lib/nvoi/deployer/image_builder.rb +0 -23
  130. data/lib/nvoi/deployer/infrastructure.rb +0 -126
  131. data/lib/nvoi/deployer/orchestrator.rb +0 -146
  132. data/lib/nvoi/deployer/service_deployer.rb +0 -311
  133. data/lib/nvoi/deployer/tunnel_manager.rb +0 -57
  134. data/lib/nvoi/deployer/types.rb +0 -8
  135. data/lib/nvoi/k8s/renderer.rb +0 -44
  136. data/lib/nvoi/k8s/templates.rb +0 -29
  137. data/lib/nvoi/logger.rb +0 -72
  138. data/lib/nvoi/providers/aws.rb +0 -403
  139. data/lib/nvoi/providers/base.rb +0 -111
  140. data/lib/nvoi/providers/hetzner.rb +0 -288
  141. data/lib/nvoi/providers/hetzner_client.rb +0 -170
  142. data/lib/nvoi/remote/docker_manager.rb +0 -203
  143. data/lib/nvoi/remote/ssh_executor.rb +0 -72
  144. data/lib/nvoi/remote/volume_manager.rb +0 -103
  145. data/lib/nvoi/service/delete.rb +0 -234
  146. data/lib/nvoi/service/deploy.rb +0 -80
  147. data/lib/nvoi/service/exec.rb +0 -144
  148. data/lib/nvoi/service/provider.rb +0 -36
  149. data/lib/nvoi/steps/application_deployer.rb +0 -26
  150. data/lib/nvoi/steps/database_provisioner.rb +0 -60
  151. data/lib/nvoi/steps/k3s_cluster_setup.rb +0 -105
  152. data/lib/nvoi/steps/k3s_provisioner.rb +0 -351
  153. data/lib/nvoi/steps/server_provisioner.rb +0 -43
  154. data/lib/nvoi/steps/services_provisioner.rb +0 -29
  155. data/lib/nvoi/steps/tunnel_configurator.rb +0 -66
  156. data/lib/nvoi/steps/volume_provisioner.rb +0 -154
@@ -187,6 +187,27 @@ components:
187
187
  minimum: 1
188
188
  default: 1
189
189
  example: 2
190
+ volumes:
191
+ type: object
192
+ description: Block storage volumes attached to this server (key is volume name)
193
+ additionalProperties:
194
+ $ref: "#/components/schemas/ServerVolumeConfig"
195
+ example:
196
+ database:
197
+ size: 20
198
+ uploads:
199
+ size: 10
200
+
201
+ ServerVolumeConfig:
202
+ type: object
203
+ description: Server-attached volume configuration
204
+ properties:
205
+ size:
206
+ type: integer
207
+ description: Volume size in GB
208
+ minimum: 10
209
+ default: 10
210
+ example: 20
190
211
 
191
212
  AppServiceConfig:
192
213
  type: object
@@ -227,13 +248,15 @@ components:
227
248
  description: Service-specific environment variables
228
249
  additionalProperties:
229
250
  type: string
230
- volumes:
251
+ mounts:
231
252
  type: object
232
- description: Volume mounts (key is volume name, value is mount path)
253
+ description: |
254
+ Mount server volumes into the container.
255
+ Key is volume name (must exist in server's volumes), value is container mount path.
256
+ Note: Services with mounts can only run on a single server.
233
257
  additionalProperties:
234
258
  type: string
235
259
  example:
236
- data: /app/data
237
260
  uploads: /app/public/uploads
238
261
 
239
262
  HealthCheckConfig:
@@ -294,10 +317,15 @@ components:
294
317
  type: string
295
318
  description: Docker image for database (managed databases only)
296
319
  example: postgres:15-alpine
297
- volume:
298
- type: string
299
- description: Volume mount path for data persistence
300
- example: /var/lib/postgresql/data
320
+ mount:
321
+ type: object
322
+ description: |
323
+ Mount a server volume for database data persistence.
324
+ Key is volume name (must exist in server's volumes), value is container mount path.
325
+ additionalProperties:
326
+ type: string
327
+ example:
328
+ database: /var/lib/postgresql/data
301
329
  secrets:
302
330
  type: object
303
331
  description: Database secrets (POSTGRES_PASSWORD, etc.)
@@ -331,10 +359,15 @@ components:
331
359
  description: Service environment variables
332
360
  additionalProperties:
333
361
  type: string
334
- volume:
335
- type: string
336
- description: Volume mount path for data persistence
337
- example: /data
362
+ mount:
363
+ type: object
364
+ description: |
365
+ Mount a server volume for data persistence.
366
+ Key is volume name (must exist in server's volumes), value is container mount path.
367
+ additionalProperties:
368
+ type: string
369
+ example:
370
+ redis_data: /data
338
371
 
339
372
  SSHKeyConfig:
340
373
  type: object
Binary file
@@ -97,6 +97,9 @@ func setupRouter() *gin.Engine {
97
97
  // Main endpoint: creates user on every visit, returns all users
98
98
  router.GET("/", handleVisit)
99
99
 
100
+ // Test custom error pages (nginx intercepts 502, 503, 504)
101
+ router.GET("/error/:code", handleError)
102
+
100
103
  return router
101
104
  }
102
105
 
@@ -164,6 +167,21 @@ func handleVisit(c *gin.Context) {
164
167
  })
165
168
  }
166
169
 
170
+ // Error handler for testing custom error pages (nginx intercepts 502, 503, 504)
171
+ func handleError(c *gin.Context) {
172
+ code := c.Param("code")
173
+ switch code {
174
+ case "502":
175
+ c.AbortWithStatus(http.StatusBadGateway)
176
+ case "503":
177
+ c.AbortWithStatus(http.StatusServiceUnavailable)
178
+ case "504":
179
+ c.AbortWithStatus(http.StatusGatewayTimeout)
180
+ default:
181
+ c.JSON(http.StatusOK, gin.H{"usage": "/error/{502,503,504}"})
182
+ }
183
+ }
184
+
167
185
  // Generate random name for demo purposes
168
186
  func generateRandomName() string {
169
187
  firstNames := []string{"Alice", "Bob", "Charlie", "Diana", "Eve", "Frank", "Grace", "Henry", "Ivy", "Jack"}
data/exe/nvoi CHANGED
@@ -1,6 +1,8 @@
1
1
  #!/usr/bin/env ruby
2
2
  # frozen_string_literal: true
3
3
 
4
+ $LOAD_PATH.unshift File.expand_path("../lib", __dir__)
4
5
  require "nvoi"
6
+ require "nvoi/cli"
5
7
 
6
- Nvoi::CLI.start(ARGV)
8
+ Nvoi::Cli.start(ARGV)
data/ingest ADDED
File without changes
@@ -0,0 +1,219 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nvoi
4
+ class Cli
5
+ module Config
6
+ # Command helper for all config operations
7
+ # Uses CredentialStore for crypto, ConfigApi for transformations
8
+ class Command
9
+ def initialize(options)
10
+ @options = options
11
+ @working_dir = options[:dir] || "."
12
+ end
13
+
14
+ # Initialize new config
15
+ def init(name, environment)
16
+ result = ConfigApi.init(name:, environment:)
17
+
18
+ if result.failure?
19
+ error("Failed to initialize: #{result.error_message}")
20
+ return
21
+ end
22
+
23
+ # Write encrypted config
24
+ config_path = File.join(@working_dir, Utils::DEFAULT_ENCRYPTED_FILE)
25
+ key_path = File.join(@working_dir, Utils::DEFAULT_KEY_FILE)
26
+
27
+ File.binwrite(config_path, result.config)
28
+ File.write(key_path, "#{result.master_key}\n", perm: 0o600)
29
+
30
+ update_gitignore
31
+
32
+ success("Created #{Utils::DEFAULT_ENCRYPTED_FILE}")
33
+ success("Created #{Utils::DEFAULT_KEY_FILE} (keep safe, never commit)")
34
+ puts
35
+ puts "Next steps:"
36
+ puts " nvoi config domain set cloudflare --api-token=TOKEN --account-id=ID"
37
+ puts " nvoi config provider set hetzner --api-token=TOKEN --server-type=cx22 --location=fsn1"
38
+ puts " nvoi config server set web --master"
39
+ end
40
+
41
+ # Domain provider
42
+ def domain_set(provider, api_token:, account_id:)
43
+ with_config do |data|
44
+ ConfigApi.set_domain_provider(data, provider:, api_token:, account_id:)
45
+ end
46
+ end
47
+
48
+ def domain_rm
49
+ with_config do |data|
50
+ ConfigApi.delete_domain_provider(data)
51
+ end
52
+ end
53
+
54
+ # Compute provider
55
+ def provider_set(provider, **opts)
56
+ with_config do |data|
57
+ ConfigApi.set_compute_provider(data, provider:, **opts)
58
+ end
59
+ end
60
+
61
+ def provider_rm
62
+ with_config do |data|
63
+ ConfigApi.delete_compute_provider(data)
64
+ end
65
+ end
66
+
67
+ # Server
68
+ def server_set(name, master: false, type: nil, location: nil, count: 1)
69
+ with_config do |data|
70
+ ConfigApi.set_server(data, name:, master:, type:, location:, count:)
71
+ end
72
+ end
73
+
74
+ def server_rm(name)
75
+ with_config do |data|
76
+ ConfigApi.delete_server(data, name:)
77
+ end
78
+ end
79
+
80
+ # Volume
81
+ def volume_set(server, name, size: 10)
82
+ with_config do |data|
83
+ ConfigApi.set_volume(data, server:, name:, size:)
84
+ end
85
+ end
86
+
87
+ def volume_rm(server, name)
88
+ with_config do |data|
89
+ ConfigApi.delete_volume(data, server:, name:)
90
+ end
91
+ end
92
+
93
+ # App
94
+ def app_set(name, servers:, **opts)
95
+ with_config do |data|
96
+ ConfigApi.set_app(data, name:, servers:, **opts)
97
+ end
98
+ end
99
+
100
+ def app_rm(name)
101
+ with_config do |data|
102
+ ConfigApi.delete_app(data, name:)
103
+ end
104
+ end
105
+
106
+ # Database
107
+ def database_set(servers:, adapter:, **opts)
108
+ with_config do |data|
109
+ ConfigApi.set_database(data, servers:, adapter:, **opts)
110
+ end
111
+ end
112
+
113
+ def database_rm
114
+ with_config do |data|
115
+ ConfigApi.delete_database(data)
116
+ end
117
+ end
118
+
119
+ # Service
120
+ def service_set(name, servers:, image:, **opts)
121
+ with_config do |data|
122
+ ConfigApi.set_service(data, name:, servers:, image:, **opts)
123
+ end
124
+ end
125
+
126
+ def service_rm(name)
127
+ with_config do |data|
128
+ ConfigApi.delete_service(data, name:)
129
+ end
130
+ end
131
+
132
+ # Secret
133
+ def secret_set(key_name, value)
134
+ with_config do |data|
135
+ ConfigApi.set_secret(data, key: key_name, value:)
136
+ end
137
+ end
138
+
139
+ def secret_rm(key_name)
140
+ with_config do |data|
141
+ ConfigApi.delete_secret(data, key: key_name)
142
+ end
143
+ end
144
+
145
+ # Env
146
+ def env_set(key_name, value)
147
+ with_config do |data|
148
+ ConfigApi.set_env(data, key: key_name, value:)
149
+ end
150
+ end
151
+
152
+ def env_rm(key_name)
153
+ with_config do |data|
154
+ ConfigApi.delete_env(data, key: key_name)
155
+ end
156
+ end
157
+
158
+ private
159
+
160
+ def with_config
161
+ store = Utils::CredentialStore.new(
162
+ @working_dir,
163
+ @options[:credentials],
164
+ @options[:master_key]
165
+ )
166
+
167
+ unless store.exists?
168
+ error("Config not found: #{store.encrypted_path}")
169
+ error("Run 'nvoi config init --name=myapp' first")
170
+ return
171
+ end
172
+
173
+ # Read and parse
174
+ yaml = store.read
175
+ data = YAML.safe_load(yaml, permitted_classes: [Symbol])
176
+
177
+ # Transform
178
+ result = yield(data)
179
+
180
+ if result.failure?
181
+ error("#{result.error_type}: #{result.error_message}")
182
+ else
183
+ # Serialize and write
184
+ store.write(YAML.dump(result.data))
185
+ success("Config updated")
186
+ end
187
+ rescue Errors::CredentialError => e
188
+ error(e.message)
189
+ rescue Errors::DecryptionError => e
190
+ error("Decryption failed: #{e.message}")
191
+ end
192
+
193
+ def update_gitignore
194
+ gitignore_path = File.join(@working_dir, ".gitignore")
195
+ entries = ["deploy.key", ".env", ".env.*", "!.env.example", "!.env.*.example"]
196
+
197
+ existing = File.exist?(gitignore_path) ? File.read(gitignore_path) : ""
198
+ additions = entries.reject { |e| existing.include?(e) }
199
+
200
+ return if additions.empty?
201
+
202
+ File.open(gitignore_path, "a") do |f|
203
+ f.puts "" unless existing.end_with?("\n") || existing.empty?
204
+ f.puts "# NVOI - sensitive files"
205
+ additions.each { |e| f.puts e }
206
+ end
207
+ end
208
+
209
+ def success(msg)
210
+ puts "\e[32m✓\e[0m #{msg}"
211
+ end
212
+
213
+ def error(msg)
214
+ warn "\e[31m✗\e[0m #{msg}"
215
+ end
216
+ end
217
+ end
218
+ end
219
+ end