nvoi 0.1.5 → 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/00-overview.md +171 -0
- data/.claude/todo/refactor/01-objects.md +96 -0
- data/.claude/todo/refactor/02-utils.md +143 -0
- data/.claude/todo/refactor/03-external-cloud.md +164 -0
- data/.claude/todo/refactor/04-external-dns.md +104 -0
- data/.claude/todo/refactor/05-external.md +133 -0
- data/.claude/todo/refactor/06-cli.md +123 -0
- data/.claude/todo/refactor/07-cli-deploy-command.md +177 -0
- data/.claude/todo/refactor/08-cli-deploy-steps.md +201 -0
- data/.claude/todo/refactor/09-cli-delete-command.md +169 -0
- data/.claude/todo/refactor/10-cli-exec-command.md +157 -0
- data/.claude/todo/refactor/11-cli-credentials-command.md +190 -0
- data/.claude/todo/refactor/12-cli-db-command.md +128 -0
- data/.claude/todo/refactor/_target.md +79 -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/todo/scaleway.impl.md +644 -0
- data/.claude/todo/scaleway.reference.md +520 -0
- data/.claude/todos.md +550 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +46 -5
- data/Rakefile +1 -1
- data/doc/config-schema.yaml +44 -11
- data/examples/golang/deploy.enc +0 -0
- data/examples/golang/main.go +18 -0
- data/exe/nvoi +3 -1
- data/ingest +0 -0
- data/lib/nvoi/cli/config/command.rb +219 -0
- data/lib/nvoi/cli/credentials/edit/command.rb +384 -0
- data/lib/nvoi/cli/credentials/show/command.rb +35 -0
- data/lib/nvoi/cli/db/command.rb +308 -0
- data/lib/nvoi/cli/delete/command.rb +75 -0
- data/lib/nvoi/cli/delete/steps/detach_volumes.rb +98 -0
- data/lib/nvoi/cli/delete/steps/teardown_dns.rb +50 -0
- data/lib/nvoi/cli/delete/steps/teardown_firewall.rb +46 -0
- data/lib/nvoi/cli/delete/steps/teardown_network.rb +30 -0
- data/lib/nvoi/cli/delete/steps/teardown_server.rb +50 -0
- data/lib/nvoi/cli/delete/steps/teardown_tunnel.rb +44 -0
- data/lib/nvoi/cli/delete/steps/teardown_volume.rb +61 -0
- data/lib/nvoi/cli/deploy/command.rb +184 -0
- data/lib/nvoi/cli/deploy/steps/build_image.rb +27 -0
- data/lib/nvoi/cli/deploy/steps/cleanup_images.rb +42 -0
- data/lib/nvoi/cli/deploy/steps/configure_tunnel.rb +102 -0
- data/lib/nvoi/cli/deploy/steps/deploy_service.rb +399 -0
- data/lib/nvoi/cli/deploy/steps/provision_network.rb +44 -0
- data/lib/nvoi/cli/deploy/steps/provision_server.rb +143 -0
- data/lib/nvoi/cli/deploy/steps/provision_volume.rb +171 -0
- data/lib/nvoi/cli/deploy/steps/setup_k3s.rb +490 -0
- data/lib/nvoi/cli/exec/command.rb +173 -0
- 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 +339 -141
- data/lib/nvoi/config_api/actions/app.rb +53 -0
- data/lib/nvoi/config_api/actions/compute_provider.rb +55 -0
- data/lib/nvoi/config_api/actions/database.rb +70 -0
- data/lib/nvoi/config_api/actions/domain_provider.rb +40 -0
- data/lib/nvoi/config_api/actions/env.rb +32 -0
- data/lib/nvoi/config_api/actions/init.rb +67 -0
- data/lib/nvoi/config_api/actions/secret.rb +32 -0
- data/lib/nvoi/config_api/actions/server.rb +66 -0
- data/lib/nvoi/config_api/actions/service.rb +52 -0
- data/lib/nvoi/config_api/actions/volume.rb +40 -0
- data/lib/nvoi/config_api/base.rb +38 -0
- data/lib/nvoi/config_api/result.rb +26 -0
- data/lib/nvoi/config_api.rb +93 -0
- data/lib/nvoi/errors.rb +68 -50
- data/lib/nvoi/external/cloud/aws.rb +450 -0
- data/lib/nvoi/external/cloud/base.rb +99 -0
- data/lib/nvoi/external/cloud/factory.rb +48 -0
- data/lib/nvoi/external/cloud/hetzner.rb +402 -0
- data/lib/nvoi/external/cloud/scaleway.rb +559 -0
- data/lib/nvoi/external/cloud.rb +15 -0
- data/lib/nvoi/external/containerd.rb +86 -0
- data/lib/nvoi/external/database/mysql.rb +84 -0
- data/lib/nvoi/external/database/postgres.rb +82 -0
- data/lib/nvoi/external/database/provider.rb +65 -0
- data/lib/nvoi/external/database/sqlite.rb +72 -0
- data/lib/nvoi/external/database.rb +22 -0
- data/lib/nvoi/external/dns/cloudflare.rb +310 -0
- data/lib/nvoi/external/kubectl.rb +65 -0
- data/lib/nvoi/external/ssh.rb +106 -0
- data/lib/nvoi/objects/config_override.rb +60 -0
- data/lib/nvoi/objects/configuration.rb +483 -0
- data/lib/nvoi/objects/database.rb +56 -0
- data/lib/nvoi/objects/dns.rb +14 -0
- data/lib/nvoi/objects/firewall.rb +11 -0
- data/lib/nvoi/objects/network.rb +11 -0
- data/lib/nvoi/objects/server.rb +14 -0
- data/lib/nvoi/objects/service_spec.rb +26 -0
- data/lib/nvoi/objects/tunnel.rb +14 -0
- data/lib/nvoi/objects/volume.rb +17 -0
- data/lib/nvoi/utils/config_loader.rb +172 -0
- data/lib/nvoi/utils/constants.rb +61 -0
- data/lib/nvoi/{credentials/manager.rb → utils/credential_store.rb} +16 -16
- data/lib/nvoi/{credentials → utils}/crypto.rb +8 -5
- data/lib/nvoi/{config → utils}/env_resolver.rb +10 -2
- data/lib/nvoi/utils/logger.rb +84 -0
- data/lib/nvoi/{config/naming.rb → utils/namer.rb} +37 -25
- data/lib/nvoi/{deployer → utils}/retry.rb +23 -3
- data/lib/nvoi/utils/templates.rb +62 -0
- data/lib/nvoi/version.rb +1 -1
- data/lib/nvoi.rb +27 -55
- data/templates/app-ingress.yaml.erb +3 -1
- data/templates/error-backend.yaml.erb +134 -0
- metadata +121 -44
- data/examples/golang/deploy.yml +0 -54
- data/lib/nvoi/cloudflare/client.rb +0 -287
- data/lib/nvoi/config/config.rb +0 -248
- data/lib/nvoi/config/loader.rb +0 -102
- data/lib/nvoi/config/ssh_keys.rb +0 -82
- data/lib/nvoi/config/types.rb +0 -274
- data/lib/nvoi/constants.rb +0 -59
- data/lib/nvoi/credentials/editor.rb +0 -272
- data/lib/nvoi/deployer/cleaner.rb +0 -36
- data/lib/nvoi/deployer/image_builder.rb +0 -23
- data/lib/nvoi/deployer/infrastructure.rb +0 -126
- data/lib/nvoi/deployer/orchestrator.rb +0 -146
- data/lib/nvoi/deployer/service_deployer.rb +0 -311
- data/lib/nvoi/deployer/tunnel_manager.rb +0 -57
- data/lib/nvoi/deployer/types.rb +0 -8
- data/lib/nvoi/k8s/renderer.rb +0 -44
- data/lib/nvoi/k8s/templates.rb +0 -29
- data/lib/nvoi/logger.rb +0 -72
- data/lib/nvoi/providers/aws.rb +0 -403
- data/lib/nvoi/providers/base.rb +0 -111
- data/lib/nvoi/providers/hetzner.rb +0 -288
- data/lib/nvoi/providers/hetzner_client.rb +0 -170
- data/lib/nvoi/remote/docker_manager.rb +0 -203
- data/lib/nvoi/remote/ssh_executor.rb +0 -72
- data/lib/nvoi/remote/volume_manager.rb +0 -103
- data/lib/nvoi/service/delete.rb +0 -234
- data/lib/nvoi/service/deploy.rb +0 -80
- data/lib/nvoi/service/exec.rb +0 -144
- data/lib/nvoi/service/provider.rb +0 -36
- data/lib/nvoi/steps/application_deployer.rb +0 -26
- data/lib/nvoi/steps/database_provisioner.rb +0 -60
- data/lib/nvoi/steps/k3s_cluster_setup.rb +0 -105
- data/lib/nvoi/steps/k3s_provisioner.rb +0 -351
- data/lib/nvoi/steps/server_provisioner.rb +0 -43
- data/lib/nvoi/steps/services_provisioner.rb +0 -29
- data/lib/nvoi/steps/tunnel_configurator.rb +0 -66
- data/lib/nvoi/steps/volume_provisioner.rb +0 -154
data/examples/golang/deploy.yml
DELETED
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
application:
|
|
2
|
-
name: example-golang
|
|
3
|
-
environment: production
|
|
4
|
-
|
|
5
|
-
# Provider configuration
|
|
6
|
-
domain_provider:
|
|
7
|
-
cloudflare:
|
|
8
|
-
api_token: $CLOUDFLARE_API_TOKEN
|
|
9
|
-
account_id: $CLOUDFLARE_ACCOUNT_ID
|
|
10
|
-
|
|
11
|
-
compute_provider:
|
|
12
|
-
hetzner:
|
|
13
|
-
api_token: $HETZNER_API_TOKEN
|
|
14
|
-
server_type: cx22 # Explicit server type
|
|
15
|
-
server_location: fsn1 # Explicit location
|
|
16
|
-
|
|
17
|
-
# Server configuration (single server, master: true is implicit)
|
|
18
|
-
servers:
|
|
19
|
-
master:
|
|
20
|
-
type: cx22
|
|
21
|
-
location: fsn1
|
|
22
|
-
|
|
23
|
-
# Container retention
|
|
24
|
-
keep_count: 2
|
|
25
|
-
|
|
26
|
-
app:
|
|
27
|
-
web:
|
|
28
|
-
servers: [master]
|
|
29
|
-
domain: rb.run # Your domain (must be on Cloudflare)
|
|
30
|
-
subdomain: golang # Subdomain for this app
|
|
31
|
-
port: 3000
|
|
32
|
-
|
|
33
|
-
# Nested health check configuration
|
|
34
|
-
healthcheck:
|
|
35
|
-
type: http
|
|
36
|
-
path: /health
|
|
37
|
-
port: 3000
|
|
38
|
-
interval: 10s
|
|
39
|
-
timeout: 5s
|
|
40
|
-
retries: 3
|
|
41
|
-
|
|
42
|
-
# Volume mounts for SQLite data persistence
|
|
43
|
-
volumes:
|
|
44
|
-
data: /app/data
|
|
45
|
-
|
|
46
|
-
# SQLite database (adapter specified, volume configured at service level)
|
|
47
|
-
database:
|
|
48
|
-
servers: [master]
|
|
49
|
-
adapter: sqlite3
|
|
50
|
-
|
|
51
|
-
env:
|
|
52
|
-
APP_NAME: nvoi-example-app
|
|
53
|
-
LOG_LEVEL: info
|
|
54
|
-
GIN_MODE: release
|
|
@@ -1,287 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "faraday"
|
|
4
|
-
require "base64"
|
|
5
|
-
|
|
6
|
-
module Nvoi
|
|
7
|
-
module Cloudflare
|
|
8
|
-
# Tunnel represents a Cloudflare tunnel
|
|
9
|
-
Tunnel = Struct.new(:id, :name, :token, keyword_init: true)
|
|
10
|
-
|
|
11
|
-
# Zone represents a Cloudflare DNS zone
|
|
12
|
-
Zone = Struct.new(:id, :name, keyword_init: true)
|
|
13
|
-
|
|
14
|
-
# DNSRecord represents a Cloudflare DNS record
|
|
15
|
-
DNSRecord = Struct.new(:id, :type, :name, :content, :proxied, :ttl, keyword_init: true)
|
|
16
|
-
|
|
17
|
-
# Client handles Cloudflare API operations
|
|
18
|
-
class Client
|
|
19
|
-
BASE_URL = "https://api.cloudflare.com/client/v4"
|
|
20
|
-
|
|
21
|
-
def initialize(token, account_id)
|
|
22
|
-
@token = token
|
|
23
|
-
@account_id = account_id
|
|
24
|
-
@conn = Faraday.new(url: BASE_URL) do |f|
|
|
25
|
-
f.request :json
|
|
26
|
-
f.response :json
|
|
27
|
-
f.adapter Faraday.default_adapter
|
|
28
|
-
end
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
# Tunnel operations
|
|
32
|
-
|
|
33
|
-
def create_tunnel(name)
|
|
34
|
-
url = "accounts/#{@account_id}/cfd_tunnel"
|
|
35
|
-
tunnel_secret = generate_tunnel_secret
|
|
36
|
-
|
|
37
|
-
response = post(url, {
|
|
38
|
-
name:,
|
|
39
|
-
tunnel_secret:,
|
|
40
|
-
config_src: "cloudflare"
|
|
41
|
-
})
|
|
42
|
-
|
|
43
|
-
result = response["result"]
|
|
44
|
-
Tunnel.new(
|
|
45
|
-
id: result["id"],
|
|
46
|
-
name: result["name"],
|
|
47
|
-
token: result["token"]
|
|
48
|
-
)
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
def find_tunnel(name)
|
|
52
|
-
url = "accounts/#{@account_id}/cfd_tunnel"
|
|
53
|
-
response = get(url, { name:, is_deleted: false })
|
|
54
|
-
|
|
55
|
-
results = response["result"]
|
|
56
|
-
return nil if results.nil? || results.empty?
|
|
57
|
-
|
|
58
|
-
result = results[0]
|
|
59
|
-
Tunnel.new(
|
|
60
|
-
id: result["id"],
|
|
61
|
-
name: result["name"],
|
|
62
|
-
token: result["token"]
|
|
63
|
-
)
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
def get_tunnel_token(tunnel_id)
|
|
67
|
-
url = "accounts/#{@account_id}/cfd_tunnel/#{tunnel_id}/token"
|
|
68
|
-
response = get(url)
|
|
69
|
-
response["result"]
|
|
70
|
-
end
|
|
71
|
-
|
|
72
|
-
def update_tunnel_configuration(tunnel_id, hostname, service_url)
|
|
73
|
-
url = "accounts/#{@account_id}/cfd_tunnel/#{tunnel_id}/configurations"
|
|
74
|
-
|
|
75
|
-
config = {
|
|
76
|
-
ingress: [
|
|
77
|
-
{
|
|
78
|
-
hostname:,
|
|
79
|
-
service: service_url,
|
|
80
|
-
originRequest: { httpHostHeader: hostname }
|
|
81
|
-
},
|
|
82
|
-
{ service: "http_status:404" }
|
|
83
|
-
]
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
put(url, { config: })
|
|
87
|
-
end
|
|
88
|
-
|
|
89
|
-
def verify_tunnel_configuration(tunnel_id, expected_hostname, expected_service, max_attempts)
|
|
90
|
-
url = "accounts/#{@account_id}/cfd_tunnel/#{tunnel_id}/configurations"
|
|
91
|
-
|
|
92
|
-
max_attempts.times do
|
|
93
|
-
begin
|
|
94
|
-
response = get(url)
|
|
95
|
-
|
|
96
|
-
if response["success"]
|
|
97
|
-
config = response.dig("result", "config")
|
|
98
|
-
config&.dig("ingress")&.each do |rule|
|
|
99
|
-
if rule["hostname"] == expected_hostname && rule["service"] == expected_service
|
|
100
|
-
return true
|
|
101
|
-
end
|
|
102
|
-
end
|
|
103
|
-
end
|
|
104
|
-
rescue StandardError
|
|
105
|
-
# Continue retrying
|
|
106
|
-
end
|
|
107
|
-
|
|
108
|
-
sleep(2)
|
|
109
|
-
end
|
|
110
|
-
|
|
111
|
-
raise TunnelError, "tunnel configuration not propagated after #{max_attempts} attempts"
|
|
112
|
-
end
|
|
113
|
-
|
|
114
|
-
def delete_tunnel(tunnel_id)
|
|
115
|
-
# Clean up all connections first
|
|
116
|
-
connections_url = "accounts/#{@account_id}/cfd_tunnel/#{tunnel_id}/connections"
|
|
117
|
-
begin
|
|
118
|
-
delete(connections_url)
|
|
119
|
-
rescue StandardError
|
|
120
|
-
# Ignore connection cleanup errors
|
|
121
|
-
end
|
|
122
|
-
|
|
123
|
-
# Now delete the tunnel
|
|
124
|
-
url = "accounts/#{@account_id}/cfd_tunnel/#{tunnel_id}"
|
|
125
|
-
delete(url)
|
|
126
|
-
end
|
|
127
|
-
|
|
128
|
-
# DNS operations
|
|
129
|
-
|
|
130
|
-
def find_zone(domain)
|
|
131
|
-
url = "zones"
|
|
132
|
-
response = get(url)
|
|
133
|
-
|
|
134
|
-
results = response["result"]
|
|
135
|
-
return nil unless results
|
|
136
|
-
|
|
137
|
-
zone_data = results.find { |z| z["name"] == domain }
|
|
138
|
-
return nil unless zone_data
|
|
139
|
-
|
|
140
|
-
Zone.new(id: zone_data["id"], name: zone_data["name"])
|
|
141
|
-
end
|
|
142
|
-
|
|
143
|
-
def find_dns_record(zone_id, name, record_type)
|
|
144
|
-
url = "zones/#{zone_id}/dns_records"
|
|
145
|
-
response = get(url)
|
|
146
|
-
|
|
147
|
-
results = response["result"]
|
|
148
|
-
return nil unless results
|
|
149
|
-
|
|
150
|
-
record_data = results.find { |r| r["name"] == name && r["type"] == record_type }
|
|
151
|
-
return nil unless record_data
|
|
152
|
-
|
|
153
|
-
DNSRecord.new(
|
|
154
|
-
id: record_data["id"],
|
|
155
|
-
type: record_data["type"],
|
|
156
|
-
name: record_data["name"],
|
|
157
|
-
content: record_data["content"],
|
|
158
|
-
proxied: record_data["proxied"],
|
|
159
|
-
ttl: record_data["ttl"]
|
|
160
|
-
)
|
|
161
|
-
end
|
|
162
|
-
|
|
163
|
-
def create_dns_record(zone_id, name, record_type, content, proxied: true)
|
|
164
|
-
url = "zones/#{zone_id}/dns_records"
|
|
165
|
-
|
|
166
|
-
response = post(url, {
|
|
167
|
-
type: record_type,
|
|
168
|
-
name:,
|
|
169
|
-
content:,
|
|
170
|
-
proxied:,
|
|
171
|
-
ttl: 1
|
|
172
|
-
})
|
|
173
|
-
|
|
174
|
-
result = response["result"]
|
|
175
|
-
DNSRecord.new(
|
|
176
|
-
id: result["id"],
|
|
177
|
-
type: result["type"],
|
|
178
|
-
name: result["name"],
|
|
179
|
-
content: result["content"],
|
|
180
|
-
proxied: result["proxied"],
|
|
181
|
-
ttl: result["ttl"]
|
|
182
|
-
)
|
|
183
|
-
end
|
|
184
|
-
|
|
185
|
-
def update_dns_record(zone_id, record_id, name, record_type, content, proxied: true)
|
|
186
|
-
url = "zones/#{zone_id}/dns_records/#{record_id}"
|
|
187
|
-
|
|
188
|
-
response = patch(url, {
|
|
189
|
-
type: record_type,
|
|
190
|
-
name:,
|
|
191
|
-
content:,
|
|
192
|
-
proxied:,
|
|
193
|
-
ttl: 1
|
|
194
|
-
})
|
|
195
|
-
|
|
196
|
-
result = response["result"]
|
|
197
|
-
DNSRecord.new(
|
|
198
|
-
id: result["id"],
|
|
199
|
-
type: result["type"],
|
|
200
|
-
name: result["name"],
|
|
201
|
-
content: result["content"],
|
|
202
|
-
proxied: result["proxied"],
|
|
203
|
-
ttl: result["ttl"]
|
|
204
|
-
)
|
|
205
|
-
end
|
|
206
|
-
|
|
207
|
-
def create_or_update_dns_record(zone_id, name, record_type, content, proxied: true)
|
|
208
|
-
existing = find_dns_record(zone_id, name, record_type)
|
|
209
|
-
|
|
210
|
-
if existing
|
|
211
|
-
update_dns_record(zone_id, existing.id, name, record_type, content, proxied:)
|
|
212
|
-
else
|
|
213
|
-
create_dns_record(zone_id, name, record_type, content, proxied:)
|
|
214
|
-
end
|
|
215
|
-
end
|
|
216
|
-
|
|
217
|
-
def delete_dns_record(zone_id, record_id)
|
|
218
|
-
url = "zones/#{zone_id}/dns_records/#{record_id}"
|
|
219
|
-
delete(url)
|
|
220
|
-
end
|
|
221
|
-
|
|
222
|
-
private
|
|
223
|
-
|
|
224
|
-
def get(url, params = {})
|
|
225
|
-
response = @conn.get(url) do |req|
|
|
226
|
-
req.headers["Authorization"] = "Bearer #{@token}"
|
|
227
|
-
req.params = params unless params.empty?
|
|
228
|
-
end
|
|
229
|
-
handle_response(response)
|
|
230
|
-
end
|
|
231
|
-
|
|
232
|
-
def post(url, body)
|
|
233
|
-
response = @conn.post(url) do |req|
|
|
234
|
-
req.headers["Authorization"] = "Bearer #{@token}"
|
|
235
|
-
req.body = body
|
|
236
|
-
end
|
|
237
|
-
handle_response(response)
|
|
238
|
-
end
|
|
239
|
-
|
|
240
|
-
def put(url, body)
|
|
241
|
-
response = @conn.put(url) do |req|
|
|
242
|
-
req.headers["Authorization"] = "Bearer #{@token}"
|
|
243
|
-
req.body = body
|
|
244
|
-
end
|
|
245
|
-
handle_response(response)
|
|
246
|
-
end
|
|
247
|
-
|
|
248
|
-
def patch(url, body)
|
|
249
|
-
response = @conn.patch(url) do |req|
|
|
250
|
-
req.headers["Authorization"] = "Bearer #{@token}"
|
|
251
|
-
req.body = body
|
|
252
|
-
end
|
|
253
|
-
handle_response(response)
|
|
254
|
-
end
|
|
255
|
-
|
|
256
|
-
def delete(url)
|
|
257
|
-
response = @conn.delete(url) do |req|
|
|
258
|
-
req.headers["Authorization"] = "Bearer #{@token}"
|
|
259
|
-
end
|
|
260
|
-
|
|
261
|
-
# 404 is ok for idempotent delete
|
|
262
|
-
return { "success" => true } if response.status == 404
|
|
263
|
-
|
|
264
|
-
handle_response(response)
|
|
265
|
-
end
|
|
266
|
-
|
|
267
|
-
def handle_response(response)
|
|
268
|
-
body = response.body
|
|
269
|
-
|
|
270
|
-
unless body.is_a?(Hash)
|
|
271
|
-
raise CloudflareError, "unexpected response format"
|
|
272
|
-
end
|
|
273
|
-
|
|
274
|
-
unless body["success"]
|
|
275
|
-
errors = body["errors"]&.map { |e| e["message"] }&.join(", ") || "unknown error"
|
|
276
|
-
raise CloudflareError, "API error: #{errors}"
|
|
277
|
-
end
|
|
278
|
-
|
|
279
|
-
body
|
|
280
|
-
end
|
|
281
|
-
|
|
282
|
-
def generate_tunnel_secret
|
|
283
|
-
Base64.strict_encode64(SecureRandom.random_bytes(32))
|
|
284
|
-
end
|
|
285
|
-
end
|
|
286
|
-
end
|
|
287
|
-
end
|
data/lib/nvoi/config/config.rb
DELETED
|
@@ -1,248 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Nvoi
|
|
4
|
-
module Config
|
|
5
|
-
# Configuration holds the complete configuration including deployment config and runtime settings
|
|
6
|
-
class Configuration
|
|
7
|
-
attr_accessor :deploy, :ssh_key_path, :ssh_public_key, :server_name,
|
|
8
|
-
:firewall_name, :network_name, :docker_network_name, :container_prefix
|
|
9
|
-
|
|
10
|
-
def initialize(deploy_config)
|
|
11
|
-
@deploy = deploy_config
|
|
12
|
-
@namer = nil
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
# Returns the ResourceNamer for centralized naming
|
|
16
|
-
def namer
|
|
17
|
-
@namer ||= ResourceNamer.new(self)
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
# Returns environment variables for a specific service
|
|
21
|
-
def env_for_service(service_name)
|
|
22
|
-
resolver = EnvResolver.new(self)
|
|
23
|
-
resolver.env_for_service(service_name)
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
# Validate config structure
|
|
27
|
-
def validate_config
|
|
28
|
-
app = @deploy.application
|
|
29
|
-
|
|
30
|
-
# Validate provider configuration
|
|
31
|
-
validate_providers_config
|
|
32
|
-
|
|
33
|
-
# Validate database secrets (if database configured)
|
|
34
|
-
validate_database_secrets(app.database) if app.database
|
|
35
|
-
|
|
36
|
-
# Auto-inject database environment variables into app services
|
|
37
|
-
inject_database_env_vars
|
|
38
|
-
|
|
39
|
-
# Validate service-to-server bindings
|
|
40
|
-
validate_service_server_bindings
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
# ProviderName returns the name of the configured compute provider
|
|
44
|
-
def provider_name
|
|
45
|
-
return "hetzner" if @deploy.application.compute_provider.hetzner
|
|
46
|
-
return "aws" if @deploy.application.compute_provider.aws
|
|
47
|
-
|
|
48
|
-
""
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
def hetzner
|
|
52
|
-
@deploy.application.compute_provider.hetzner
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
def aws
|
|
56
|
-
@deploy.application.compute_provider.aws
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
def cloudflare
|
|
60
|
-
@deploy.application.domain_provider.cloudflare
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
def keep_count_value
|
|
64
|
-
count = @deploy.application.keep_count
|
|
65
|
-
count && count.positive? ? count : 2
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
private
|
|
69
|
-
|
|
70
|
-
def validate_service_server_bindings
|
|
71
|
-
app = @deploy.application
|
|
72
|
-
|
|
73
|
-
# Collect all defined server names and validate single master
|
|
74
|
-
defined_servers = {}
|
|
75
|
-
master_count = 0
|
|
76
|
-
|
|
77
|
-
app.servers.each do |server_name, server_config|
|
|
78
|
-
defined_servers[server_name] = true
|
|
79
|
-
master_count += 1 if server_config&.master
|
|
80
|
-
end
|
|
81
|
-
|
|
82
|
-
# Require servers to be defined if any services exist
|
|
83
|
-
if app.servers.empty?
|
|
84
|
-
has_services = !app.app.empty? || app.database || !app.services.empty?
|
|
85
|
-
raise ConfigValidationError, "servers must be defined when deploying services" if has_services
|
|
86
|
-
end
|
|
87
|
-
|
|
88
|
-
# Validate master designation
|
|
89
|
-
if app.servers.size > 1
|
|
90
|
-
raise ConfigValidationError, "when multiple servers are defined, exactly one must have master: true" if master_count.zero?
|
|
91
|
-
raise ConfigValidationError, "only one server can have master: true, found #{master_count}" if master_count > 1
|
|
92
|
-
elsif app.servers.size == 1 && master_count > 1
|
|
93
|
-
raise ConfigValidationError, "only one server can have master: true, found #{master_count}"
|
|
94
|
-
end
|
|
95
|
-
|
|
96
|
-
# Validate app services
|
|
97
|
-
app.app.each do |service_name, service_config|
|
|
98
|
-
raise ConfigValidationError, "app.#{service_name}: servers field is required" if service_config.servers.empty?
|
|
99
|
-
|
|
100
|
-
service_config.servers.each do |server_ref|
|
|
101
|
-
unless defined_servers[server_ref]
|
|
102
|
-
raise ConfigValidationError, "app.#{service_name}: references undefined server '#{server_ref}'"
|
|
103
|
-
end
|
|
104
|
-
end
|
|
105
|
-
end
|
|
106
|
-
|
|
107
|
-
# Validate database
|
|
108
|
-
if app.database
|
|
109
|
-
raise ConfigValidationError, "database: servers field is required" if app.database.servers.empty?
|
|
110
|
-
|
|
111
|
-
app.database.servers.each do |server_ref|
|
|
112
|
-
raise ConfigValidationError, "database: references undefined server '#{server_ref}'" unless defined_servers[server_ref]
|
|
113
|
-
end
|
|
114
|
-
end
|
|
115
|
-
|
|
116
|
-
# Validate additional services
|
|
117
|
-
app.services.each do |service_name, service_config|
|
|
118
|
-
raise ConfigValidationError, "services.#{service_name}: servers field is required" if service_config.servers.empty?
|
|
119
|
-
|
|
120
|
-
service_config.servers.each do |server_ref|
|
|
121
|
-
unless defined_servers[server_ref]
|
|
122
|
-
raise ConfigValidationError, "services.#{service_name}: references undefined server '#{server_ref}'"
|
|
123
|
-
end
|
|
124
|
-
end
|
|
125
|
-
end
|
|
126
|
-
end
|
|
127
|
-
|
|
128
|
-
def validate_database_secrets(db)
|
|
129
|
-
adapter = db.adapter&.downcase
|
|
130
|
-
|
|
131
|
-
case adapter
|
|
132
|
-
when "postgres", "postgresql"
|
|
133
|
-
required_keys = %w[POSTGRES_USER POSTGRES_PASSWORD POSTGRES_DB]
|
|
134
|
-
required_keys.each do |key|
|
|
135
|
-
raise ConfigValidationError, "postgres database requires #{key} in secrets" unless db.secrets[key]
|
|
136
|
-
end
|
|
137
|
-
when "mysql"
|
|
138
|
-
required_keys = %w[MYSQL_USER MYSQL_PASSWORD MYSQL_DATABASE]
|
|
139
|
-
required_keys.each do |key|
|
|
140
|
-
raise ConfigValidationError, "mysql database requires #{key} in secrets" unless db.secrets[key]
|
|
141
|
-
end
|
|
142
|
-
when "sqlite3"
|
|
143
|
-
# SQLite doesn't require secrets
|
|
144
|
-
end
|
|
145
|
-
end
|
|
146
|
-
|
|
147
|
-
def validate_providers_config
|
|
148
|
-
app = @deploy.application
|
|
149
|
-
|
|
150
|
-
# Validate domain provider (required)
|
|
151
|
-
unless app.domain_provider.cloudflare
|
|
152
|
-
raise ConfigValidationError, "domain provider required: currently only cloudflare is supported"
|
|
153
|
-
end
|
|
154
|
-
|
|
155
|
-
cf = app.domain_provider.cloudflare
|
|
156
|
-
raise ConfigValidationError, "cloudflare api_token is required" if cf.api_token.nil? || cf.api_token.empty?
|
|
157
|
-
raise ConfigValidationError, "cloudflare account_id is required" if cf.account_id.nil? || cf.account_id.empty?
|
|
158
|
-
|
|
159
|
-
# Validate compute provider (at least one required)
|
|
160
|
-
has_provider = false
|
|
161
|
-
|
|
162
|
-
if app.compute_provider.hetzner
|
|
163
|
-
has_provider = true
|
|
164
|
-
h = app.compute_provider.hetzner
|
|
165
|
-
raise ConfigValidationError, "hetzner api_token is required" if h.api_token.nil? || h.api_token.empty?
|
|
166
|
-
raise ConfigValidationError, "hetzner server_type is required" if h.server_type.nil? || h.server_type.empty?
|
|
167
|
-
raise ConfigValidationError, "hetzner server_location is required" if h.server_location.nil? || h.server_location.empty?
|
|
168
|
-
end
|
|
169
|
-
|
|
170
|
-
if app.compute_provider.aws
|
|
171
|
-
has_provider = true
|
|
172
|
-
a = app.compute_provider.aws
|
|
173
|
-
raise ConfigValidationError, "aws access_key_id is required" if a.access_key_id.nil? || a.access_key_id.empty?
|
|
174
|
-
raise ConfigValidationError, "aws secret_access_key is required" if a.secret_access_key.nil? || a.secret_access_key.empty?
|
|
175
|
-
raise ConfigValidationError, "aws region is required" if a.region.nil? || a.region.empty?
|
|
176
|
-
raise ConfigValidationError, "aws instance_type is required" if a.instance_type.nil? || a.instance_type.empty?
|
|
177
|
-
end
|
|
178
|
-
|
|
179
|
-
raise ConfigValidationError, "compute provider required: hetzner or aws must be configured" unless has_provider
|
|
180
|
-
end
|
|
181
|
-
|
|
182
|
-
def inject_database_env_vars
|
|
183
|
-
app = @deploy.application
|
|
184
|
-
|
|
185
|
-
# Skip if no database configured
|
|
186
|
-
return unless app.database
|
|
187
|
-
|
|
188
|
-
db = app.database
|
|
189
|
-
adapter = db.adapter&.downcase
|
|
190
|
-
|
|
191
|
-
# SQLite doesn't need network connection vars
|
|
192
|
-
return if adapter == "sqlite3"
|
|
193
|
-
|
|
194
|
-
# Build connection variables
|
|
195
|
-
db_host = namer.database_service_name
|
|
196
|
-
db_port, db_user, db_password, db_name = extract_db_credentials(adapter, db)
|
|
197
|
-
|
|
198
|
-
return unless db_user && db_password && db_name
|
|
199
|
-
|
|
200
|
-
# Build DATABASE_URL
|
|
201
|
-
database_url = case adapter
|
|
202
|
-
when "postgres", "postgresql"
|
|
203
|
-
"postgresql://#{db_user}:#{db_password}@#{db_host}:#{db_port}/#{db_name}"
|
|
204
|
-
when "mysql"
|
|
205
|
-
"mysql://#{db_user}:#{db_password}@#{db_host}:#{db_port}/#{db_name}"
|
|
206
|
-
end
|
|
207
|
-
|
|
208
|
-
# Inject into all app services
|
|
209
|
-
app.app.each_value do |service_config|
|
|
210
|
-
service_config.env ||= {}
|
|
211
|
-
|
|
212
|
-
# Only inject if not already set by user
|
|
213
|
-
service_config.env["DATABASE_URL"] ||= database_url
|
|
214
|
-
|
|
215
|
-
case adapter
|
|
216
|
-
when "postgres", "postgresql"
|
|
217
|
-
unless service_config.env.key?("POSTGRES_HOST")
|
|
218
|
-
service_config.env["POSTGRES_HOST"] = db_host
|
|
219
|
-
service_config.env["POSTGRES_PORT"] = db_port
|
|
220
|
-
service_config.env["POSTGRES_USER"] = db_user
|
|
221
|
-
service_config.env["POSTGRES_PASSWORD"] = db_password
|
|
222
|
-
service_config.env["POSTGRES_DB"] = db_name
|
|
223
|
-
end
|
|
224
|
-
when "mysql"
|
|
225
|
-
unless service_config.env.key?("MYSQL_HOST")
|
|
226
|
-
service_config.env["MYSQL_HOST"] = db_host
|
|
227
|
-
service_config.env["MYSQL_PORT"] = db_port
|
|
228
|
-
service_config.env["MYSQL_USER"] = db_user
|
|
229
|
-
service_config.env["MYSQL_PASSWORD"] = db_password
|
|
230
|
-
service_config.env["MYSQL_DATABASE"] = db_name
|
|
231
|
-
end
|
|
232
|
-
end
|
|
233
|
-
end
|
|
234
|
-
end
|
|
235
|
-
|
|
236
|
-
def extract_db_credentials(adapter, db)
|
|
237
|
-
case adapter
|
|
238
|
-
when "postgres", "postgresql"
|
|
239
|
-
["5432", db.secrets["POSTGRES_USER"], db.secrets["POSTGRES_PASSWORD"], db.secrets["POSTGRES_DB"]]
|
|
240
|
-
when "mysql"
|
|
241
|
-
["3306", db.secrets["MYSQL_USER"], db.secrets["MYSQL_PASSWORD"], db.secrets["MYSQL_DATABASE"]]
|
|
242
|
-
else
|
|
243
|
-
[nil, nil, nil, nil]
|
|
244
|
-
end
|
|
245
|
-
end
|
|
246
|
-
end
|
|
247
|
-
end
|
|
248
|
-
end
|
data/lib/nvoi/config/loader.rb
DELETED
|
@@ -1,102 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Nvoi
|
|
4
|
-
module Config
|
|
5
|
-
# ConfigLoader handles loading and initializing configuration
|
|
6
|
-
class ConfigLoader
|
|
7
|
-
attr_accessor :credentials_path, :master_key_path
|
|
8
|
-
|
|
9
|
-
def initialize
|
|
10
|
-
@credentials_path = nil
|
|
11
|
-
@master_key_path = nil
|
|
12
|
-
end
|
|
13
|
-
|
|
14
|
-
# Set explicit path to encrypted credentials file
|
|
15
|
-
def with_credentials_path(path)
|
|
16
|
-
@credentials_path = path
|
|
17
|
-
self
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
# Set explicit path to master key file
|
|
21
|
-
def with_master_key_path(path)
|
|
22
|
-
@master_key_path = path
|
|
23
|
-
self
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
# Load reads and parses the deployment configuration from encrypted file
|
|
27
|
-
def load(config_path)
|
|
28
|
-
# Determine working directory
|
|
29
|
-
working_dir = config_path && !config_path.empty? ? File.dirname(config_path) : "."
|
|
30
|
-
|
|
31
|
-
# Use explicit credentials path or derive from config_path
|
|
32
|
-
enc_path = @credentials_path
|
|
33
|
-
enc_path = config_path if enc_path.nil? || enc_path.empty?
|
|
34
|
-
|
|
35
|
-
# Create credentials manager
|
|
36
|
-
manager = Credentials::Manager.new(working_dir, enc_path, @master_key_path)
|
|
37
|
-
|
|
38
|
-
# Decrypt credentials
|
|
39
|
-
plaintext = manager.read
|
|
40
|
-
raise ConfigError, "Failed to decrypt credentials" unless plaintext
|
|
41
|
-
|
|
42
|
-
# Parse YAML
|
|
43
|
-
data = YAML.safe_load(plaintext, permitted_classes: [Symbol])
|
|
44
|
-
raise ConfigError, "Invalid config format" unless data.is_a?(Hash)
|
|
45
|
-
|
|
46
|
-
deploy_config = DeployConfig.new(data)
|
|
47
|
-
|
|
48
|
-
# Create config
|
|
49
|
-
cfg = Configuration.new(deploy_config)
|
|
50
|
-
|
|
51
|
-
# Load SSH keys from config content
|
|
52
|
-
key_loader = SSHKeyLoader.new(cfg)
|
|
53
|
-
key_loader.load_keys
|
|
54
|
-
|
|
55
|
-
# Validate config structure
|
|
56
|
-
cfg.validate_config
|
|
57
|
-
|
|
58
|
-
# Generate resource names
|
|
59
|
-
namer = ResourceNamer.new(cfg)
|
|
60
|
-
cfg.container_prefix = namer.infer_container_prefix
|
|
61
|
-
master_group = find_master_server_group(cfg)
|
|
62
|
-
cfg.server_name = namer.server_name(master_group, 1)
|
|
63
|
-
cfg.firewall_name = namer.firewall_name
|
|
64
|
-
cfg.network_name = namer.network_name
|
|
65
|
-
cfg.docker_network_name = namer.docker_network_name
|
|
66
|
-
|
|
67
|
-
cfg
|
|
68
|
-
end
|
|
69
|
-
|
|
70
|
-
private
|
|
71
|
-
|
|
72
|
-
def find_master_server_group(cfg)
|
|
73
|
-
servers = cfg.deploy.application.servers
|
|
74
|
-
return "master" if servers.empty?
|
|
75
|
-
|
|
76
|
-
# Find explicit master
|
|
77
|
-
servers.each do |name, server_cfg|
|
|
78
|
-
return name if server_cfg&.master
|
|
79
|
-
end
|
|
80
|
-
|
|
81
|
-
# Single server group: use it as master
|
|
82
|
-
return servers.keys.first if servers.size == 1
|
|
83
|
-
|
|
84
|
-
# Fallback
|
|
85
|
-
"master"
|
|
86
|
-
end
|
|
87
|
-
end
|
|
88
|
-
|
|
89
|
-
# Module-level load function
|
|
90
|
-
def self.load(config_path)
|
|
91
|
-
ConfigLoader.new.load(config_path)
|
|
92
|
-
end
|
|
93
|
-
|
|
94
|
-
# Module-level load with explicit key paths
|
|
95
|
-
def self.load_with_keys(config_path, credentials_path, master_key_path)
|
|
96
|
-
ConfigLoader.new
|
|
97
|
-
.with_credentials_path(credentials_path)
|
|
98
|
-
.with_master_key_path(master_key_path)
|
|
99
|
-
.load(config_path)
|
|
100
|
-
end
|
|
101
|
-
end
|
|
102
|
-
end
|