nvoi 0.1.8 → 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.
Files changed (110) 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/lib/nvoi/cli/config/command.rb +46 -41
  6. data/lib/nvoi/cli/credentials/edit/command.rb +20 -20
  7. data/lib/nvoi/cli/credentials/show/command.rb +1 -1
  8. data/lib/nvoi/cli/db/command.rb +10 -10
  9. data/lib/nvoi/cli/delete/command.rb +2 -2
  10. data/lib/nvoi/cli/deploy/command.rb +2 -2
  11. data/lib/nvoi/cli/deploy/steps/configure_tunnel.rb +2 -2
  12. data/lib/nvoi/cli/deploy/steps/provision_server.rb +1 -1
  13. data/lib/nvoi/cli/deploy/steps/provision_volume.rb +1 -1
  14. data/lib/nvoi/cli/exec/command.rb +3 -3
  15. data/lib/nvoi/cli/logs/command.rb +2 -2
  16. data/lib/nvoi/cli/onboard/command.rb +176 -622
  17. data/lib/nvoi/cli/onboard/steps/app.rb +108 -0
  18. data/lib/nvoi/cli/onboard/steps/app_name.rb +26 -0
  19. data/lib/nvoi/cli/onboard/steps/compute.rb +139 -0
  20. data/lib/nvoi/cli/onboard/steps/database.rb +97 -0
  21. data/lib/nvoi/cli/onboard/steps/domain.rb +48 -0
  22. data/lib/nvoi/cli/onboard/steps/env.rb +67 -0
  23. data/lib/nvoi/cli/onboard/ui.rb +84 -0
  24. data/lib/nvoi/cli/unlock/command.rb +2 -2
  25. data/lib/nvoi/cli.rb +0 -32
  26. data/lib/nvoi/configuration/app_service.rb +54 -0
  27. data/lib/nvoi/configuration/application.rb +44 -0
  28. data/lib/nvoi/configuration/builder.rb +417 -0
  29. data/lib/nvoi/configuration/database.rb +56 -0
  30. data/lib/nvoi/configuration/deploy.rb +15 -0
  31. data/lib/nvoi/{objects/service_spec.rb → configuration/deployment.rb} +4 -3
  32. data/lib/nvoi/{objects/config_override.rb → configuration/override.rb} +4 -4
  33. data/lib/nvoi/configuration/providers.rb +78 -0
  34. data/lib/nvoi/configuration/result.rb +43 -0
  35. data/lib/nvoi/configuration/root.rb +234 -0
  36. data/lib/nvoi/configuration/server.rb +39 -0
  37. data/lib/nvoi/configuration/service.rb +62 -0
  38. data/lib/nvoi/external/cloud/aws.rb +12 -12
  39. data/lib/nvoi/external/cloud/hetzner.rb +7 -7
  40. data/lib/nvoi/external/cloud/scaleway.rb +7 -7
  41. data/lib/nvoi/external/cloud/types.rb +42 -0
  42. data/lib/nvoi/external/database/mysql.rb +1 -1
  43. data/lib/nvoi/external/database/postgres.rb +1 -1
  44. data/lib/nvoi/external/database/provider.rb +1 -1
  45. data/lib/nvoi/external/database/sqlite.rb +1 -1
  46. data/lib/nvoi/external/database/types.rb +55 -0
  47. data/lib/nvoi/external/dns/cloudflare.rb +6 -6
  48. data/lib/nvoi/external/dns/types.rb +24 -0
  49. data/lib/nvoi/utils/config_loader.rb +12 -12
  50. data/lib/nvoi/utils/credential_store.rb +4 -4
  51. data/lib/nvoi/utils/env_resolver.rb +3 -3
  52. data/lib/nvoi/utils/namer.rb +2 -2
  53. data/lib/nvoi/utils/presence.rb +23 -0
  54. data/lib/nvoi/version.rb +1 -1
  55. data/lib/nvoi.rb +2 -17
  56. metadata +95 -58
  57. data/.claude/todo/refactor/00-overview.md +0 -171
  58. data/.claude/todo/refactor/01-objects.md +0 -96
  59. data/.claude/todo/refactor/02-utils.md +0 -143
  60. data/.claude/todo/refactor/03-external-cloud.md +0 -164
  61. data/.claude/todo/refactor/04-external-dns.md +0 -104
  62. data/.claude/todo/refactor/05-external.md +0 -133
  63. data/.claude/todo/refactor/06-cli.md +0 -123
  64. data/.claude/todo/refactor/07-cli-deploy-command.md +0 -177
  65. data/.claude/todo/refactor/08-cli-deploy-steps.md +0 -201
  66. data/.claude/todo/refactor/09-cli-delete-command.md +0 -169
  67. data/.claude/todo/refactor/10-cli-exec-command.md +0 -157
  68. data/.claude/todo/refactor/11-cli-credentials-command.md +0 -190
  69. data/.claude/todo/refactor/12-cli-db-command.md +0 -128
  70. data/.claude/todo/refactor/_target.md +0 -79
  71. data/.claude/todo/refactor-execution/00-entrypoint.md +0 -49
  72. data/.claude/todo/refactor-execution/01-objects.md +0 -42
  73. data/.claude/todo/refactor-execution/02-utils.md +0 -41
  74. data/.claude/todo/refactor-execution/03-external-cloud.md +0 -38
  75. data/.claude/todo/refactor-execution/04-external-dns.md +0 -35
  76. data/.claude/todo/refactor-execution/05-external-other.md +0 -46
  77. data/.claude/todo/refactor-execution/06-cli-deploy.md +0 -45
  78. data/.claude/todo/refactor-execution/07-cli-delete.md +0 -43
  79. data/.claude/todo/refactor-execution/08-cli-exec.md +0 -30
  80. data/.claude/todo/refactor-execution/09-cli-credentials.md +0 -34
  81. data/.claude/todo/refactor-execution/10-cli-db.md +0 -31
  82. data/.claude/todo/refactor-execution/11-cli-router.md +0 -44
  83. data/.claude/todo/refactor-execution/12-cleanup.md +0 -120
  84. data/.claude/todo/refactor-execution/_monitoring-strategy.md +0 -126
  85. data/.claude/todo/scaleway.impl.md +0 -644
  86. data/.claude/todo/scaleway.reference.md +0 -520
  87. data/.claude/todos/buckets.md +0 -41
  88. data/.claude/todos.md +0 -550
  89. data/ingest +0 -0
  90. data/lib/nvoi/config_api/actions/app.rb +0 -53
  91. data/lib/nvoi/config_api/actions/compute_provider.rb +0 -55
  92. data/lib/nvoi/config_api/actions/database.rb +0 -70
  93. data/lib/nvoi/config_api/actions/domain_provider.rb +0 -40
  94. data/lib/nvoi/config_api/actions/env.rb +0 -32
  95. data/lib/nvoi/config_api/actions/init.rb +0 -67
  96. data/lib/nvoi/config_api/actions/secret.rb +0 -32
  97. data/lib/nvoi/config_api/actions/server.rb +0 -66
  98. data/lib/nvoi/config_api/actions/service.rb +0 -52
  99. data/lib/nvoi/config_api/actions/volume.rb +0 -40
  100. data/lib/nvoi/config_api/base.rb +0 -38
  101. data/lib/nvoi/config_api/result.rb +0 -26
  102. data/lib/nvoi/config_api.rb +0 -93
  103. data/lib/nvoi/objects/configuration.rb +0 -483
  104. data/lib/nvoi/objects/database.rb +0 -56
  105. data/lib/nvoi/objects/dns.rb +0 -14
  106. data/lib/nvoi/objects/firewall.rb +0 -11
  107. data/lib/nvoi/objects/network.rb +0 -11
  108. data/lib/nvoi/objects/server.rb +0 -14
  109. data/lib/nvoi/objects/tunnel.rb +0 -14
  110. data/lib/nvoi/objects/volume.rb +0 -17
