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,234 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nvoi
4
+ module Configuration
5
+ # Root holds the complete configuration including deployment config and runtime settings
6
+ class Root
7
+ attr_accessor :deploy, :ssh_key_path, :ssh_public_key, :server_name,
8
+ :firewall_name, :network_name, :docker_network_name, :container_prefix, :namer
9
+
10
+ def initialize(deploy_config)
11
+ @deploy = deploy_config
12
+ @namer = nil
13
+ end
14
+
15
+ def namer
16
+ @namer ||= Utils::Namer.new(self)
17
+ end
18
+
19
+ def env_for_service(service_name)
20
+ Utils::EnvResolver.new(self).env_for_service(service_name)
21
+ end
22
+
23
+ def validate_config
24
+ app = @deploy.application
25
+ validate_providers_config
26
+ validate_database_secrets(app.database) if app.database
27
+ inject_database_env_vars
28
+ validate_service_server_bindings
29
+ validate_domain_uniqueness
30
+ end
31
+
32
+ def provider_name
33
+ return "hetzner" if @deploy.application.compute_provider.hetzner
34
+ return "aws" if @deploy.application.compute_provider.aws
35
+ return "scaleway" if @deploy.application.compute_provider.scaleway
36
+
37
+ ""
38
+ end
39
+
40
+ def hetzner
41
+ @deploy.application.compute_provider.hetzner
42
+ end
43
+
44
+ def aws
45
+ @deploy.application.compute_provider.aws
46
+ end
47
+
48
+ def scaleway
49
+ @deploy.application.compute_provider.scaleway
50
+ end
51
+
52
+ def cloudflare
53
+ @deploy.application.domain_provider.cloudflare
54
+ end
55
+
56
+ def keep_count_value
57
+ count = @deploy.application.keep_count
58
+ count && count.positive? ? count : 2
59
+ end
60
+
61
+ private
62
+
63
+ def validate_service_server_bindings
64
+ app = @deploy.application
65
+ defined_servers = {}
66
+ master_count = 0
67
+
68
+ app.servers.each do |server_name, server_config|
69
+ defined_servers[server_name] = true
70
+ master_count += 1 if server_config&.master
71
+ end
72
+
73
+ if app.servers.empty?
74
+ has_services = !app.app.empty? || app.database || !app.services.empty?
75
+ raise Errors::ConfigValidationError, "servers must be defined when deploying services" if has_services
76
+ end
77
+
78
+ if app.servers.size > 1
79
+ raise Errors::ConfigValidationError, "when multiple servers are defined, exactly one must have master: true" if master_count.zero?
80
+ raise Errors::ConfigValidationError, "only one server can have master: true, found #{master_count}" if master_count > 1
81
+ elsif app.servers.size == 1 && master_count > 1
82
+ raise Errors::ConfigValidationError, "only one server can have master: true, found #{master_count}"
83
+ end
84
+
85
+ app.app.each do |svc_name, svc_config|
86
+ raise Errors::ConfigValidationError, "app.#{svc_name}: servers field is required" if svc_config.servers.empty?
87
+
88
+ svc_config.servers.each do |server_ref|
89
+ raise Errors::ConfigValidationError, "app.#{svc_name}: references undefined server '#{server_ref}'" unless defined_servers[server_ref]
90
+ end
91
+ end
92
+
93
+ if app.database
94
+ raise Errors::ConfigValidationError, "database: servers field is required" if app.database.servers.empty?
95
+
96
+ app.database.servers.each do |server_ref|
97
+ raise Errors::ConfigValidationError, "database: references undefined server '#{server_ref}'" unless defined_servers[server_ref]
98
+ end
99
+ end
100
+
101
+ app.services.each do |svc_name, svc_config|
102
+ raise Errors::ConfigValidationError, "services.#{svc_name}: servers field is required" if svc_config.servers.empty?
103
+
104
+ svc_config.servers.each do |server_ref|
105
+ raise Errors::ConfigValidationError, "services.#{svc_name}: references undefined server '#{server_ref}'" unless defined_servers[server_ref]
106
+ end
107
+ end
108
+ end
109
+
110
+ def validate_database_secrets(db)
111
+ adapter = db.adapter&.downcase
112
+ return unless db.url.blank?
113
+
114
+ case adapter
115
+ when "postgres", "postgresql"
116
+ %w[POSTGRES_USER POSTGRES_PASSWORD POSTGRES_DB].each do |key|
117
+ raise Errors::ConfigValidationError, "postgres database requires #{key} in secrets (or provide database.url)" unless db.secrets[key]
118
+ end
119
+ when "mysql"
120
+ %w[MYSQL_USER MYSQL_PASSWORD MYSQL_DATABASE].each do |key|
121
+ raise Errors::ConfigValidationError, "mysql database requires #{key} in secrets (or provide database.url)" unless db.secrets[key]
122
+ end
123
+ when "sqlite", "sqlite3"
124
+ # SQLite doesn't require secrets
125
+ end
126
+ end
127
+
128
+ def validate_providers_config
129
+ app = @deploy.application
130
+
131
+ unless app.domain_provider.cloudflare
132
+ raise Errors::ConfigValidationError, "domain provider required: currently only cloudflare is supported"
133
+ end
134
+
135
+ cf = app.domain_provider.cloudflare
136
+ raise Errors::ConfigValidationError, "cloudflare api_token is required" if cf.api_token.blank?
137
+ raise Errors::ConfigValidationError, "cloudflare account_id is required" if cf.account_id.blank?
138
+
139
+ has_provider = false
140
+
141
+ if app.compute_provider.hetzner
142
+ has_provider = true
143
+ h = app.compute_provider.hetzner
144
+ raise Errors::ConfigValidationError, "hetzner api_token is required" if h.api_token.blank?
145
+ raise Errors::ConfigValidationError, "hetzner server_type is required" if h.server_type.blank?
146
+ raise Errors::ConfigValidationError, "hetzner server_location is required" if h.server_location.blank?
147
+ end
148
+
149
+ if app.compute_provider.aws
150
+ has_provider = true
151
+ a = app.compute_provider.aws
152
+ raise Errors::ConfigValidationError, "aws access_key_id is required" if a.access_key_id.blank?
153
+ raise Errors::ConfigValidationError, "aws secret_access_key is required" if a.secret_access_key.blank?
154
+ raise Errors::ConfigValidationError, "aws region is required" if a.region.blank?
155
+ raise Errors::ConfigValidationError, "aws instance_type is required" if a.instance_type.blank?
156
+ end
157
+
158
+ if app.compute_provider.scaleway
159
+ has_provider = true
160
+ s = app.compute_provider.scaleway
161
+ raise Errors::ConfigValidationError, "scaleway secret_key is required" if s.secret_key.blank?
162
+ raise Errors::ConfigValidationError, "scaleway project_id is required" if s.project_id.blank?
163
+ raise Errors::ConfigValidationError, "scaleway server_type is required" if s.server_type.blank?
164
+ end
165
+
166
+ raise Errors::ConfigValidationError, "compute provider required: hetzner, aws, or scaleway must be configured" unless has_provider
167
+ end
168
+
169
+ def inject_database_env_vars
170
+ app = @deploy.application
171
+ return unless app.database
172
+
173
+ db = app.database
174
+ adapter = db.adapter&.downcase
175
+ return unless adapter
176
+
177
+ provider = External::Database.provider_for(adapter)
178
+ return unless provider.needs_container?
179
+
180
+ creds = parse_database_credentials(db, provider)
181
+ return unless creds
182
+
183
+ db_host = namer.database_service_name
184
+ env_vars = provider.app_env(creds, host: db_host)
185
+
186
+ app.app.each_value do |svc_config|
187
+ svc_config.env ||= {}
188
+ env_vars.each { |key, value| svc_config.env[key] ||= value }
189
+ end
190
+ end
191
+
192
+ def parse_database_credentials(db, provider)
193
+ return provider.parse_url(db.url) unless db.url.blank?
194
+
195
+ adapter = db.adapter&.downcase
196
+ case adapter
197
+ when "postgres", "postgresql"
198
+ External::Database::Types::Credentials.new(
199
+ user: db.secrets["POSTGRES_USER"],
200
+ password: db.secrets["POSTGRES_PASSWORD"],
201
+ database: db.secrets["POSTGRES_DB"],
202
+ port: provider.default_port
203
+ )
204
+ when "mysql"
205
+ External::Database::Types::Credentials.new(
206
+ user: db.secrets["MYSQL_USER"],
207
+ password: db.secrets["MYSQL_PASSWORD"],
208
+ database: db.secrets["MYSQL_DATABASE"],
209
+ port: provider.default_port
210
+ )
211
+ end
212
+ end
213
+
214
+ def validate_domain_uniqueness
215
+ app = @deploy.application
216
+ return unless app.app
217
+
218
+ seen = {}
219
+ app.app.each do |name, cfg|
220
+ next if cfg.domain.blank?
221
+
222
+ hostnames = Utils::Namer.build_hostnames(cfg.subdomain, cfg.domain)
223
+ hostnames.each do |hostname|
224
+ if seen[hostname]
225
+ raise Errors::ConfigValidationError,
226
+ "domain '#{hostname}' used by both '#{seen[hostname]}' and '#{name}'"
227
+ end
228
+ seen[hostname] = name
229
+ end
230
+ end
231
+ end
232
+ end
233
+ end
234
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nvoi
4
+ module Configuration
5
+ # ServerVolume defines a volume attached to a server
6
+ class ServerVolume
7
+ attr_accessor :size
8
+
9
+ def initialize(data = nil)
10
+ data ||= {}
11
+ raise ArgumentError, "volume config must be a hash with 'size' key" unless data.is_a?(Hash)
12
+
13
+ @size = data["size"]&.to_i || 10
14
+ end
15
+ end
16
+
17
+ # Server contains server instance configuration
18
+ class Server
19
+ attr_accessor :master, :type, :location, :count, :volumes
20
+
21
+ def initialize(data = nil)
22
+ data ||= {}
23
+ @master = data["master"] || false
24
+ @type = data["type"]
25
+ @location = data["location"]
26
+ @count = data["count"]&.to_i || 1
27
+ @volumes = (data["volumes"] || {}).transform_values { |v| ServerVolume.new(v) }
28
+ end
29
+
30
+ def master?
31
+ @master == true
32
+ end
33
+
34
+ def volume(name)
35
+ @volumes[name.to_s]
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nvoi
4
+ module Configuration
5
+ # Service defines a generic service
6
+ class Service
7
+ attr_accessor :servers, :image, :port, :command, :env, :mount
8
+
9
+ def initialize(data = nil)
10
+ data ||= {}
11
+ @servers = data["servers"] || []
12
+ @image = data["image"]
13
+ @port = data["port"]&.to_i
14
+ @command = data["command"]
15
+ @env = data["env"] || {}
16
+ @mount = data["mount"] || {}
17
+ end
18
+
19
+ def to_service_spec(app_name, service_name)
20
+ cmd = @command ? @command.split : []
21
+ port = @port && @port.positive? ? @port : infer_port_from_image
22
+
23
+ Configuration::Deployment.new(
24
+ name: "#{app_name}-#{service_name}",
25
+ image: @image,
26
+ port:,
27
+ command: cmd,
28
+ env: @env,
29
+ mounts: @mount,
30
+ replicas: 1,
31
+ stateful_set: false,
32
+ servers: @servers
33
+ )
34
+ end
35
+
36
+ private
37
+
38
+ def infer_port_from_image
39
+ case @image
40
+ when /redis/ then 6379
41
+ when /postgres/ then 5432
42
+ when /mysql/ then 3306
43
+ when /memcache/ then 11211
44
+ when /mongo/ then 27017
45
+ when /elastic/ then 9200
46
+ else 0
47
+ end
48
+ end
49
+ end
50
+
51
+ # SshKey defines SSH key content (stored in encrypted config)
52
+ class SshKey
53
+ attr_accessor :private_key, :public_key
54
+
55
+ def initialize(data = nil)
56
+ data ||= {}
57
+ @private_key = data["private_key"]
58
+ @public_key = data["public_key"]
59
+ end
60
+ end
61
+ end
62
+ end
@@ -21,7 +21,7 @@ module Nvoi
21
21
  def find_or_create_network(name)
