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.
- checksums.yaml +4 -4
- data/Gemfile +1 -5
- data/Gemfile.lock +17 -8
- data/Rakefile +1 -1
- data/lib/nvoi/cli/config/command.rb +46 -41
- data/lib/nvoi/cli/credentials/edit/command.rb +20 -20
- data/lib/nvoi/cli/credentials/show/command.rb +1 -1
- data/lib/nvoi/cli/db/command.rb +10 -10
- data/lib/nvoi/cli/delete/command.rb +2 -2
- data/lib/nvoi/cli/deploy/command.rb +2 -2
- data/lib/nvoi/cli/deploy/steps/configure_tunnel.rb +2 -2
- data/lib/nvoi/cli/deploy/steps/provision_server.rb +1 -1
- data/lib/nvoi/cli/deploy/steps/provision_volume.rb +1 -1
- data/lib/nvoi/cli/exec/command.rb +3 -3
- data/lib/nvoi/cli/logs/command.rb +2 -2
- data/lib/nvoi/cli/onboard/command.rb +176 -622
- data/lib/nvoi/cli/onboard/steps/app.rb +108 -0
- data/lib/nvoi/cli/onboard/steps/app_name.rb +26 -0
- data/lib/nvoi/cli/onboard/steps/compute.rb +139 -0
- data/lib/nvoi/cli/onboard/steps/database.rb +97 -0
- data/lib/nvoi/cli/onboard/steps/domain.rb +48 -0
- data/lib/nvoi/cli/onboard/steps/env.rb +67 -0
- data/lib/nvoi/cli/onboard/ui.rb +84 -0
- data/lib/nvoi/cli/unlock/command.rb +2 -2
- data/lib/nvoi/cli.rb +0 -32
- data/lib/nvoi/configuration/app_service.rb +54 -0
- data/lib/nvoi/configuration/application.rb +44 -0
- data/lib/nvoi/configuration/builder.rb +417 -0
- data/lib/nvoi/configuration/database.rb +56 -0
- data/lib/nvoi/configuration/deploy.rb +15 -0
- data/lib/nvoi/{objects/service_spec.rb → configuration/deployment.rb} +4 -3
- data/lib/nvoi/{objects/config_override.rb → configuration/override.rb} +4 -4
- data/lib/nvoi/configuration/providers.rb +78 -0
- data/lib/nvoi/configuration/result.rb +43 -0
- data/lib/nvoi/configuration/root.rb +234 -0
- data/lib/nvoi/configuration/server.rb +39 -0
- data/lib/nvoi/configuration/service.rb +62 -0
- data/lib/nvoi/external/cloud/aws.rb +12 -12
- data/lib/nvoi/external/cloud/hetzner.rb +7 -7
- data/lib/nvoi/external/cloud/scaleway.rb +7 -7
- data/lib/nvoi/external/cloud/types.rb +42 -0
- data/lib/nvoi/external/database/mysql.rb +1 -1
- data/lib/nvoi/external/database/postgres.rb +1 -1
- data/lib/nvoi/external/database/provider.rb +1 -1
- data/lib/nvoi/external/database/sqlite.rb +1 -1
- data/lib/nvoi/external/database/types.rb +55 -0
- data/lib/nvoi/external/dns/cloudflare.rb +6 -6
- data/lib/nvoi/external/dns/types.rb +24 -0
- data/lib/nvoi/utils/config_loader.rb +12 -12
- data/lib/nvoi/utils/credential_store.rb +4 -4
- data/lib/nvoi/utils/env_resolver.rb +3 -3
- data/lib/nvoi/utils/namer.rb +2 -2
- data/lib/nvoi/utils/presence.rb +23 -0
- data/lib/nvoi/version.rb +1 -1
- data/lib/nvoi.rb +2 -17
- metadata +95 -58
- data/.claude/todo/refactor/00-overview.md +0 -171
- data/.claude/todo/refactor/01-objects.md +0 -96
- data/.claude/todo/refactor/02-utils.md +0 -143
- data/.claude/todo/refactor/03-external-cloud.md +0 -164
- data/.claude/todo/refactor/04-external-dns.md +0 -104
- data/.claude/todo/refactor/05-external.md +0 -133
- data/.claude/todo/refactor/06-cli.md +0 -123
- data/.claude/todo/refactor/07-cli-deploy-command.md +0 -177
- data/.claude/todo/refactor/08-cli-deploy-steps.md +0 -201
- data/.claude/todo/refactor/09-cli-delete-command.md +0 -169
- data/.claude/todo/refactor/10-cli-exec-command.md +0 -157
- data/.claude/todo/refactor/11-cli-credentials-command.md +0 -190
- data/.claude/todo/refactor/12-cli-db-command.md +0 -128
- data/.claude/todo/refactor/_target.md +0 -79
- data/.claude/todo/refactor-execution/00-entrypoint.md +0 -49
- data/.claude/todo/refactor-execution/01-objects.md +0 -42
- data/.claude/todo/refactor-execution/02-utils.md +0 -41
- data/.claude/todo/refactor-execution/03-external-cloud.md +0 -38
- data/.claude/todo/refactor-execution/04-external-dns.md +0 -35
- data/.claude/todo/refactor-execution/05-external-other.md +0 -46
- data/.claude/todo/refactor-execution/06-cli-deploy.md +0 -45
- data/.claude/todo/refactor-execution/07-cli-delete.md +0 -43
- data/.claude/todo/refactor-execution/08-cli-exec.md +0 -30
- data/.claude/todo/refactor-execution/09-cli-credentials.md +0 -34
- data/.claude/todo/refactor-execution/10-cli-db.md +0 -31
- data/.claude/todo/refactor-execution/11-cli-router.md +0 -44
- data/.claude/todo/refactor-execution/12-cleanup.md +0 -120
- data/.claude/todo/refactor-execution/_monitoring-strategy.md +0 -126
- data/.claude/todo/scaleway.impl.md +0 -644
- data/.claude/todo/scaleway.reference.md +0 -520
- data/.claude/todos/buckets.md +0 -41
- data/.claude/todos.md +0 -550
- data/ingest +0 -0
- data/lib/nvoi/config_api/actions/app.rb +0 -53
- data/lib/nvoi/config_api/actions/compute_provider.rb +0 -55
- data/lib/nvoi/config_api/actions/database.rb +0 -70
- data/lib/nvoi/config_api/actions/domain_provider.rb +0 -40
- data/lib/nvoi/config_api/actions/env.rb +0 -32
- data/lib/nvoi/config_api/actions/init.rb +0 -67
- data/lib/nvoi/config_api/actions/secret.rb +0 -32
- data/lib/nvoi/config_api/actions/server.rb +0 -66
- data/lib/nvoi/config_api/actions/service.rb +0 -52
- data/lib/nvoi/config_api/actions/volume.rb +0 -40
- data/lib/nvoi/config_api/base.rb +0 -38
- data/lib/nvoi/config_api/result.rb +0 -26
- data/lib/nvoi/config_api.rb +0 -93
- data/lib/nvoi/objects/configuration.rb +0 -483
- data/lib/nvoi/objects/database.rb +0 -56
- data/lib/nvoi/objects/dns.rb +0 -14
- data/lib/nvoi/objects/firewall.rb +0 -11
- data/lib/nvoi/objects/network.rb +0 -11
- data/lib/nvoi/objects/server.rb +0 -14
- data/lib/nvoi/objects/tunnel.rb +0 -14
- 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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|