@@ -0,0 +1,417 @@
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
+ }.compact
362
+ when "aws"
363
+ {
364
+ "access_key_id" => opts[:access_key_id],
365
+ "secret_access_key" => opts[:secret_access_key],
366
+ "region" => opts[:region],
367
+ "instance_type" => opts[:instance_type]
368
+ }.compact
369
+ when "scaleway"
370
+ {
371
+ "secret_key" => opts[:secret_key],
372
+ "project_id" => opts[:project_id],
373
+ "zone" => opts[:zone],
374
+ "server_type" => opts[:server_type]
375
+ }.compact
376
+ end
377
+ end
378
+
379
+ def build_domain_config(provider, opts)
380
+ case provider
381
+ when "cloudflare"
382
+ {
383
+ "api_token" => opts[:api_token],
384
+ "account_id" => opts[:account_id]
385
+ }.compact
386
+ end
387
+ end
388
+
389
+ def build_database_secrets(adapter, user, password, database_name)
390
+ case adapter.to_s.downcase
391
+ when "postgres", "postgresql"
392
+ {
393
+ "POSTGRES_USER" => user,
394
+ "POSTGRES_PASSWORD" => password,
395
+ "POSTGRES_DB" => database_name
396
+ }.compact
397
+ when "mysql"
398
+ {
399
+ "MYSQL_USER" => user,
400
+ "MYSQL_PASSWORD" => password,
401
+ "MYSQL_DATABASE" => database_name
402
+ }.compact
403
+ else
404
+ {}
405
+ end
406
+ end
407
+
408
+ def wrap_success
409
+ Result.success(@data)
410
+ end
411
+
412
+ def wrap_failure(type, message)
413
+ Result.failure(type, message)
414
+ end
415
+ end
416
+ end
417
+ 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,78 @@
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
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
+ end
49
+ end
50
+
51
+ # AwsCfg contains AWS-specific configuration
52
+ class AwsCfg
53
+ attr_accessor :access_key_id, :secret_access_key, :region, :instance_type
54
+
55
+ def initialize(data = nil)
56
+ data ||= {}
57
+ @access_key_id = data["access_key_id"]
58
+ @secret_access_key = data["secret_access_key"]
59
+ @region = data["region"]
60
+ @instance_type = data["instance_type"]
61
+ end
62
+ end
63
+
64
+ # Scaleway contains Scaleway-specific configuration
65
+ class Scaleway
66
+ attr_accessor :secret_key, :project_id, :zone, :server_type
67
+
68
+ def initialize(data = nil)
69
+ data ||= {}
70
+ @secret_key = data["secret_key"]
71
+ @project_id = data["project_id"]
72
+ @zone = data["zone"] || "fr-par-1"
73
+ @server_type = data["server_type"]
74
+ end
75
+ end
76
+ end
77
+ end
78
+ 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