22
22
  vpc = find_vpc_by_name(name)
23
23
  if vpc
24
- return Objects::Network::Record.new(
24
+ return Types::Network::Record.new(
25
25
  id: vpc.vpc_id,
26
26
  name:,
27
27
  ip_range: vpc.cidr_block
@@ -89,7 +89,7 @@ module Nvoi
89
89
  subnet_id: subnet_resp.subnet.subnet_id
90
90
  )
91
91
 
92
- Objects::Network::Record.new(
92
+ Types::Network::Record.new(
93
93
  id: vpc_id,
94
94
  name:,
95
95
  ip_range: create_resp.vpc.cidr_block
@@ -100,7 +100,7 @@ module Nvoi
100
100
  vpc = find_vpc_by_name(name)
101
101
  raise Errors::NetworkError, "network not found: #{name}" unless vpc
102
102
 
103
- Objects::Network::Record.new(
103
+ Types::Network::Record.new(
104
104
  id: vpc.vpc_id,
105
105
  name:,
106
106
  ip_range: vpc.cidr_block
@@ -116,7 +116,7 @@ module Nvoi
116
116
  def find_or_create_firewall(name)
117
117
  sg = find_security_group_by_name(name)
118
118
  if sg
119
- return Objects::Firewall::Record.new(id: sg.group_id, name:)
119
+ return Types::Firewall::Record.new(id: sg.group_id, name:)
120
120
  end
121
121
 
122
122
  # Get default VPC
@@ -145,14 +145,14 @@ module Nvoi
145
145
  }]
146
146
  )
147
147
 
148
- Objects::Firewall::Record.new(id: create_resp.group_id, name:)
148
+ Types::Firewall::Record.new(id: create_resp.group_id, name:)
149
149
  end
150
150
 
151
151
  def get_firewall_by_name(name)
152
152
  sg = find_security_group_by_name(name)
153
153
  raise Errors::FirewallError, "firewall not found: #{name}" unless sg
154
154
 
155
- Objects::Firewall::Record.new(id: sg.group_id, name:)
155
+ Types::Firewall::Record.new(id: sg.group_id, name:)
156
156
  end
157
157
 
158
158
  def delete_firewall(id)
@@ -210,7 +210,7 @@ module Nvoi
210
210
  }
211
211
 
212
212
  # Add network configuration if provided
213
- if opts.network_id && !opts.network_id.empty?
213
+ unless opts.network_id.blank?
214
214
  subnets = @client.describe_subnets(
215
215
  filters: [{ name: "vpc-id", values: [opts.network_id] }]
216
216
  )
@@ -218,7 +218,7 @@ module Nvoi
218
218
  end
219
219
 
220
220
  # Add security group if provided
221
- if opts.firewall_id && !opts.firewall_id.empty?
221
+ unless opts.firewall_id.blank?
222
222
  input[:security_group_ids] = [opts.firewall_id]
223
223
  end
224
224
 
@@ -266,7 +266,7 @@ module Nvoi
266
266
  }]
