nvoi 0.1.8 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/Gemfile +1 -5
- data/Gemfile.lock +17 -8
- data/Rakefile +1 -1
- data/_TODO-rails-example.md +816 -0
- data/_TODO-rails-optimization.md +433 -0
- data/doc/config-schema.yaml +12 -0
- data/examples/apex-wildcard/deploy.yml +1 -0
- data/examples/golang-postgres-multi/deploy.yml +1 -0
- data/examples/postgres-multi/deploy.yml +1 -0
- data/examples/postgres-single/deploy.yml +1 -0
- data/examples/rails-single/deploy.yml +1 -0
- data/lib/nvoi/cli/config/command.rb +46 -41
- data/lib/nvoi/cli/credentials/edit/command.rb +24 -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/build_image.rb +2 -1
- data/lib/nvoi/cli/deploy/steps/configure_tunnel.rb +2 -2
- data/lib/nvoi/cli/deploy/steps/deploy_service.rb +7 -4
- 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 +186 -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 +2 -33
- data/lib/nvoi/configuration/app_service.rb +54 -0
- data/lib/nvoi/configuration/application.rb +44 -0
- data/lib/nvoi/configuration/builder.rb +420 -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 +81 -0
- data/lib/nvoi/configuration/result.rb +43 -0
- data/lib/nvoi/configuration/root.rb +252 -0
- data/lib/nvoi/configuration/server.rb +39 -0
- data/lib/nvoi/configuration/service.rb +51 -0
- data/lib/nvoi/configuration/ssh_key.rb +16 -0
- data/lib/nvoi/external/cloud/aws.rb +26 -16
- data/lib/nvoi/external/cloud/hetzner.rb +40 -25
- data/lib/nvoi/external/cloud/scaleway.rb +10 -8
- 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 +11 -11
- 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 +8 -3
- data/lib/nvoi/utils/presence.rb +23 -0
- data/lib/nvoi/version.rb +1 -1
- data/lib/nvoi.rb +2 -17
- metadata +98 -59
- 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/Makefile +0 -26
- 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
|
@@ -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
|
|
@@ -313,11 +313,13 @@ module Nvoi
|
|
|
313
313
|
# List available server types for onboarding
|
|
314
314
|
def list_server_types
|
|
315
315
|
list_server_types_api.map do |name, info|
|
|
316
|
+
arch = info.dig("arch") || "x86_64"
|
|
316
317
|
{
|
|
317
318
|
name:,
|
|
318
319
|
cores: info.dig("ncpus"),
|
|
319
320
|
ram: info.dig("ram"),
|
|
320
|
-
hourly_price: info.dig("hourly_price")
|
|
321
|
+
hourly_price: info.dig("hourly_price"),
|
|
322
|
+
architecture: arch.include?("arm") ? "arm64" : "x86"
|
|
321
323
|
}
|
|
322
324
|
end
|
|
323
325
|
end
|
|
@@ -511,7 +513,7 @@ module Nvoi
|
|
|
511
513
|
end
|
|
512
514
|
|
|
513
515
|
def to_network(data)
|
|
514
|
-
|
|
516
|
+
Types::Network::Record.new(
|
|
515
517
|
id: data["id"],
|
|
516
518
|
name: data["name"],
|
|
517
519
|
ip_range: data.dig("subnets", 0, "subnet") || data["subnets"]&.first
|
|
@@ -519,7 +521,7 @@ module Nvoi
|
|
|
519
521
|
end
|
|
520
522
|
|
|
521
523
|
def to_firewall(data)
|
|
522
|
-
|
|
524
|
+
Types::Firewall::Record.new(
|
|
523
525
|
id: data["id"],
|
|
524
526
|
name: data["name"]
|
|
525
527
|
)
|
|
@@ -529,7 +531,7 @@ module Nvoi
|
|
|
529
531
|
# Scaleway doesn't include private_ips in the NIC response directly
|
|
530
532
|
# We'd need to call IPAM API which adds complexity
|
|
531
533
|
# Instead, private IP discovery happens via SSH in setup_k3s
|
|
532
|
-
|
|
534
|
+
Types::Server::Record.new(
|
|
533
535
|
id: data["id"],
|
|
534
536
|
name: data["name"],
|
|
535
537
|
status: data["state"],
|
|
@@ -543,7 +545,7 @@ module Nvoi
|
|
|
543
545
|
r["product_resource_type"] == "instance_server"
|
|
544
546
|
}&.dig("product_resource_id")
|
|
545
547
|
|
|
546
|
-
|
|
548
|
+
Types::Volume::Record.new(
|
|
547
549
|
id: data["id"],
|
|
548
550
|
name: data["name"],
|
|
549
551
|
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
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Nvoi
|
|
4
|
+
module External
|
|
5
|
+
module Database
|
|
6
|
+
module Types
|
|
7
|
+
# Parsed credentials from database URL
|
|
8
|
+
Credentials = Struct.new(:user, :password, :host, :port, :database, :path, :host_path, keyword_init: true)
|
|
9
|
+
|
|
10
|
+
# Options for dumping a database
|
|
11
|
+
DumpOptions = Struct.new(:pod_name, :database, :user, :password, :host_path, keyword_init: true)
|
|
12
|
+
|
|
13
|
+
# Options for restoring a database
|
|
14
|
+
RestoreOptions = Struct.new(:pod_name, :database, :user, :password, :source_db, :host_path, keyword_init: true)
|
|
15
|
+
|
|
16
|
+
# Options for creating a database
|
|
17
|
+
CreateOptions = Struct.new(:pod_name, :database, :user, :password, keyword_init: true)
|
|
18
|
+
|
|
19
|
+
# Branch represents a database branch (snapshot)
|
|
20
|
+
Branch = Struct.new(:id, :created_at, :size, :adapter, :database, keyword_init: true) do
|
|
21
|
+
def to_h
|
|
22
|
+
{ id:, created_at:, size:, adapter:, database: }
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# BranchMetadata holds all branches for an app
|
|
27
|
+
class BranchMetadata
|
|
28
|
+
attr_accessor :branches
|
|
29
|
+
|
|
30
|
+
def initialize(branches = [])
|
|
31
|
+
@branches = branches
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def to_json(*_args)
|
|
35
|
+
JSON.pretty_generate({ branches: @branches.map(&:to_h) })
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def self.from_json(json_str)
|
|
39
|
+
data = JSON.parse(json_str)
|
|
40
|
+
branches = (data["branches"] || []).map do |b|
|
|
41
|
+
Branch.new(
|
|
42
|
+
id: b["id"],
|
|
43
|
+
created_at: b["created_at"],
|
|
44
|
+
size: b["size"],
|
|
45
|
+
adapter: b["adapter"],
|
|
46
|
+
database: b["database"]
|
|
47
|
+
)
|
|
48
|
+
end
|
|
49
|
+
new(branches)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -36,7 +36,7 @@ module Nvoi
|
|
|
36
36
|
})
|
|
37
37
|
|
|
38
38
|
result = response["result"]
|
|
39
|
-
|
|
39
|
+
Types::Tunnel::Record.new(
|
|
40
40
|
id: result["id"],
|
|
41
41
|
name: result["name"],
|
|
42
42
|
token: result["token"]
|
|
@@ -51,7 +51,7 @@ module Nvoi
|
|
|
51
51
|
return nil if results.nil? || results.empty?
|
|
52
52
|
|
|
53
53
|
result = results[0]
|
|
54
|
-
|
|
54
|
+
Types::Tunnel::Record.new(
|
|
55
55
|
id: result["id"],
|
|
56
56
|
name: result["name"],
|
|
57
57
|
token: result["token"]
|
|
@@ -69,11 +69,11 @@ module Nvoi
|
|
|
69
69
|
url = "accounts/#{@account_id}/cfd_tunnel/#{tunnel_id}/configurations"
|
|
70
70
|
|
|
71
71
|
ingress_rules = hostnames.map do |hostname|
|
|
72
|
-
{
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
72
|
+
rule = { hostname:, service: service_url }
|
|
73
|
+
# Only set httpHostHeader for non-wildcard hostnames
|
|
74
|
+
# Wildcards should pass through the original Host header
|
|
75
|
+
rule[:originRequest] = { httpHostHeader: hostname } unless hostname.start_with?("*.")
|
|
76
|
+
rule
|
|
77
77
|
end
|
|
78
78
|
ingress_rules << { service: "http_status:404" }
|
|
79
79
|
|
|
@@ -144,7 +144,7 @@ module Nvoi
|
|
|
144
144
|
zone_data = results.find { |z| z["name"] == domain }
|
|
145
145
|
return nil unless zone_data
|
|
146
146
|
|
|
147
|
-
|
|
147
|
+
Types::Zone.new(id: zone_data["id"], name: zone_data["name"])
|
|
148
148
|
end
|
|
149
149
|
|
|
150
150
|
def subdomain_available?(zone_id, subdomain, domain)
|
|
@@ -163,7 +163,7 @@ module Nvoi
|
|
|
163
163
|
record_data = results.find { |r| r["name"] == name && r["type"] == record_type }
|
|
164
164
|
return nil unless record_data
|
|
165
165
|
|
|
166
|
-
|
|
166
|
+
Types::Record.new(
|
|
167
167
|
id: record_data["id"],
|
|
168
168
|
type: record_data["type"],
|
|
169
169
|
name: record_data["name"],
|
|
@@ -185,7 +185,7 @@ module Nvoi
|
|
|
185
185
|
})
|
|
186
186
|
|
|
187
187
|
result = response["result"]
|
|
188
|
-
|
|
188
|
+
Types::Record.new(
|
|
189
189
|
id: result["id"],
|
|
190
190
|
type: result["type"],
|
|
191
191
|
name: result["name"],
|
|
@@ -207,7 +207,7 @@ module Nvoi
|
|
|
207
207
|
})
|
|
208
208
|
|
|
209
209
|
result = response["result"]
|
|
210
|
-
|
|
210
|
+
Types::Record.new(
|
|
211
211
|
id: result["id"],
|
|
212
212
|
type: result["type"],
|
|
213
213
|
name: result["name"],
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Nvoi
|
|
4
|
+
module External
|
|
5
|
+
module Dns
|
|
6
|
+
module Types
|
|
7
|
+
# Zone represents a Cloudflare DNS zone
|
|
8
|
+
Zone = Struct.new(:id, :name, keyword_init: true)
|
|
9
|
+
|
|
10
|
+
# Record represents a Cloudflare DNS record
|
|
11
|
+
Record = Struct.new(:id, :type, :name, :content, :proxied, :ttl, keyword_init: true)
|
|
12
|
+
|
|
13
|
+
# Tunnel-related structs (Cloudflare tunnels)
|
|
14
|
+
module Tunnel
|
|
15
|
+
# Record represents a Cloudflare tunnel
|
|
16
|
+
Record = Struct.new(:id, :name, :token, keyword_init: true)
|
|
17
|
+
|
|
18
|
+
# Info holds information about a configured tunnel
|
|
19
|
+
Info = Struct.new(:service_name, :hostname, :tunnel_id, :tunnel_token, :port, keyword_init: true)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -10,8 +10,8 @@ module Nvoi
|
|
|
10
10
|
class << self
|
|
11
11
|
# Load reads and parses the deployment configuration from encrypted file
|
|
12
12
|
def load(config_path, credentials_path: nil, master_key_path: nil)
|
|
13
|
-
working_dir = config_path
|
|
14
|
-
enc_path = credentials_path.
|
|
13
|
+
working_dir = config_path.blank? ? "." : File.dirname(config_path)
|
|
14
|
+
enc_path = credentials_path.blank? ? config_path : credentials_path
|
|
15
15
|
|
|
16
16
|
manager = CredentialStore.new(working_dir, enc_path, master_key_path)
|
|
17
17
|
plaintext = manager.read
|
|
@@ -20,8 +20,8 @@ module Nvoi
|
|
|
20
20
|
data = YAML.safe_load(plaintext, permitted_classes: [Symbol])
|
|
21
21
|
raise Errors::ConfigError, "Invalid config format" unless data.is_a?(Hash)
|
|
22
22
|
|
|
23
|
-
deploy_config =
|
|
24
|
-
cfg =
|
|
23
|
+
deploy_config = Configuration::Deploy.new(data)
|
|
24
|
+
cfg = Configuration::Root.new(deploy_config)
|
|
25
25
|
|
|
26
26
|
load_ssh_keys(cfg)
|
|
27
27
|
cfg.validate_config
|
|
@@ -39,7 +39,7 @@ module Nvoi
|
|
|
39
39
|
|
|
40
40
|
provider = External::Database.provider_for(adapter)
|
|
41
41
|
|
|
42
|
-
|
|
42
|
+
unless db_config.url.blank?
|
|
43
43
|
creds = provider.parse_url(db_config.url)
|
|
44
44
|
host_path = nil
|
|
45
45
|
|
|
@@ -47,7 +47,7 @@ module Nvoi
|
|
|
47
47
|
host_path = resolve_sqlite_host_path(db_config, namer, creds.database || "app.db")
|
|
48
48
|
end
|
|
49
49
|
|
|
50
|
-
return
|
|
50
|
+
return External::Database::Types::Credentials.new(
|
|
51
51
|
user: creds.user,
|
|
52
52
|
password: creds.password,
|
|
53
53
|
host: creds.host,
|
|
@@ -61,21 +61,21 @@ module Nvoi
|
|
|
61
61
|
# Fall back to secrets-based credentials
|
|
62
62
|
case adapter
|
|
63
63
|
when "postgres", "postgresql"
|
|
64
|
-
|
|
64
|
+
External::Database::Types::Credentials.new(
|
|
65
65
|
port: provider.default_port,
|
|
66
66
|
user: db_config.secrets["POSTGRES_USER"],
|
|
67
67
|
password: db_config.secrets["POSTGRES_PASSWORD"],
|
|
68
68
|
database: db_config.secrets["POSTGRES_DB"]
|
|
69
69
|
)
|
|
70
70
|
when "mysql", "mysql2"
|
|
71
|
-
|
|
71
|
+
External::Database::Types::Credentials.new(
|
|
72
72
|
port: provider.default_port,
|
|
73
73
|
user: db_config.secrets["MYSQL_USER"],
|
|
74
74
|
password: db_config.secrets["MYSQL_PASSWORD"],
|
|
75
75
|
database: db_config.secrets["MYSQL_DATABASE"]
|
|
76
76
|
)
|
|
77
77
|
when "sqlite", "sqlite3"
|
|
78
|
-
|
|
78
|
+
External::Database::Types::Credentials.new(
|
|
79
79
|
database: "app.db",
|
|
80
80
|
host_path: resolve_sqlite_host_path(db_config, namer, "app.db")
|
|
81
81
|
)
|
|
@@ -93,8 +93,8 @@ module Nvoi
|
|
|
93
93
|
raise Errors::ConfigError, "ssh_keys section is required in config. Run 'nvoi credentials edit' to generate keys."
|
|
94
94
|
end
|
|
95
95
|
|
|
96
|
-
raise Errors::ConfigError, "ssh_keys.private_key is required"
|
|
97
|
-
raise Errors::ConfigError, "ssh_keys.public_key is required"
|
|
96
|
+
raise Errors::ConfigError, "ssh_keys.private_key is required" if ssh_keys.private_key.blank?
|
|
97
|
+
raise Errors::ConfigError, "ssh_keys.public_key is required" if ssh_keys.public_key.blank?
|
|
98
98
|
|
|
99
99
|
temp_dir = Dir.mktmpdir("nvoi-ssh-")
|
|
100
100
|
|
|
@@ -136,7 +136,7 @@ module Nvoi
|
|
|
136
136
|
server_name = db_config.servers.first
|
|
137
137
|
mount = db_config.mount
|
|
138
138
|
|
|
139
|
-
|
|
139
|
+
unless mount.blank?
|
|
140
140
|
vol_name = mount.keys.first
|
|
141
141
|
base_path = namer.server_volume_host_path(server_name, vol_name)
|
|
142
142
|
return "#{base_path}/#{filename}"
|
|
@@ -17,7 +17,7 @@ module Nvoi
|
|
|
17
17
|
# key_path: explicit path to key file (optional, nil = auto-discover)
|
|
18
18
|
def initialize(working_dir, encrypted_path = nil, key_path = nil)
|
|
19
19
|
@working_dir = working_dir
|
|
20
|
-
@encrypted_path = encrypted_path
|
|
20
|
+
@encrypted_path = encrypted_path.blank? ? find_encrypted_file : encrypted_path
|
|
21
21
|
@key_path = nil
|
|
22
22
|
@master_key = nil
|
|
23
23
|
|
|
@@ -41,7 +41,7 @@ module Nvoi
|
|
|
41
41
|
|
|
42
42
|
# Check if the store has a master key loaded
|
|
43
43
|
def has_key?
|
|
44
|
-
!@master_key.
|
|
44
|
+
!@master_key.blank?
|
|
45
45
|
end
|
|
46
46
|
|
|
47
47
|
# Decrypt and return the credentials content
|
|
@@ -131,7 +131,7 @@ module Nvoi
|
|
|
131
131
|
|
|
132
132
|
def resolve_key(explicit_key_path)
|
|
133
133
|
# Priority 1: Explicit key file path
|
|
134
|
-
|
|
134
|
+
unless explicit_key_path.blank?
|
|
135
135
|
@master_key = load_key_from_file(explicit_key_path)
|
|
136
136
|
@key_path = explicit_key_path
|
|
137
137
|
return
|
|
@@ -139,7 +139,7 @@ module Nvoi
|
|
|
139
139
|
|
|
140
140
|
# Priority 2: Environment variable
|
|
141
141
|
env_key = ENV[MASTER_KEY_ENV_VAR]
|
|
142
|
-
|
|
142
|
+
unless env_key.blank?
|
|
143
143
|
Crypto.validate_key(env_key)
|
|
144
144
|
@master_key = env_key
|
|
145
145
|
return
|
|
@@ -44,12 +44,12 @@ module Nvoi
|
|
|
44
44
|
db = @config.deploy.application.database
|
|
45
45
|
return unless db
|
|
46
46
|
|
|
47
|
-
env["DATABASE_ADAPTER"] = db.adapter
|
|
47
|
+
env["DATABASE_ADAPTER"] = db.adapter unless db.adapter.blank?
|
|
48
48
|
|
|
49
49
|
# Handle database URL
|
|
50
50
|
if db.adapter == "sqlite3"
|
|
51
51
|
env["DATABASE_URL"] = sqlite_database_url(db)
|
|
52
|
-
elsif
|
|
52
|
+
elsif !db.url.blank?
|
|
53
53
|
env["DATABASE_URL"] = db.url
|
|
54
54
|
end
|
|
55
55
|
|
|
@@ -60,7 +60,7 @@ module Nvoi
|
|
|
60
60
|
end
|
|
61
61
|
|
|
62
62
|
def sqlite_database_url(db)
|
|
63
|
-
raise Errors::ConfigError, "sqlite3 requires database.mount to be configured" if db.mount.
|
|
63
|
+
raise Errors::ConfigError, "sqlite3 requires database.mount to be configured" if db.mount.blank?
|
|
64
64
|
|
|
65
65
|
mount_path = db.mount.values.first
|
|
66
66
|
app_name = @config.deploy.application.name
|
data/lib/nvoi/utils/namer.rb
CHANGED
|
@@ -33,7 +33,7 @@ module Nvoi
|
|
|
33
33
|
|
|
34
34
|
# ServerName returns the server name for a given group and index
|
|
35
35
|
def server_name(group, index)
|
|
36
|
-
"#{@config.deploy.application.name}-#{group}-#{index}"
|
|
36
|
+
"#{sanitize_name(@config.deploy.application.name)}-#{group}-#{index}"
|
|
37
37
|
end
|
|
38
38
|
|
|
39
39
|
def firewall_name
|
|
@@ -178,7 +178,7 @@ module Nvoi
|
|
|
178
178
|
|
|
179
179
|
# Class method for building hostname without instance
|
|
180
180
|
def self.build_hostname(subdomain, domain)
|
|
181
|
-
if subdomain.
|
|
181
|
+
if subdomain.blank? || subdomain == "@"
|
|
182
182
|
domain
|
|
183
183
|
else
|
|
184
184
|
"#{subdomain}.#{domain}"
|
|
@@ -187,7 +187,7 @@ module Nvoi
|
|
|
187
187
|
|
|
188
188
|
# Returns array of hostnames - apex returns [domain, *.domain], subdomain returns [sub.domain]
|
|
189
189
|
def self.build_hostnames(subdomain, domain)
|
|
190
|
-
if subdomain.
|
|
190
|
+
if subdomain.blank? || subdomain == "@"
|
|
191
191
|
[domain, "*.#{domain}"]
|
|
192
192
|
else
|
|
193
193
|
["#{subdomain}.#{domain}"]
|
|
@@ -196,6 +196,11 @@ module Nvoi
|
|
|
196
196
|
|
|
197
197
|
private
|
|
198
198
|
|
|
199
|
+
# Sanitize name for cloud provider compatibility (no underscores, lowercase, etc.)
|
|
200
|
+
def sanitize_name(name)
|
|
201
|
+
name.to_s.gsub("_", "-").downcase
|
|
202
|
+
end
|
|
203
|
+
|
|
199
204
|
def hash_string(str)
|
|
200
205
|
Digest::SHA256.hexdigest(str)[0, 16]
|
|
201
206
|
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Core extensions for blank?/present? checks
|
|
4
|
+
|
|
5
|
+
class NilClass
|
|
6
|
+
def blank? = true
|
|
7
|
+
def present? = false
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
class String
|
|
11
|
+
def blank? = empty?
|
|
12
|
+
def present? = !empty?
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
class Array
|
|
16
|
+
def blank? = empty?
|
|
17
|
+
def present? = !empty?
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
class Hash
|
|
21
|
+
def blank? = empty?
|
|
22
|
+
def present? = !empty?
|
|
23
|
+
end
|
data/lib/nvoi/version.rb
CHANGED
data/lib/nvoi.rb
CHANGED
|
@@ -13,26 +13,11 @@ require "fileutils"
|
|
|
13
13
|
require "tempfile"
|
|
14
14
|
require "open3"
|
|
15
15
|
require "faraday"
|
|
16
|
+
require_relative "nvoi/utils/presence"
|
|
16
17
|
|
|
17
18
|
loader = Zeitwerk::Loader.for_gem
|
|
18
|
-
loader.ignore("#{__dir__}/nvoi/cli") # CLI commands are lazy-loaded
|
|
19
|
-
loader.ignore("#{__dir__}/nvoi/config_api") # ConfigApi uses non-standard naming
|
|
20
19
|
loader.setup
|
|
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"
|
|
20
|
+
loader.eager_load_namespace(Nvoi::Cli)
|
|
36
21
|
|
|
37
22
|
module Nvoi
|
|
38
23
|
class << self
|