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,420 @@
1
+ # frozen_string_literal: true
2
+
3
+
4
+ module Nvoi
5
+ module Configuration
6
+ # Builder for constructing and modifying config data hashes
7
+ # Replaces ConfigApi with a cleaner, chainable interface
8
+ class Builder
9
+ COMPUTE_PROVIDERS = %w[hetzner aws scaleway].freeze
10
+ DOMAIN_PROVIDERS = %w[cloudflare].freeze
11
+ DATABASE_ADAPTERS = %w[postgres postgresql mysql sqlite sqlite3].freeze
12
+
13
+ attr_reader :data
14
+
15
+ def initialize(data = nil)
16
+ @data = data || { "application" => {} }
17
+ end
18
+
19
+ # ─── Class Methods ───
20
+
21
+ def self.from_hash(data)
22
+ new(data)
23
+ end
24
+
25
+ def self.init(name:, environment: "production")
26
+ raise ArgumentError, "name is required" if name.blank?
27
+
28
+ master_key = Utils::Crypto.generate_key
29
+ private_key, public_key = Utils::ConfigLoader.generate_keypair
30
+
31
+ builder = new
32
+ builder.name(name)
33
+ builder.environment(environment)
34
+ builder.ssh_keys(private_key, public_key)
35
+
36
+ yaml = YAML.dump(builder.to_h)
37
+ encrypted_config = Utils::Crypto.encrypt(yaml, master_key)
38
+
39
+ Result::Init.new(
40
+ config: encrypted_config,
41
+ master_key:,
42
+ ssh_public_key: public_key
43
+ )
44
+ rescue ArgumentError => e
45
+ Result::Init.new(error_type: :invalid_args, error_message: e.message)
46
+ rescue Errors::ConfigError => e
47
+ Result::Init.new(error_type: :config_error, error_message: e.message)
48
+ end
49
+
50
+ # ─── Basic Setters ───
51
+
52
+ def name(n)
53
+ app["name"] = n.to_s
54
+ self
55
+ end
56
+
57
+ def environment(e)
58
+ app["environment"] = e.to_s
59
+ self
60
+ end
61
+
62
+ def ssh_keys(private_key, public_key)
63
+ app["ssh_keys"] = {
64
+ "private_key" => private_key,
65
+ "public_key" => public_key
66
+ }
67
+ self
68
+ end
69
+
70
+ # ─── Compute Provider ───
71
+
72
+ def compute_provider(provider, **opts)
73
+ validate_presence!(provider, "provider")
74
+ validate_inclusion!(provider.to_s, COMPUTE_PROVIDERS, "provider")
75
+
76
+ app["compute_provider"] = { provider.to_s => build_compute_config(provider.to_s, opts) }
77
+ wrap_success
78
+ end
79
+
80
+ def remove_compute_provider
81
+ app["compute_provider"] = {}
82
+ wrap_success
83
+ end
84
+
85
+ # ─── Domain Provider ───
86
+
87
+ def domain_provider(provider, **opts)
88
+ validate_presence!(provider, "provider")
89
+ validate_inclusion!(provider.to_s, DOMAIN_PROVIDERS, "provider")
90
+
91
+ app["domain_provider"] = { provider.to_s => build_domain_config(provider.to_s, opts) }
92
+ wrap_success
93
+ end
94
+
95
+ def remove_domain_provider
96
+ app["domain_provider"] = {}
97
+ wrap_success
98
+ end
99
+
100
+ # ─── Server ───
101
+
102
+ def server(name, master: false, type: nil, location: nil, count: 1)
103
+ validate_presence!(name, "name")
104
+ validate_positive!(count, "count") if count
105
+
106
+ servers[name.to_s] = {
107
+ "master" => master,
108
+ "type" => type,
109
+ "location" => location,
110
+ "count" => count
111
+ }.compact
112
+ wrap_success
113
+ end
114
+
115
+ def remove_server(name)
116
+ validate_presence!(name, "name")
117
+ validate_exists!(servers, name.to_s, "server")
118
+ check_server_references(name.to_s)
119
+
120
+ servers.delete(name.to_s)
121
+ wrap_success
122
+ end
123
+
124
+ # ─── Volume ───
125
+
126
+ def volume(server_name, name, size: 10)
127
+ validate_presence!(server_name, "server")
128
+ validate_presence!(name, "name")
129
+ validate_positive!(size, "size") if size
130
+ validate_exists!(servers, server_name.to_s, "server")
131
+
132
+ servers[server_name.to_s]["volumes"] ||= {}
133
+ servers[server_name.to_s]["volumes"][name.to_s] = { "size" => size }
134
+ wrap_success
135
+ end
136
+
137
+ def remove_volume(server_name, name)
138
+ validate_presence!(server_name, "server")
139
+ validate_presence!(name, "name")
140
+ validate_exists!(servers, server_name.to_s, "server")
141
+
142
+ volumes = servers[server_name.to_s]["volumes"] || {}
143
+ validate_exists!(volumes, name.to_s, "volume")
144
+
145
+ volumes.delete(name.to_s)
146
+ wrap_success
147
+ end
148
+
149
+ # ─── App ───
150
+
151
+ def app_entry(name, servers:, domain: nil, subdomain: nil, port: nil, command: nil, pre_run_command: nil, env: nil, mounts: nil)
152
+ validate_presence!(name, "name")
153
+ validate_servers_array!(servers)
154
+ validate_server_refs!(servers)
155
+
156
+ apps[name.to_s] = {
157
+ "servers" => servers.map(&:to_s),
158
+ "domain" => domain,
159
+ "subdomain" => subdomain,
160
+ "port" => port,
161
+ "command" => command,
162
+ "pre_run_command" => pre_run_command,
163
+ "env" => env,
164
+ "mounts" => mounts
165
+ }.compact
166
+ wrap_success
167
+ end
168
+
169
+ def remove_app(name)
170
+ validate_presence!(name, "name")
171
+ validate_exists!(apps, name.to_s, "app")
172
+
173
+ apps.delete(name.to_s)
174
+ wrap_success
175
+ end
176
+
177
+ # ─── Database ───
178
+
179
+ def database(servers:, adapter:, image: nil, url: nil, user: nil, password: nil, database_name: nil, mount: nil, path: nil)
180
+ validate_servers_array!(servers)
181
+ validate_presence!(adapter, "adapter")
182
+ validate_inclusion!(adapter.to_s.downcase, DATABASE_ADAPTERS, "adapter")
183
+ validate_server_refs!(servers)
184
+
185
+ secrets = build_database_secrets(adapter, user, password, database_name)
186
+
187
+ app["database"] = {
188
+ "servers" => servers.map(&:to_s),
189
+ "adapter" => adapter.to_s,
190
+ "image" => image,
191
+ "url" => url,
192
+ "secrets" => secrets.empty? ? nil : secrets,
193
+ "mount" => mount,
194
+ "path" => path
195
+ }.compact
196
+ wrap_success
197
+ end
198
+
199
+ def remove_database
200
+ app.delete("database")
201
+ wrap_success
202
+ end
203
+
204
+ # ─── Service ───
205
+
206
+ def service(name, servers:, image:, port: nil, command: nil, env: nil, mount: nil)
207
+ validate_presence!(name, "name")
208
+ validate_servers_array!(servers)
209
+ validate_presence!(image, "image")
210
+ validate_server_refs!(servers)
211
+
212
+ services[name.to_s] = {
213
+ "servers" => servers.map(&:to_s),
214
+ "image" => image.to_s,
215
+ "port" => port,
216
+ "command" => command,
217
+ "env" => env,
218
+ "mount" => mount
219
+ }.compact
220
+ wrap_success
221
+ end
222
+
223
+ def remove_service(name)
224
+ validate_presence!(name, "name")
225
+ validate_exists!(services, name.to_s, "service")
226
+
227
+ services.delete(name.to_s)
228
+ wrap_success
229
+ end
230
+
231
+ # ─── Secret ───
232
+
233
+ def secret(key, value)
234
+ validate_presence!(key, "key")
235
+ raise ArgumentError, "value is required" if value.nil?
236
+
237
+ secrets[key.to_s] = value.to_s
238
+ wrap_success
239
+ end
240
+
241
+ def remove_secret(key)
242
+ validate_presence!(key, "key")
243
+ validate_exists!(secrets, key.to_s, "secret")
244
+
245
+ secrets.delete(key.to_s)
246
+ wrap_success
247
+ end
248
+
249
+ # ─── Env ───
250
+
251
+ def env(key, value)
252
+ validate_presence!(key, "key")
253
+ raise ArgumentError, "value is required" if value.nil?
254
+
255
+ env_vars[key.to_s] = value.to_s
256
+ wrap_success
257
+ end
258
+
259
+ def remove_env(key)
260
+ validate_presence!(key, "key")
261
+ validate_exists!(env_vars, key.to_s, "env")
262
+
263
+ env_vars.delete(key.to_s)
264
+ wrap_success
265
+ end
266
+
267
+ # ─── Output ───
268
+
269
+ def to_h
270
+ @data
271
+ end
272
+
273
+ def to_yaml
274
+ YAML.dump(@data)
275
+ end
276
+
277
+ private
278
+
279
+ def app
280
+ @data["application"] ||= {}
281
+ end
282
+
283
+ def servers
284
+ app["servers"] ||= {}
285
+ end
286
+
287
+ def apps
288
+ app["app"] ||= {}
289
+ end
290
+
291
+ def services
292
+ app["services"] ||= {}
293
+ end
294
+
295
+ def secrets
296
+ app["secrets"] ||= {}
297
+ end
298
+
299
+ def env_vars
300
+ app["env"] ||= {}
301
+ end
302
+
303
+ # ─── Validation Helpers ───
304
+
305
+ def validate_presence!(value, field)
306
+ raise ArgumentError, "#{field} is required" if value.blank?
307
+ end
308
+
309
+ def validate_inclusion!(value, list, field)
310
+ raise ArgumentError, "#{field} must be one of: #{list.join(', ')}" unless list.include?(value)
311
+ end
312
+
313
+ def validate_positive!(value, field)
314
+ raise ArgumentError, "#{field} must be positive" if value && value < 1
315
+ end
316
+
317
+ def validate_exists!(hash, key, type)
318
+ raise Errors::ConfigValidationError, "#{type} '#{key}' not found" unless hash.key?(key)
319
+ end
320
+
321
+ def validate_servers_array!(server_refs)
322
+ raise ArgumentError, "servers is required" if server_refs.to_a.empty?
323
+ raise ArgumentError, "servers must be an array" unless server_refs.is_a?(Array)
324
+ end
325
+
326
+ def validate_server_refs!(server_refs)
327
+ defined = servers.keys
328
+ server_refs.each do |ref|
329
+ raise Errors::ConfigValidationError, "server '#{ref}' not found" unless defined.include?(ref.to_s)
330
+ end
331
+ end
332
+
333
+ def check_server_references(server_name)
334
+ apps.each do |app_name, cfg|
335
+ if (cfg["servers"] || []).include?(server_name)
336
+ raise Errors::ConfigValidationError, "app.#{app_name} references server '#{server_name}'"
337
+ end
338
+ end
339
+
340
+ db = app["database"]
341
+ if db && (db["servers"] || []).include?(server_name)
342
+ raise Errors::ConfigValidationError, "database references server '#{server_name}'"
343
+ end
344
+
345
+ services.each do |svc_name, cfg|
346
+ if (cfg["servers"] || []).include?(server_name)
347
+ raise Errors::ConfigValidationError, "services.#{svc_name} references server '#{server_name}'"
348
+ end
349
+ end
350
+ end
351
+
352
+ # ─── Config Builders ───
353
+
354
+ def build_compute_config(provider, opts)
355
+ case provider
356
+ when "hetzner"
357
+ {
358
+ "api_token" => opts[:api_token],
359
+ "server_type" => opts[:server_type],
360
+ "server_location" => opts[:server_location],
361
+ "architecture" => opts[:architecture]
362
+ }.compact
363
+ when "aws"
364
+ {
365
+ "access_key_id" => opts[:access_key_id],
366
+ "secret_access_key" => opts[:secret_access_key],
367
+ "region" => opts[:region],
368
+ "instance_type" => opts[:instance_type],
369
+ "architecture" => opts[:architecture]
370
+ }.compact
371
+ when "scaleway"
372
+ {
373
+ "secret_key" => opts[:secret_key],
374
+ "project_id" => opts[:project_id],
375
+ "zone" => opts[:zone],
376
+ "server_type" => opts[:server_type],
377
+ "architecture" => opts[:architecture]
378
+ }.compact
379
+ end
380
+ end
381
+
382
+ def build_domain_config(provider, opts)
383
+ case provider
384
+ when "cloudflare"
385
+ {
386
+ "api_token" => opts[:api_token],
387
+ "account_id" => opts[:account_id]
388
+ }.compact
389
+ end
390
+ end
391
+
392
+ def build_database_secrets(adapter, user, password, database_name)
393
+ case adapter.to_s.downcase
394
+ when "postgres", "postgresql"
395
+ {
396
+ "POSTGRES_USER" => user,
397
+ "POSTGRES_PASSWORD" => password,
398
+ "POSTGRES_DB" => database_name
399
+ }.compact
400
+ when "mysql"
401
+ {
402
+ "MYSQL_USER" => user,
403
+ "MYSQL_PASSWORD" => password,
404
+ "MYSQL_DATABASE" => database_name
405
+ }.compact
406
+ else
407
+ {}
408
+ end
409
+ end
410
+
411
+ def wrap_success
412
+ Result.success(@data)
413
+ end
414
+
415
+ def wrap_failure(type, message)
416
+ Result.failure(type, message)
417
+ end
418
+ end
419
+ end
420
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nvoi
4
+ module Configuration
5
+ # Database defines database configuration
6
+ class Database
7
+ attr_accessor :servers, :adapter, :url, :image, :mount, :secrets, :path
8
+
9
+ def initialize(data = nil)
10
+ data ||= {}
11
+ @servers = data["servers"] || []
12
+ @adapter = data["adapter"]
13
+ @url = data["url"]
14
+ @image = data["image"]
15
+ @mount = data["mount"] || {}
16
+ @secrets = data["secrets"] || {}
17
+ @path = data["path"]
18
+ end
19
+
20
+ def postgres?
21
+ @adapter&.downcase&.start_with?("postgres")
22
+ end
23
+
24
+ def mysql?
25
+ @adapter&.downcase == "mysql"
26
+ end
27
+
28
+ def sqlite?
29
+ @adapter&.downcase&.start_with?("sqlite")
30
+ end
31
+
32
+ def to_service_spec(namer)
33
+ return nil if @adapter&.downcase&.start_with?("sqlite")
34
+
35
+ port = case @adapter&.downcase
36
+ when "mysql" then 3306
37
+ else 5432
38
+ end
39
+
40
+ image = @image || Utils::Constants::DATABASE_IMAGES[@adapter&.downcase]
41
+
42
+ Configuration::Deployment.new(
43
+ name: namer.database_service_name,
44
+ image:,
45
+ port:,
46
+ env: nil,
47
+ mounts: @mount,
48
+ replicas: 1,
49
+ stateful_set: true,
50
+ secrets: @secrets,
51
+ servers: @servers
52
+ )
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nvoi
4
+ module Configuration
5
+ # Deploy represents the root deployment configuration
6
+ class Deploy
7
+ attr_accessor :application
8
+
9
+ def initialize(data = nil)
10
+ data ||= {}
11
+ @application = Application.new(data["application"])
12
+ end
13
+ end
14
+ end
15
+ end
@@ -1,9 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Nvoi
4
- module Objects
5
- # ServiceSpec is the CORE primitive - pure K8s deployment specification
6
- class ServiceSpec
4
+ module Configuration
5
+ # Deployment is the normalized service definition ready for deployment
6
+ # Created by Configuration::Database and Configuration::Service
7
+ class Deployment
7
8
  attr_accessor :name, :image, :port, :command, :env, :mounts, :replicas,