267
267
  )
268
268
 
269
- Objects::Volume::Record.new(
269
+ Types::Volume::Record.new(
270
270
  id: create_resp.volume_id,
271
271
  name: opts.name,
272
272
  size: create_resp.size,
@@ -317,7 +317,7 @@ module Nvoi
317
317
  next nil if vol.attachments.empty?
318
318
 
319
319
  device = vol.attachments[0].device
320
- device if device && !device.empty?
320
+ device unless device.blank?
321
321
  end
322
322
  end
323
323
 
@@ -417,7 +417,7 @@ module Nvoi
417
417
  def instance_to_server(instance)
418
418
  name = instance.tags&.find { |t| t.key == "Name" }&.value || ""
419
419
 
420
- Objects::Server::Record.new(
420
+ Types::Server::Record.new(
421
421
  id: instance.instance_id,
422
422
  name:,
423
423
  status: instance.state.name,
@@ -429,7 +429,7 @@ module Nvoi
429
429
  def volume_to_object(vol)
430
430
  name = vol.tags&.find { |t| t.key == "Name" }&.value || ""
431
431
 
432
- v = Objects::Volume::Record.new(
432
+ v = Types::Volume::Record.new(
433
433
  id: vol.volume_id,
434
434
  name:,
435
435
  size: vol.size,
@@ -122,12 +122,12 @@ module Nvoi
122
122
  }
123
123
 
124
124
  # Add network if provided
125
- if opts.network_id && !opts.network_id.empty?
125
+ unless opts.network_id.blank?
126
126
  create_opts[:networks] = [opts.network_id.to_i]
127
127
  end
128
128
 
129
129
  # Add firewall if provided
130
- if opts.firewall_id && !opts.firewall_id.empty?
130
+ unless opts.firewall_id.blank?
131
131
  create_opts[:firewalls] = [{ firewall: opts.firewall_id.to_i }]
132
132
  end
133
133
 
@@ -216,7 +216,7 @@ module Nvoi
216
216
  # Hetzner provides device_path in API response
217
217
  Utils::Retry.poll(max_attempts: 30, interval: 2) do
218
218
  volume = get("/volumes/#{volume_id.to_i}")["volume"]
219
- volume["linux_device"] if volume && volume["linux_device"] && !volume["linux_device"].empty?
219
+ volume&.dig("linux_device").then { |d| d unless d.blank? }
220
220
  end
221
221
  end
222
222
 
@@ -358,7 +358,7 @@ module Nvoi
358
358
  end
359
359
 
360
360
  def to_network(data)
361
- Objects::Network::Record.new(
361
+ Types::Network::Record.new(
362
362
  id: data["id"].to_s,
363
363
  name: data["name"],
364
364
  ip_range: data["ip_range"]
@@ -366,7 +366,7 @@ module Nvoi
366
366
  end
367
367
 
368
368
  def to_firewall(data)
369
- Objects::Firewall::Record.new(
369
+ Types::Firewall::Record.new(
370
370
  id: data["id"].to_s,
371
371
  name: data["name"]
372
372
  )
@@ -376,7 +376,7 @@ module Nvoi
376
376
  # Get private IP from private_net array
377
377
  private_ip = data["private_net"]&.first&.dig("ip")
378
378
 
379
- Objects::Server::Record.new(
379
+ Types::Server::Record.new(
380
380
  id: data["id"].to_s,
381
381
  name: data["name"],
382
382
  status: data["status"],
@@ -386,7 +386,7 @@ module Nvoi
386
386
  end
387
387
 
388
388
  def to_volume(data)
389
- Objects::Volume::Record.new(
389
+ Types::Volume::Record.new(
390
390
  id: data["id"].to_s,
391
391
  name: data["name"],
392
392
  size: data["size"],
@@ -146,14 +146,14 @@ module Nvoi
146
146
  }
147
147
 
148
148
  # Add security group if provided
149
- if opts.firewall_id && !opts.firewall_id.empty?
149
+ unless opts.firewall_id.blank?
150
150
  create_opts[:security_group] = opts.firewall_id
151
151
  end
152
152
 
153
153
  server = post(instance_url("/servers"), create_opts)["server"]
154
154
 
155
155
  # Set cloud-init user data if provided
156
- if opts.user_data && !opts.user_data.empty?
156
+ unless opts.user_data.blank?
157
157
  set_user_data(server["id"], "cloud-init", opts.user_data)
158
158
  end
159
159
 
@@ -161,7 +161,7 @@ module Nvoi
161
161
  server_action(server["id"], "poweron")
162
162
 
163
163
  # Attach to private network if provided
164
- if opts.network_id && !opts.network_id.empty?
164
+ unless opts.network_id.blank?
165
165
  wait_for_server_state(server["id"], "running", 30)
166
166
  create_private_nic(server["id"], opts.network_id)
167
167
  end
@@ -511,7 +511,7 @@ module Nvoi
511
511
  end
512
512
 
513
513
  def to_network(data)
514
- Objects::Network::Record.new(
514
+ Types::Network::Record.new(
515
515
  id: data["id"],
516
516
  name: data["name"],
517
517
  ip_range: data.dig("subnets", 0, "subnet") || data["subnets"]&.first
@@ -519,7 +519,7 @@ module Nvoi
519
519
  end
520
520
 
521
521
  def to_firewall(data)
522
- Objects::Firewall::Record.new(
522
+ Types::Firewall::Record.new(
523
523
  id: data["id"],
524
524
  name: data["name"]
525
525
  )
@@ -529,7 +529,7 @@ module Nvoi
529
529
  # Scaleway doesn't include private_ips in the NIC response directly
530
530
  # We'd need to call IPAM API which adds complexity
531
531
  # Instead, private IP discovery happens via SSH in setup_k3s
532
- Objects::Server::Record.new(
532
+ Types::Server::Record.new(
533
533
  id: data["id"],
534
534
  name: data["name"],
535
535
  status: data["state"],
@@ -543,7 +543,7 @@ module Nvoi
543
543
  r["product_resource_type"] == "instance_server"
544
544
  }&.dig("product_resource_id")
545
545
 
546
- Objects::Volume::Record.new(
546
+ Types::Volume::Record.new(
547
547
  id: data["id"],
548
548
  name: data["name"],
549
549
  size: (data["size"] || 0) / 1_000_000_000,
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nvoi
4
+ module External
5
+ module Cloud
6
+ module Types
7
+ # Server-related structs
8
+ module Server
9
+ # Record represents a compute server/instance
10
+ Record = Struct.new(:id, :name, :status, :public_ipv4, :private_ipv4, keyword_init: true)
11
+
12
+ # CreateOptions contains options for creating a server
13
+ CreateOptions = Struct.new(:name, :type, :image, :location, :user_data, :network_id, :firewall_id, :ssh_keys, keyword_init: true)
14
+ end
15
+
16
+ # Volume-related structs
17
+ module Volume
18
+ # Record represents a block storage volume
19
+ Record = Struct.new(:id, :name, :size, :location, :status, :server_id, :device_path, keyword_init: true)
20
+
21
+ # CreateOptions contains options for creating a volume
22
+ CreateOptions = Struct.new(:name, :size, :server_id, :location, keyword_init: true)
23
+
24
+ # MountOptions contains options for mounting a volume
25
+ MountOptions = Struct.new(:device_path, :mount_path, :fs_type, keyword_init: true)
26
+ end
27
+
28
+ # Network-related structs
29
+ module Network
30
+ # Record represents a virtual network
31
+ Record = Struct.new(:id, :name, :ip_range, keyword_init: true)
32
+ end
33
+
34
+ # Firewall-related structs
35
+ module Firewall
36
+ # Record represents a firewall configuration
37
+ Record = Struct.new(:id, :name, keyword_init: true)
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -48,7 +48,7 @@ module Nvoi
48
48
  end
49
49
 
50
50
  def restore(ssh, data, opts)
51
- create_database(ssh, Objects::Database::CreateOptions.new(
51
+ create_database(ssh, Types::CreateOptions.new(
52
52
  pod_name: opts.pod_name,
53
53
  database: opts.database,
54
54
  user: opts.user,
@@ -46,7 +46,7 @@ module Nvoi
46
46
  end
47
47
 
48
48
  def restore(ssh, data, opts)
49
- create_database(ssh, Objects::Database::CreateOptions.new(
49
+ create_database(ssh, Types::CreateOptions.new(
50
50
  pod_name: opts.pod_name,
51
51
  database: opts.database,
52
52
  user: opts.user,
@@ -49,7 +49,7 @@ module Nvoi
49
49
 
50
50
  def parse_standard_url(url, default_port)
51
51
  uri = URI.parse(url)
52
- Objects::Database::Credentials.new(
52
+ Types::Credentials.new(
53
53
  user: uri.user,
54
54
  password: uri.password,
55
55
  host: uri.host,
@@ -15,7 +15,7 @@ module Nvoi
15
15
 
16
16
  def parse_url(url)
17
17
  path = url.sub(%r{^sqlite3?:///?}, "")
18
- Objects::Database::Credentials.new(
18
+ Types::Credentials.new(
19
19
  path:,
20
20
  database: File.basename(path)
21
21
  )