nvoi 0.1.6 → 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.claude/todo/refactor/12-cli-db-command.md +128 -0
- data/.claude/todo/refactor-execution/00-entrypoint.md +49 -0
- data/.claude/todo/refactor-execution/01-objects.md +42 -0
- data/.claude/todo/refactor-execution/02-utils.md +41 -0
- data/.claude/todo/refactor-execution/03-external-cloud.md +38 -0
- data/.claude/todo/refactor-execution/04-external-dns.md +35 -0
- data/.claude/todo/refactor-execution/05-external-other.md +46 -0
- data/.claude/todo/refactor-execution/06-cli-deploy.md +45 -0
- data/.claude/todo/refactor-execution/07-cli-delete.md +43 -0
- data/.claude/todo/refactor-execution/08-cli-exec.md +30 -0
- data/.claude/todo/refactor-execution/09-cli-credentials.md +34 -0
- data/.claude/todo/refactor-execution/10-cli-db.md +31 -0
- data/.claude/todo/refactor-execution/11-cli-router.md +44 -0
- data/.claude/todo/refactor-execution/12-cleanup.md +120 -0
- data/.claude/todo/refactor-execution/_monitoring-strategy.md +126 -0
- data/.claude/todos.md +550 -0
- data/Gemfile +5 -0
- data/Gemfile.lock +35 -4
- data/Rakefile +1 -1
- data/ingest +0 -0
- data/lib/nvoi/cli/config/command.rb +219 -0
- data/lib/nvoi/cli/delete/steps/teardown_dns.rb +12 -11
- data/lib/nvoi/cli/deploy/steps/configure_tunnel.rb +15 -13
- data/lib/nvoi/cli/deploy/steps/deploy_service.rb +5 -2
- data/lib/nvoi/cli/deploy/steps/setup_k3s.rb +10 -1
- data/lib/nvoi/cli/logs/command.rb +66 -0
- data/lib/nvoi/cli/onboard/command.rb +761 -0
- data/lib/nvoi/cli/unlock/command.rb +72 -0
- data/lib/nvoi/cli.rb +257 -0
- data/lib/nvoi/config_api/actions/app.rb +30 -30
- data/lib/nvoi/config_api/actions/compute_provider.rb +31 -31
- data/lib/nvoi/config_api/actions/database.rb +42 -42
- data/lib/nvoi/config_api/actions/domain_provider.rb +40 -0
- data/lib/nvoi/config_api/actions/env.rb +12 -12
- data/lib/nvoi/config_api/actions/init.rb +67 -0
- data/lib/nvoi/config_api/actions/secret.rb +12 -12
- data/lib/nvoi/config_api/actions/server.rb +35 -35
- data/lib/nvoi/config_api/actions/service.rb +52 -0
- data/lib/nvoi/config_api/actions/volume.rb +18 -18
- data/lib/nvoi/config_api/base.rb +15 -21
- data/lib/nvoi/config_api/result.rb +5 -5
- data/lib/nvoi/config_api.rb +51 -28
- data/lib/nvoi/external/cloud/aws.rb +26 -1
- data/lib/nvoi/external/cloud/hetzner.rb +27 -1
- data/lib/nvoi/external/cloud/scaleway.rb +32 -6
- data/lib/nvoi/external/containerd.rb +4 -0
- data/lib/nvoi/external/dns/cloudflare.rb +34 -16
- data/lib/nvoi/objects/configuration.rb +20 -0
- data/lib/nvoi/utils/namer.rb +9 -0
- data/lib/nvoi/utils/retry.rb +1 -1
- data/lib/nvoi/version.rb +1 -1
- data/lib/nvoi.rb +16 -0
- data/templates/app-ingress.yaml.erb +3 -1
- metadata +25 -1
|
@@ -127,7 +127,7 @@ module Nvoi
|
|
|
127
127
|
|
|
128
128
|
def create_server(opts)
|
|
129
129
|
# Validate server type
|
|
130
|
-
server_types =
|
|
130
|
+
server_types = list_server_types_api
|
|
131
131
|
unless server_types.key?(opts.type)
|
|
132
132
|
raise Errors::ValidationError, "invalid server type: #{opts.type}"
|
|
133
133
|
end
|
|
@@ -170,7 +170,7 @@ module Nvoi
|
|
|
170
170
|
end
|
|
171
171
|
|
|
172
172
|
def wait_for_server(server_id, max_attempts)
|
|
173
|
-
server = Utils::Retry.poll(max_attempts
|
|
173
|
+
server = Utils::Retry.poll(max_attempts:, interval: Utils::Constants::SERVER_READY_INTERVAL) do
|
|
174
174
|
s = get_server_api(server_id)
|
|
175
175
|
to_server(s) if s["state"] == "running" && s.dig("public_ip", "address")
|
|
176
176
|
end
|
|
@@ -281,7 +281,7 @@ module Nvoi
|
|
|
281
281
|
# Validation operations
|
|
282
282
|
|
|
283
283
|
def validate_instance_type(instance_type)
|
|
284
|
-
server_types =
|
|
284
|
+
server_types = list_server_types_api
|
|
285
285
|
unless server_types.key?(instance_type)
|
|
286
286
|
raise Errors::ValidationError, "invalid scaleway server type: #{instance_type}"
|
|
287
287
|
end
|
|
@@ -298,7 +298,7 @@ module Nvoi
|
|
|
298
298
|
end
|
|
299
299
|
|
|
300
300
|
def validate_credentials
|
|
301
|
-
|
|
301
|
+
list_server_types_api
|
|
302
302
|
true
|
|
303
303
|
rescue Errors::AuthenticationError => e
|
|
304
304
|
raise Errors::ValidationError, "scaleway credentials invalid: #{e.message}"
|
|
@@ -310,6 +310,32 @@ module Nvoi
|
|
|
310
310
|
server&.public_ipv4
|
|
311
311
|
end
|
|
312
312
|
|
|
313
|
+
# List available server types for onboarding
|
|
314
|
+
def list_server_types
|
|
315
|
+
list_server_types_api.map do |name, info|
|
|
316
|
+
{
|
|
317
|
+
name:,
|
|
318
|
+
cores: info.dig("ncpus"),
|
|
319
|
+
ram: info.dig("ram"),
|
|
320
|
+
hourly_price: info.dig("hourly_price")
|
|
321
|
+
}
|
|
322
|
+
end
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
# List available zones for onboarding
|
|
326
|
+
def list_zones
|
|
327
|
+
VALID_ZONES.map do |z|
|
|
328
|
+
parts = z.split("-")
|
|
329
|
+
city = case parts[0..1].join("-")
|
|
330
|
+
when "fr-par" then "Paris"
|
|
331
|
+
when "nl-ams" then "Amsterdam"
|
|
332
|
+
when "pl-waw" then "Warsaw"
|
|
333
|
+
else parts[0..1].join("-")
|
|
334
|
+
end
|
|
335
|
+
{ name: z, city: }
|
|
336
|
+
end
|
|
337
|
+
end
|
|
338
|
+
|
|
313
339
|
private
|
|
314
340
|
|
|
315
341
|
def zone_to_region(zone)
|
|
@@ -402,7 +428,7 @@ module Nvoi
|
|
|
402
428
|
post(instance_url("/servers/#{id}/action"), { action: })
|
|
403
429
|
end
|
|
404
430
|
|
|
405
|
-
def
|
|
431
|
+
def list_server_types_api
|
|
406
432
|
get(instance_url("/products/servers"))["servers"] || {}
|
|
407
433
|
end
|
|
408
434
|
|
|
@@ -450,7 +476,7 @@ module Nvoi
|
|
|
450
476
|
end
|
|
451
477
|
|
|
452
478
|
def wait_for_server_state(server_id, target_state, max_attempts)
|
|
453
|
-
Utils::Retry.poll(max_attempts
|
|
479
|
+
Utils::Retry.poll(max_attempts:, interval: 2) do
|
|
454
480
|
server = get_server_api(server_id)
|
|
455
481
|
server if server["state"] == target_state
|
|
456
482
|
end
|
|
@@ -19,6 +19,9 @@ module Nvoi
|
|
|
19
19
|
raise Errors::SshError, "local build failed"
|
|
20
20
|
end
|
|
21
21
|
|
|
22
|
+
# Tag as :latest for next build's cache
|
|
23
|
+
system("docker", "tag", tag, cache_from) if cache_from
|
|
24
|
+
|
|
22
25
|
tar_file = "/tmp/#{tag.tr(':', '_')}.tar"
|
|
23
26
|
unless system("docker", "save", tag, "-o", tar_file)
|
|
24
27
|
raise Errors::SshError, "docker save failed"
|
|
@@ -37,6 +40,7 @@ module Nvoi
|
|
|
37
40
|
raise Errors::SshError, "rsync failed"
|
|
38
41
|
end
|
|
39
42
|
|
|
43
|
+
Nvoi.logger.info "Importing image into containerd..."
|
|
40
44
|
@ssh.execute("sudo ctr -n k8s.io images import #{remote_tar_path}")
|
|
41
45
|
|
|
42
46
|
full_image_ref = "docker.io/library/#{tag}"
|
|
@@ -64,24 +64,25 @@ module Nvoi
|
|
|
64
64
|
response["result"]
|
|
65
65
|
end
|
|
66
66
|
|
|
67
|
-
def update_tunnel_configuration(tunnel_id,
|
|
67
|
+
def update_tunnel_configuration(tunnel_id, hostnames, service_url)
|
|
68
|
+
hostnames = Array(hostnames)
|
|
68
69
|
url = "accounts/#{@account_id}/cfd_tunnel/#{tunnel_id}/configurations"
|
|
69
70
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
]
|
|
79
|
-
}
|
|
71
|
+
ingress_rules = hostnames.map do |hostname|
|
|
72
|
+
{
|
|
73
|
+
hostname:,
|
|
74
|
+
service: service_url,
|
|
75
|
+
originRequest: { httpHostHeader: hostname.sub(/^\*\./, "") } # Use apex for wildcard
|
|
76
|
+
}
|
|
77
|
+
end
|
|
78
|
+
ingress_rules << { service: "http_status:404" }
|
|
80
79
|
|
|
80
|
+
config = { ingress: ingress_rules }
|
|
81
81
|
put(url, { config: })
|
|
82
82
|
end
|
|
83
83
|
|
|
84
|
-
def verify_tunnel_configuration(tunnel_id,
|
|
84
|
+
def verify_tunnel_configuration(tunnel_id, expected_hostnames, expected_service, max_attempts)
|
|
85
|
+
expected_hostnames = Array(expected_hostnames)
|
|
85
86
|
url = "accounts/#{@account_id}/cfd_tunnel/#{tunnel_id}/configurations"
|
|
86
87
|
|
|
87
88
|
max_attempts.times do
|
|
@@ -90,10 +91,11 @@ module Nvoi
|
|
|
90
91
|
|
|
91
92
|
if response["success"]
|
|
92
93
|
config = response.dig("result", "config")
|
|
93
|
-
config&.dig("ingress")
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
94
|
+
ingress = config&.dig("ingress") || []
|
|
95
|
+
configured_hostnames = ingress.map { |r| r["hostname"] }.compact
|
|
96
|
+
|
|
97
|
+
if expected_hostnames.all? { |h| configured_hostnames.include?(h) }
|
|
98
|
+
return true
|
|
97
99
|
end
|
|
98
100
|
end
|
|
99
101
|
rescue StandardError
|
|
@@ -122,6 +124,16 @@ module Nvoi
|
|
|
122
124
|
|
|
123
125
|
# DNS operations
|
|
124
126
|
|
|
127
|
+
def list_zones
|
|
128
|
+
url = "zones"
|
|
129
|
+
response = get(url)
|
|
130
|
+
|
|
131
|
+
results = response["result"] || []
|
|
132
|
+
results.map do |z|
|
|
133
|
+
{ id: z["id"], name: z["name"], status: z["status"] }
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
125
137
|
def find_zone(domain)
|
|
126
138
|
url = "zones"
|
|
127
139
|
response = get(url)
|
|
@@ -135,6 +147,12 @@ module Nvoi
|
|
|
135
147
|
Objects::Dns::Zone.new(id: zone_data["id"], name: zone_data["name"])
|
|
136
148
|
end
|
|
137
149
|
|
|
150
|
+
def subdomain_available?(zone_id, subdomain, domain)
|
|
151
|
+
fqdn = subdomain.empty? ? domain : "#{subdomain}.#{domain}"
|
|
152
|
+
# Check for CNAME or A record
|
|
153
|
+
!find_dns_record(zone_id, fqdn, "CNAME") && !find_dns_record(zone_id, fqdn, "A")
|
|
154
|
+
end
|
|
155
|
+
|
|
138
156
|
def find_dns_record(zone_id, name, record_type)
|
|
139
157
|
url = "zones/#{zone_id}/dns_records"
|
|
140
158
|
response = get(url)
|
|
@@ -28,6 +28,7 @@ module Nvoi
|
|
|
28
28
|
validate_database_secrets(app.database) if app.database
|
|
29
29
|
inject_database_env_vars
|
|
30
30
|
validate_service_server_bindings
|
|
31
|
+
validate_domain_uniqueness
|
|
31
32
|
end
|
|
32
33
|
|
|
33
34
|
def provider_name
|
|
@@ -211,6 +212,25 @@ module Nvoi
|
|
|
211
212
|
)
|
|
212
213
|
end
|
|
213
214
|
end
|
|
215
|
+
|
|
216
|
+
def validate_domain_uniqueness
|
|
217
|
+
app = @deploy.application
|
|
218
|
+
return unless app.app
|
|
219
|
+
|
|
220
|
+
seen = {}
|
|
221
|
+
app.app.each do |name, cfg|
|
|
222
|
+
next unless cfg.domain && !cfg.domain.empty?
|
|
223
|
+
|
|
224
|
+
hostnames = Utils::Namer.build_hostnames(cfg.subdomain, cfg.domain)
|
|
225
|
+
hostnames.each do |hostname|
|
|
226
|
+
if seen[hostname]
|
|
227
|
+
raise Errors::ConfigValidationError,
|
|
228
|
+
"domain '#{hostname}' used by both '#{seen[hostname]}' and '#{name}'"
|
|
229
|
+
end
|
|
230
|
+
seen[hostname] = name
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
end
|
|
214
234
|
end
|
|
215
235
|
|
|
216
236
|
# Deploy represents the root deployment configuration
|
data/lib/nvoi/utils/namer.rb
CHANGED
|
@@ -185,6 +185,15 @@ module Nvoi
|
|
|
185
185
|
end
|
|
186
186
|
end
|
|
187
187
|
|
|
188
|
+
# Returns array of hostnames - apex returns [domain, *.domain], subdomain returns [sub.domain]
|
|
189
|
+
def self.build_hostnames(subdomain, domain)
|
|
190
|
+
if subdomain.nil? || subdomain.empty? || subdomain == "@"
|
|
191
|
+
[domain, "*.#{domain}"]
|
|
192
|
+
else
|
|
193
|
+
["#{subdomain}.#{domain}"]
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
|
|
188
197
|
private
|
|
189
198
|
|
|
190
199
|
def hash_string(str)
|
data/lib/nvoi/utils/retry.rb
CHANGED
|
@@ -77,7 +77,7 @@ module Nvoi
|
|
|
77
77
|
|
|
78
78
|
# Poll with error on timeout
|
|
79
79
|
def self.poll!(max_attempts: 30, interval: 2, error_message: "operation timed out")
|
|
80
|
-
result = poll(max_attempts
|
|
80
|
+
result = poll(max_attempts:, interval:) { yield }
|
|
81
81
|
raise Errors::TimeoutError, error_message unless result
|
|
82
82
|
|
|
83
83
|
result
|
data/lib/nvoi/version.rb
CHANGED
data/lib/nvoi.rb
CHANGED
|
@@ -16,8 +16,24 @@ require "faraday"
|
|
|
16
16
|
|
|
17
17
|
loader = Zeitwerk::Loader.for_gem
|
|
18
18
|
loader.ignore("#{__dir__}/nvoi/cli") # CLI commands are lazy-loaded
|
|
19
|
+
loader.ignore("#{__dir__}/nvoi/config_api") # ConfigApi uses non-standard naming
|
|
19
20
|
loader.setup
|
|
20
21
|
|
|
22
|
+
# Load ConfigApi manually (uses non-standard naming convention)
|
|
23
|
+
require_relative "nvoi/config_api/result"
|
|
24
|
+
require_relative "nvoi/config_api/base"
|
|
25
|
+
require_relative "nvoi/config_api/actions/init"
|
|
26
|
+
require_relative "nvoi/config_api/actions/domain_provider"
|
|
27
|
+
require_relative "nvoi/config_api/actions/compute_provider"
|
|
28
|
+
require_relative "nvoi/config_api/actions/server"
|
|
29
|
+
require_relative "nvoi/config_api/actions/volume"
|
|
30
|
+
require_relative "nvoi/config_api/actions/app"
|
|
31
|
+
require_relative "nvoi/config_api/actions/database"
|
|
32
|
+
require_relative "nvoi/config_api/actions/secret"
|
|
33
|
+
require_relative "nvoi/config_api/actions/env"
|
|
34
|
+
require_relative "nvoi/config_api/actions/service"
|
|
35
|
+
require_relative "nvoi/config_api"
|
|
36
|
+
|
|
21
37
|
module Nvoi
|
|
22
38
|
class << self
|
|
23
39
|
attr_accessor :logger
|
|
@@ -8,7 +8,8 @@ metadata:
|
|
|
8
8
|
spec:
|
|
9
9
|
ingressClassName: nginx
|
|
10
10
|
rules:
|
|
11
|
-
|
|
11
|
+
<% domains.each do |domain| -%>
|
|
12
|
+
- host: "<%= domain %>"
|
|
12
13
|
http:
|
|
13
14
|
paths:
|
|
14
15
|
- path: /
|
|
@@ -18,3 +19,4 @@ spec:
|
|
|
18
19
|
name: <%= name %>
|
|
19
20
|
port:
|
|
20
21
|
number: <%= port %>
|
|
22
|
+
<% end -%>
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: nvoi
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.7
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- NVOI
|
|
@@ -173,6 +173,20 @@ executables:
|
|
|
173
173
|
extensions: []
|
|
174
174
|
extra_rdoc_files: []
|
|
175
175
|
files:
|
|
176
|
+
- ".claude/todo/refactor-execution/00-entrypoint.md"
|
|
177
|
+
- ".claude/todo/refactor-execution/01-objects.md"
|
|
178
|
+
- ".claude/todo/refactor-execution/02-utils.md"
|
|
179
|
+
- ".claude/todo/refactor-execution/03-external-cloud.md"
|
|
180
|
+
- ".claude/todo/refactor-execution/04-external-dns.md"
|
|
181
|
+
- ".claude/todo/refactor-execution/05-external-other.md"
|
|
182
|
+
- ".claude/todo/refactor-execution/06-cli-deploy.md"
|
|
183
|
+
- ".claude/todo/refactor-execution/07-cli-delete.md"
|
|
184
|
+
- ".claude/todo/refactor-execution/08-cli-exec.md"
|
|
185
|
+
- ".claude/todo/refactor-execution/09-cli-credentials.md"
|
|
186
|
+
- ".claude/todo/refactor-execution/10-cli-db.md"
|
|
187
|
+
- ".claude/todo/refactor-execution/11-cli-router.md"
|
|
188
|
+
- ".claude/todo/refactor-execution/12-cleanup.md"
|
|
189
|
+
- ".claude/todo/refactor-execution/_monitoring-strategy.md"
|
|
176
190
|
- ".claude/todo/refactor/00-overview.md"
|
|
177
191
|
- ".claude/todo/refactor/01-objects.md"
|
|
178
192
|
- ".claude/todo/refactor/02-utils.md"
|
|
@@ -185,9 +199,11 @@ files:
|
|
|
185
199
|
- ".claude/todo/refactor/09-cli-delete-command.md"
|
|
186
200
|
- ".claude/todo/refactor/10-cli-exec-command.md"
|
|
187
201
|
- ".claude/todo/refactor/11-cli-credentials-command.md"
|
|
202
|
+
- ".claude/todo/refactor/12-cli-db-command.md"
|
|
188
203
|
- ".claude/todo/refactor/_target.md"
|
|
189
204
|
- ".claude/todo/scaleway.impl.md"
|
|
190
205
|
- ".claude/todo/scaleway.reference.md"
|
|
206
|
+
- ".claude/todos.md"
|
|
191
207
|
- ".rubocop.yml"
|
|
192
208
|
- Gemfile
|
|
193
209
|
- Gemfile.lock
|
|
@@ -311,8 +327,10 @@ files:
|
|
|
311
327
|
- examples/rails-single/vendor/.keep
|
|
312
328
|
- examples/rails-single/yarn.lock
|
|
313
329
|
- exe/nvoi
|
|
330
|
+
- ingest
|
|
314
331
|
- lib/nvoi.rb
|
|
315
332
|
- lib/nvoi/cli.rb
|
|
333
|
+
- lib/nvoi/cli/config/command.rb
|
|
316
334
|
- lib/nvoi/cli/credentials/edit/command.rb
|
|
317
335
|
- lib/nvoi/cli/credentials/show/command.rb
|
|
318
336
|
- lib/nvoi/cli/db/command.rb
|
|
@@ -334,13 +352,19 @@ files:
|
|
|
334
352
|
- lib/nvoi/cli/deploy/steps/provision_volume.rb
|
|
335
353
|
- lib/nvoi/cli/deploy/steps/setup_k3s.rb
|
|
336
354
|
- lib/nvoi/cli/exec/command.rb
|
|
355
|
+
- lib/nvoi/cli/logs/command.rb
|
|
356
|
+
- lib/nvoi/cli/onboard/command.rb
|
|
357
|
+
- lib/nvoi/cli/unlock/command.rb
|
|
337
358
|
- lib/nvoi/config_api.rb
|
|
338
359
|
- lib/nvoi/config_api/actions/app.rb
|
|
339
360
|
- lib/nvoi/config_api/actions/compute_provider.rb
|
|
340
361
|
- lib/nvoi/config_api/actions/database.rb
|
|
362
|
+
- lib/nvoi/config_api/actions/domain_provider.rb
|
|
341
363
|
- lib/nvoi/config_api/actions/env.rb
|
|
364
|
+
- lib/nvoi/config_api/actions/init.rb
|
|
342
365
|
- lib/nvoi/config_api/actions/secret.rb
|
|
343
366
|
- lib/nvoi/config_api/actions/server.rb
|
|
367
|
+
- lib/nvoi/config_api/actions/service.rb
|
|
344
368
|
- lib/nvoi/config_api/actions/volume.rb
|
|
345
369
|
- lib/nvoi/config_api/base.rb
|
|
346
370
|
- lib/nvoi/config_api/result.rb
|