8
9
  :healthcheck, :stateful_set, :secrets, :servers
9
10
 
@@ -1,9 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Nvoi
4
- module Objects
5
- # ConfigOverride allows CLI to override app name and subdomain for branch deployments
6
- class ConfigOverride
4
+ module Configuration
5
+ # Override allows CLI to override app name and subdomain for branch deployments
6
+ class Override
7
7
  BRANCH_PATTERN = /\A[a-z0-9-]+\z/
8
8
 
9
9
  attr_reader :branch
@@ -32,7 +32,7 @@ module Nvoi
32
32
  private
33
33
 
34
34
  def validate!(branch)
35
- raise ArgumentError, "--branch value required" unless branch && !branch.empty?
35
+ raise ArgumentError, "--branch value required" if branch.blank?
36
36
  raise ArgumentError, "invalid branch format (lowercase alphanumeric and hyphens only)" unless branch.match?(BRANCH_PATTERN)
37
37
  end
38
38
 
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nvoi
4
+ module Configuration
5
+ module Providers
6
+ # DomainProvider contains domain provider configuration
7
+ class DomainProvider
8
+ attr_accessor :cloudflare
9
+
10
+ def initialize(data = nil)
11
+ data ||= {}
12
+ @cloudflare = data["cloudflare"] ? Cloudflare.new(data["cloudflare"]) : nil
13
+ end
14
+ end
15
+
16
+ # ComputeProvider contains compute provider configuration
17
+ class ComputeProvider
18
+ attr_accessor :hetzner, :aws, :scaleway
19
+
20
+ def initialize(data = nil)
21
+ data ||= {}
22
+ @hetzner = data["hetzner"] ? Hetzner.new(data["hetzner"]) : nil
23
+ @aws = data["aws"] ? AwsCfg.new(data["aws"]) : nil
24
+ @scaleway = data["scaleway"] ? Scaleway.new(data["scaleway"]) : nil
25
+ end
26
+ end
27
+
28
+ # Cloudflare contains Cloudflare-specific configuration
29
+ class Cloudflare
30
+ attr_accessor :api_token, :account_id
31
+
32
+ def initialize(data = nil)
33
+ data ||= {}
34
+ @api_token = data["api_token"]
35
+ @account_id = data["account_id"]
36
+ end
37
+ end
38
+
39
+ # Hetzner contains Hetzner-specific configuration
40
+ class Hetzner
41
+ attr_accessor :api_token, :server_type, :server_location, :architecture
42
+
43
+ def initialize(data = nil)
44
+ data ||= {}
45
+ @api_token = data["api_token"]
46
+ @server_type = data["server_type"]
47
+ @server_location = data["server_location"]
48
+ @architecture = data["architecture"]
49
+ end
50
+ end
51
+
52
+ # AwsCfg contains AWS-specific configuration
53
+ class AwsCfg
54
+ attr_accessor :access_key_id, :secret_access_key, :region, :instance_type, :architecture
55
+
56
+ def initialize(data = nil)
57
+ data ||= {}
58
+ @access_key_id = data["access_key_id"]
59
+ @secret_access_key = data["secret_access_key"]
60
+ @region = data["region"]
61
+ @instance_type = data["instance_type"]
62
+ @architecture = data["architecture"]
63
+ end
64
+ end
65
+
66
+ # Scaleway contains Scaleway-specific configuration
67
+ class Scaleway
68
+ attr_accessor :secret_key, :project_id, :zone, :server_type, :architecture
69
+
70
+ def initialize(data = nil)
71
+ data ||= {}
72
+ @secret_key = data["secret_key"]
73
+ @project_id = data["project_id"]
74
+ @zone = data["zone"] || "fr-par-1"
75
+ @server_type = data["server_type"]
76
+ @architecture = data["architecture"]
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nvoi
4
+ module Configuration
5
+ # Result wrapper for Builder operations
6
+ class Result
7
+ attr_reader :data, :error_type, :error_message
8
+
9
+ def initialize(data: nil, error_type: nil, error_message: nil)
10
+ @data = data
11
+ @error_type = error_type
12
+ @error_message = error_message
13
+ end
14
+
15
+ def success? = @error_type.nil?
16
+ def failure? = !success?
17
+
18
+ def self.success(data)
19
+ new(data:)
20
+ end
21
+
22
+ def self.failure(type, message)
23
+ new(error_type: type, error_message: message)
24
+ end
25
+
26
+ # Result for init operations (includes encryption artifacts)
27
+ class Init
28
+ attr_reader :config, :master_key, :ssh_public_key, :error_type, :error_message
29
+
30
+ def initialize(config: nil, master_key: nil, ssh_public_key: nil, error_type: nil, error_message: nil)
31
+ @config = config
32
+ @master_key = master_key
33
+ @ssh_public_key = ssh_public_key
34
+ @error_type = error_type
35
+ @error_message = error_message
36
+ end
37
+
38
+ def success? = @error_type.nil?
39
+ def failure? = !success?
40
+ end
41
+ end
42
+ end
43
+ end