nvoi 0.1.7 → 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 +29 -13
- data/lib/nvoi/cli/deploy/steps/build_image.rb +48 -6
- data/lib/nvoi/cli/deploy/steps/configure_tunnel.rb +2 -2
- data/lib/nvoi/cli/deploy/steps/deploy_service.rb +3 -13
- 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/containerd.rb +1 -48
- 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/external/ssh.rb +0 -12
- data/lib/nvoi/external/ssh_tunnel.rb +100 -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 +96 -57
- 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.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
|
@@ -2,9 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
require "stringio"
|
|
4
4
|
require "tty-prompt"
|
|
5
|
-
require "tty-box"
|
|
6
|
-
require "tty-spinner"
|
|
7
|
-
require "tty-table"
|
|
8
5
|
require "nvoi/utils/credential_store"
|
|
9
6
|
|
|
10
7
|
module Nvoi
|
|
@@ -12,536 +9,126 @@ module Nvoi
|
|
|
12
9
|
module Onboard
|
|
13
10
|
# Interactive onboarding wizard for quick setup
|
|
14
11
|
class Command
|
|
15
|
-
|
|
12
|
+
include Onboard::Ui
|
|
13
|
+
|
|
14
|
+
SUMMARY_ACTIONS = [
|
|
15
|
+
{ name: "Save configuration", value: :save },
|
|
16
|
+
{ name: "Edit application name", value: :app_name },
|
|
17
|
+
{ name: "Edit compute provider", value: :compute },
|
|
18
|
+
{ name: "Edit domain provider", value: :domain },
|
|
19
|
+
{ name: "Edit apps", value: :apps },
|
|
20
|
+
{ name: "Edit database", value: :database },
|
|
21
|
+
{ name: "Edit environment variables", value: :env },
|
|
22
|
+
{ name: "Start over", value: :restart },
|
|
23
|
+
{ name: "Cancel (discard)", value: :cancel }
|
|
24
|
+
].freeze
|
|
16
25
|
|
|
17
26
|
def initialize(prompt: nil)
|
|
18
27
|
@prompt = prompt || TTY::Prompt.new
|
|
19
|
-
@data = { "application" => {} }
|
|
20
28
|
@test_mode = prompt&.input.is_a?(StringIO)
|
|
29
|
+
@data = default_data
|
|
30
|
+
@domain_step = nil
|
|
21
31
|
end
|
|
22
32
|
|
|
23
33
|
def run
|
|
24
34
|
show_welcome
|
|
25
|
-
|
|
26
|
-
step_app_name
|
|
27
|
-
step_compute_provider
|
|
28
|
-
step_domain_provider
|
|
29
|
-
step_apps
|
|
30
|
-
step_database
|
|
31
|
-
step_env
|
|
32
|
-
|
|
35
|
+
collect_all
|
|
33
36
|
summary_loop
|
|
34
|
-
|
|
35
37
|
show_next_steps
|
|
36
38
|
rescue TTY::Reader::InputInterrupt
|
|
37
|
-
puts "\n\nSetup cancelled."
|
|
39
|
+
output.puts "\n\nSetup cancelled."
|
|
38
40
|
exit 1
|
|
39
41
|
end
|
|
40
42
|
|
|
41
43
|
private
|
|
42
44
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
)
|
|
54
|
-
puts box
|
|
55
|
-
puts
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
# ─────────────────────────────────────────────────────────────────
|
|
59
|
-
# Step 1: App Name
|
|
60
|
-
# ─────────────────────────────────────────────────────────────────
|
|
61
|
-
|
|
62
|
-
def step_app_name
|
|
63
|
-
name = @prompt.ask("Application name:") do |q|
|
|
64
|
-
q.required true
|
|
65
|
-
q.validate(/\A[a-z0-9_-]+\z/i, "Only letters, numbers, dashes, underscores")
|
|
66
|
-
end
|
|
67
|
-
@data["application"]["name"] = name
|
|
68
|
-
end
|
|
69
|
-
|
|
70
|
-
# ─────────────────────────────────────────────────────────────────
|
|
71
|
-
# Step 2: Compute Provider
|
|
72
|
-
# ─────────────────────────────────────────────────────────────────
|
|
73
|
-
|
|
74
|
-
def step_compute_provider
|
|
75
|
-
puts
|
|
76
|
-
puts section("Compute Provider")
|
|
77
|
-
|
|
78
|
-
provider = @prompt.select("Select provider:") do |menu|
|
|
79
|
-
menu.choice "Hetzner (recommended)", :hetzner
|
|
80
|
-
menu.choice "AWS", :aws
|
|
81
|
-
menu.choice "Scaleway", :scaleway
|
|
82
|
-
end
|
|
83
|
-
|
|
84
|
-
case provider
|
|
85
|
-
when :hetzner then setup_hetzner
|
|
86
|
-
when :aws then setup_aws
|
|
87
|
-
when :scaleway then setup_scaleway
|
|
88
|
-
end
|
|
89
|
-
end
|
|
90
|
-
|
|
91
|
-
def setup_hetzner
|
|
92
|
-
token = prompt_with_retry("Hetzner API Token:", mask: true) do |t|
|
|
93
|
-
client = External::Cloud::Hetzner.new(t)
|
|
94
|
-
client.validate_credentials
|
|
95
|
-
@hetzner_client = client
|
|
96
|
-
end
|
|
97
|
-
|
|
98
|
-
types, locations = with_spinner("Fetching options...") do
|
|
99
|
-
[@hetzner_client.list_server_types, @hetzner_client.list_locations]
|
|
100
|
-
end
|
|
101
|
-
|
|
102
|
-
type_choices = types.sort_by { |t| t[:name] }.map do |t|
|
|
103
|
-
price = t[:price] ? " - #{t[:price]}/mo" : ""
|
|
104
|
-
{ name: "#{t[:name]} (#{t[:cores]} vCPU, #{t[:memory] / 1024}GB#{price})", value: t[:name] }
|
|
105
|
-
end
|
|
106
|
-
|
|
107
|
-
location_choices = locations.map do |l|
|
|
108
|
-
{ name: "#{l[:name]} (#{l[:city]}, #{l[:country]})", value: l[:name] }
|
|
109
|
-
end
|
|
110
|
-
|
|
111
|
-
server_type = @prompt.select("Server type:", type_choices, per_page: 10)
|
|
112
|
-
location = @prompt.select("Location:", location_choices)
|
|
113
|
-
|
|
114
|
-
@data["application"]["compute_provider"] = {
|
|
115
|
-
"hetzner" => {
|
|
116
|
-
"api_token" => token,
|
|
117
|
-
"server_type" => server_type,
|
|
118
|
-
"server_location" => location
|
|
119
|
-
}
|
|
45
|
+
def default_data
|
|
46
|
+
{
|
|
47
|
+
name: nil,
|
|
48
|
+
compute: nil,
|
|
49
|
+
domain: nil,
|
|
50
|
+
apps: {},
|
|
51
|
+
database: nil,
|
|
52
|
+
volumes: nil,
|
|
53
|
+
env: {},
|
|
54
|
+
secrets: {}
|
|
120
55
|
}
|
|
121
56
|
end
|
|
122
57
|
|
|
123
|
-
def
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
end
|
|
127
|
-
|
|
128
|
-
secret_key = @prompt.mask("AWS Secret Access Key:")
|
|
129
|
-
|
|
130
|
-
# Get regions first with temp client
|
|
131
|
-
temp_client = External::Cloud::Aws.new(access_key, secret_key, "us-east-1")
|
|
132
|
-
regions = with_spinner("Validating credentials...") do
|
|
133
|
-
temp_client.validate_credentials
|
|
134
|
-
temp_client.list_regions
|
|
135
|
-
end
|
|
136
|
-
|
|
137
|
-
region_choices = regions.map { |r| r[:name] }.sort
|
|
138
|
-
region = @prompt.select("Region:", region_choices, per_page: 10, filter: true)
|
|
139
|
-
|
|
140
|
-
# Now get instance types for selected region
|
|
141
|
-
client = External::Cloud::Aws.new(access_key, secret_key, region)
|
|
142
|
-
types = client.list_instance_types
|
|
143
|
-
|
|
144
|
-
type_choices = types.map do |t|
|
|
145
|
-
mem = t[:memory] ? " #{t[:memory] / 1024}GB" : ""
|
|
146
|
-
{ name: "#{t[:name]} (#{t[:vcpus]} vCPU#{mem})", value: t[:name] }
|
|
147
|
-
end
|
|
58
|
+
def collect_all
|
|
59
|
+
@data[:name] = Steps::AppName.new(@prompt, test_mode: @test_mode).call
|
|
60
|
+
@data[:compute] = Steps::Compute.new(@prompt, test_mode: @test_mode).call
|
|
148
61
|
|
|
149
|
-
|
|
62
|
+
@domain_step = Steps::Domain.new(@prompt, test_mode: @test_mode)
|
|
63
|
+
@data[:domain] = @domain_step.call
|
|
150
64
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
"secret_access_key" => secret_key,
|
|
155
|
-
"region" => region,
|
|
156
|
-
"instance_type" => instance_type
|
|
157
|
-
}
|
|
158
|
-
}
|
|
65
|
+
collect_apps
|
|
66
|
+
collect_database
|
|
67
|
+
collect_env
|
|
159
68
|
end
|
|
160
69
|
|
|
161
|
-
def
|
|
162
|
-
|
|
163
|
-
project_id = @prompt.ask("Scaleway Project ID:") { |q| q.required true }
|
|
164
|
-
|
|
165
|
-
# Get zones (static list)
|
|
166
|
-
temp_client = External::Cloud::Scaleway.new(secret_key, project_id)
|
|
167
|
-
zones = temp_client.list_zones
|
|
168
|
-
|
|
169
|
-
zone_choices = zones.map { |z| { name: "#{z[:name]} (#{z[:city]})", value: z[:name] } }
|
|
170
|
-
zone = @prompt.select("Zone:", zone_choices)
|
|
171
|
-
|
|
172
|
-
# Validate and get server types
|
|
173
|
-
client = External::Cloud::Scaleway.new(secret_key, project_id, zone:)
|
|
174
|
-
types = with_spinner("Validating credentials...") do
|
|
175
|
-
client.validate_credentials
|
|
176
|
-
client.list_server_types
|
|
177
|
-
end
|
|
178
|
-
|
|
179
|
-
type_choices = types.map do |t|
|
|
180
|
-
{ name: "#{t[:name]} (#{t[:cores]} cores)", value: t[:name] }
|
|
181
|
-
end
|
|
182
|
-
|
|
183
|
-
server_type = @prompt.select("Server type:", type_choices, per_page: 10, filter: true)
|
|
184
|
-
|
|
185
|
-
@data["application"]["compute_provider"] = {
|
|
186
|
-
"scaleway" => {
|
|
187
|
-
"secret_key" => secret_key,
|
|
188
|
-
"project_id" => project_id,
|
|
189
|
-
"zone" => zone,
|
|
190
|
-
"server_type" => server_type
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
end
|
|
194
|
-
|
|
195
|
-
# ─────────────────────────────────────────────────────────────────
|
|
196
|
-
# Step 3: Domain Provider
|
|
197
|
-
# ─────────────────────────────────────────────────────────────────
|
|
198
|
-
|
|
199
|
-
def step_domain_provider
|
|
200
|
-
puts
|
|
201
|
-
puts section("Domain Provider")
|
|
202
|
-
|
|
203
|
-
setup = @prompt.yes?("Configure Cloudflare for domains/tunnels?")
|
|
204
|
-
return unless setup
|
|
205
|
-
|
|
206
|
-
token = prompt_with_retry("Cloudflare API Token:", mask: true)
|
|
207
|
-
account_id = @prompt.ask("Cloudflare Account ID:") { |q| q.required true }
|
|
208
|
-
|
|
209
|
-
@cloudflare_client = External::Dns::Cloudflare.new(token, account_id)
|
|
210
|
-
|
|
211
|
-
@cloudflare_zones = with_spinner("Fetching domains...") do
|
|
212
|
-
@cloudflare_client.validate_credentials
|
|
213
|
-
@cloudflare_client.list_zones.select { |z| z[:status] == "active" }
|
|
214
|
-
end
|
|
215
|
-
|
|
216
|
-
if @cloudflare_zones.empty?
|
|
217
|
-
warn "No active domains found in Cloudflare account"
|
|
218
|
-
end
|
|
219
|
-
|
|
220
|
-
@data["application"]["domain_provider"] = {
|
|
221
|
-
"cloudflare" => {
|
|
222
|
-
"api_token" => token,
|
|
223
|
-
"account_id" => account_id
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
end
|
|
227
|
-
|
|
228
|
-
# ─────────────────────────────────────────────────────────────────
|
|
229
|
-
# Step 4: Apps (loop)
|
|
230
|
-
# ─────────────────────────────────────────────────────────────────
|
|
231
|
-
|
|
232
|
-
def step_apps
|
|
233
|
-
puts
|
|
234
|
-
puts section("Applications")
|
|
235
|
-
|
|
236
|
-
# Ensure we have a server
|
|
237
|
-
@data["application"]["servers"] ||= {}
|
|
238
|
-
@data["application"]["servers"]["main"] = { "master" => true, "count" => 1 }
|
|
239
|
-
|
|
240
|
-
@data["application"]["app"] ||= {}
|
|
70
|
+
def collect_apps
|
|
71
|
+
section "Applications"
|
|
241
72
|
|
|
242
73
|
loop do
|
|
243
|
-
name = @prompt
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
app_config = { "servers" => ["main"] }
|
|
249
|
-
app_config["command"] = command unless command.to_s.empty?
|
|
250
|
-
app_config["port"] = port if port && port > 0
|
|
251
|
-
|
|
252
|
-
# Domain selection only if port is set (web-facing) and Cloudflare configured
|
|
253
|
-
if port && port > 0 && @cloudflare_zones&.any?
|
|
254
|
-
domain, subdomain = prompt_domain_selection
|
|
255
|
-
if domain
|
|
256
|
-
app_config["domain"] = domain
|
|
257
|
-
app_config["subdomain"] = subdomain unless subdomain.to_s.empty?
|
|
258
|
-
end
|
|
259
|
-
end
|
|
260
|
-
|
|
261
|
-
pre_run = @prompt.ask("Pre-run command (e.g. migrations):")
|
|
262
|
-
app_config["pre_run_command"] = pre_run unless pre_run.to_s.empty?
|
|
263
|
-
|
|
264
|
-
@data["application"]["app"][name] = app_config
|
|
74
|
+
name, config = Steps::App.new(@prompt, test_mode: @test_mode).call(
|
|
75
|
+
zones: @domain_step&.zones || [],
|
|
76
|
+
cloudflare_client: @domain_step&.client
|
|
77
|
+
)
|
|
78
|
+
@data[:apps][name] = config
|
|
265
79
|
|
|
266
80
|
break unless @prompt.yes?("Add another app?")
|
|
267
81
|
end
|
|
268
82
|
end
|
|
269
83
|
|
|
270
|
-
def
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
selected = @prompt.select("Domain:", domain_choices)
|
|
275
|
-
return [nil, nil] unless selected
|
|
276
|
-
|
|
277
|
-
zone_id = selected[:id]
|
|
278
|
-
domain = selected[:name]
|
|
279
|
-
|
|
280
|
-
# Prompt for subdomain with validation
|
|
281
|
-
subdomain = prompt_subdomain(zone_id, domain)
|
|
282
|
-
|
|
283
|
-
[domain, subdomain]
|
|
284
|
-
end
|
|
285
|
-
|
|
286
|
-
def prompt_subdomain(zone_id, domain)
|
|
287
|
-
loop do
|
|
288
|
-
subdomain = @prompt.ask("Subdomain (leave blank for #{domain} + *.#{domain}):")
|
|
289
|
-
subdomain = subdomain.to_s.strip.downcase
|
|
84
|
+
def collect_database
|
|
85
|
+
db_config, volume_config = Steps::Database.new(@prompt, test_mode: @test_mode)
|
|
86
|
+
.call(app_name: @data[:name])
|
|
290
87
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
error("Invalid subdomain format. Use lowercase letters, numbers, and hyphens.")
|
|
294
|
-
next
|
|
295
|
-
end
|
|
296
|
-
|
|
297
|
-
# Check availability for all hostnames that will be created
|
|
298
|
-
hostnames = Utils::Namer.build_hostnames(subdomain.empty? ? nil : subdomain, domain)
|
|
299
|
-
all_available = true
|
|
300
|
-
|
|
301
|
-
hostnames.each do |hostname|
|
|
302
|
-
available = with_spinner("Checking #{hostname}...") do
|
|
303
|
-
check_subdomain = hostname == domain ? "" : hostname.sub(".#{domain}", "")
|
|
304
|
-
@cloudflare_client.subdomain_available?(zone_id, check_subdomain, domain)
|
|
305
|
-
end
|
|
306
|
-
|
|
307
|
-
unless available
|
|
308
|
-
error("#{hostname} already has a DNS record. Choose a different subdomain.")
|
|
309
|
-
all_available = false
|
|
310
|
-
break
|
|
311
|
-
end
|
|
312
|
-
end
|
|
313
|
-
|
|
314
|
-
return subdomain if all_available
|
|
315
|
-
end
|
|
88
|
+
@data[:database] = db_config
|
|
89
|
+
@data[:volumes] = volume_config
|
|
316
90
|
end
|
|
317
91
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
def step_database
|
|
323
|
-
puts
|
|
324
|
-
puts section("Database")
|
|
325
|
-
|
|
326
|
-
adapter = @prompt.select("Database:") do |menu|
|
|
327
|
-
menu.choice "PostgreSQL", "postgres"
|
|
328
|
-
menu.choice "MySQL", "mysql"
|
|
329
|
-
menu.choice "SQLite", "sqlite3"
|
|
330
|
-
menu.choice "None (skip)", nil
|
|
331
|
-
end
|
|
332
|
-
|
|
333
|
-
return unless adapter
|
|
334
|
-
|
|
335
|
-
db_config = {
|
|
336
|
-
"servers" => ["main"],
|
|
337
|
-
"adapter" => adapter
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
case adapter
|
|
341
|
-
when "postgres"
|
|
342
|
-
db_name = @prompt.ask("Database name:", default: "#{@data["application"]["name"]}_production")
|
|
343
|
-
user = @prompt.ask("Database user:", default: @data["application"]["name"])
|
|
344
|
-
password = @prompt.mask("Database password:") { |q| q.required true }
|
|
345
|
-
|
|
346
|
-
db_config["secrets"] = {
|
|
347
|
-
"POSTGRES_DB" => db_name,
|
|
348
|
-
"POSTGRES_USER" => user,
|
|
349
|
-
"POSTGRES_PASSWORD" => password
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
# Auto-add volume
|
|
353
|
-
@data["application"]["servers"]["main"]["volumes"] = {
|
|
354
|
-
"postgres_data" => { "size" => 10 }
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
when "mysql"
|
|
358
|
-
db_name = @prompt.ask("Database name:", default: "#{@data["application"]["name"]}_production")
|
|
359
|
-
user = @prompt.ask("Database user:", default: @data["application"]["name"])
|
|
360
|
-
password = @prompt.mask("Database password:") { |q| q.required true }
|
|
361
|
-
|
|
362
|
-
db_config["secrets"] = {
|
|
363
|
-
"MYSQL_DATABASE" => db_name,
|
|
364
|
-
"MYSQL_USER" => user,
|
|
365
|
-
"MYSQL_PASSWORD" => password
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
# Auto-add volume
|
|
369
|
-
@data["application"]["servers"]["main"]["volumes"] = {
|
|
370
|
-
"mysql_data" => { "size" => 10 }
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
when "sqlite3"
|
|
374
|
-
path = @prompt.ask("Database path:", default: "/app/data/production.sqlite3")
|
|
375
|
-
db_config["path"] = path
|
|
376
|
-
db_config["mount"] = { "data" => "/app/data" }
|
|
377
|
-
|
|
378
|
-
# Auto-add volume
|
|
379
|
-
@data["application"]["servers"]["main"]["volumes"] = {
|
|
380
|
-
"sqlite_data" => { "size" => 10 }
|
|
381
|
-
}
|
|
382
|
-
end
|
|
383
|
-
|
|
384
|
-
@data["application"]["database"] = db_config
|
|
92
|
+
def collect_env
|
|
93
|
+
@data[:env], @data[:secrets] = Steps::Env.new(@prompt, test_mode: @test_mode)
|
|
94
|
+
.call(existing_env: @data[:env], existing_secrets: @data[:secrets])
|
|
385
95
|
end
|
|
386
96
|
|
|
387
|
-
#
|
|
388
|
-
# Step 6: Environment Variables
|
|
389
|
-
# ─────────────────────────────────────────────────────────────────
|
|
390
|
-
|
|
391
|
-
def step_env
|
|
392
|
-
puts
|
|
393
|
-
puts section("Environment Variables")
|
|
394
|
-
|
|
395
|
-
@data["application"]["env"] ||= {}
|
|
396
|
-
@data["application"]["secrets"] ||= {}
|
|
397
|
-
|
|
398
|
-
# Add default
|
|
399
|
-
@data["application"]["env"]["RAILS_ENV"] = "production"
|
|
400
|
-
|
|
401
|
-
loop do
|
|
402
|
-
show_env_table
|
|
403
|
-
|
|
404
|
-
choice = @prompt.select("Action:") do |menu|
|
|
405
|
-
menu.choice "Add variable", :add
|
|
406
|
-
menu.choice "Add secret (masked)", :secret
|
|
407
|
-
menu.choice "Done", :done
|
|
408
|
-
end
|
|
409
|
-
|
|
410
|
-
case choice
|
|
411
|
-
when :add
|
|
412
|
-
key = @prompt.ask("Variable name:") { |q| q.required true }
|
|
413
|
-
value = @prompt.ask("Value:") { |q| q.required true }
|
|
414
|
-
@data["application"]["env"][key] = value
|
|
415
|
-
|
|
416
|
-
when :secret
|
|
417
|
-
key = @prompt.ask("Secret name:") { |q| q.required true }
|
|
418
|
-
value = @prompt.mask("Value:") { |q| q.required true }
|
|
419
|
-
@data["application"]["secrets"][key] = value
|
|
420
|
-
|
|
421
|
-
when :done
|
|
422
|
-
break
|
|
423
|
-
end
|
|
424
|
-
end
|
|
425
|
-
end
|
|
426
|
-
|
|
427
|
-
def show_env_table
|
|
428
|
-
return if @data["application"]["env"].empty? && @data["application"]["secrets"].empty?
|
|
429
|
-
|
|
430
|
-
rows = []
|
|
431
|
-
@data["application"]["env"].each { |k, v| rows << [k, v] }
|
|
432
|
-
@data["application"]["secrets"].each { |k, _| rows << [k, "********"] }
|
|
433
|
-
|
|
434
|
-
table = TTY::Table.new(header: %w[Key Value], rows:)
|
|
435
|
-
puts table.render(:unicode, padding: [0, 1])
|
|
436
|
-
puts
|
|
437
|
-
end
|
|
438
|
-
|
|
439
|
-
# ─────────────────────────────────────────────────────────────────
|
|
440
|
-
# Summary & Save
|
|
441
|
-
# ─────────────────────────────────────────────────────────────────
|
|
442
|
-
|
|
443
|
-
def show_summary
|
|
444
|
-
puts
|
|
445
|
-
puts section("Summary")
|
|
446
|
-
|
|
447
|
-
provider_name = @data["application"]["compute_provider"]&.keys&.first || "none"
|
|
448
|
-
provider_info = case provider_name
|
|
449
|
-
when "hetzner"
|
|
450
|
-
cfg = @data["application"]["compute_provider"]["hetzner"]
|
|
451
|
-
"#{cfg["server_type"]} @ #{cfg["server_location"]}"
|
|
452
|
-
when "aws"
|
|
453
|
-
cfg = @data["application"]["compute_provider"]["aws"]
|
|
454
|
-
"#{cfg["instance_type"]} @ #{cfg["region"]}"
|
|
455
|
-
when "scaleway"
|
|
456
|
-
cfg = @data["application"]["compute_provider"]["scaleway"]
|
|
457
|
-
"#{cfg["server_type"]} @ #{cfg["zone"]}"
|
|
458
|
-
else
|
|
459
|
-
"not configured"
|
|
460
|
-
end
|
|
461
|
-
|
|
462
|
-
domain_ok = @data["application"]["domain_provider"]&.any? ? "configured" : "not configured"
|
|
463
|
-
|
|
464
|
-
# Build app list with domains
|
|
465
|
-
app_list = @data["application"]["app"]&.map do |name, cfg|
|
|
466
|
-
if cfg["domain"]
|
|
467
|
-
fqdn = cfg["subdomain"] ? "#{cfg["subdomain"]}.#{cfg["domain"]}" : cfg["domain"]
|
|
468
|
-
"#{name} (#{fqdn})"
|
|
469
|
-
else
|
|
470
|
-
name
|
|
471
|
-
end
|
|
472
|
-
end&.join(", ") || "none"
|
|
473
|
-
db = @data["application"]["database"]&.dig("adapter") || "none"
|
|
474
|
-
env_count = (@data["application"]["env"]&.size || 0) + (@data["application"]["secrets"]&.size || 0)
|
|
475
|
-
|
|
476
|
-
rows = [
|
|
477
|
-
["Application", @data["application"]["name"]],
|
|
478
|
-
["Provider", "#{provider_name} (#{provider_info})"],
|
|
479
|
-
["Domain", "Cloudflare #{domain_ok}"],
|
|
480
|
-
["Apps", app_list],
|
|
481
|
-
["Database", db],
|
|
482
|
-
["Env/Secrets", "#{env_count} variables"]
|
|
483
|
-
]
|
|
484
|
-
|
|
485
|
-
table = TTY::Table.new(rows:)
|
|
486
|
-
puts table.render(:unicode, padding: [0, 1])
|
|
487
|
-
puts
|
|
488
|
-
end
|
|
97
|
+
# ─── Summary Loop ───
|
|
489
98
|
|
|
490
99
|
def summary_loop
|
|
491
100
|
loop do
|
|
492
101
|
show_summary
|
|
493
102
|
|
|
494
|
-
|
|
495
|
-
menu.choice "Save configuration", :save
|
|
496
|
-
menu.choice "Edit application name", :app_name
|
|
497
|
-
menu.choice "Edit compute provider", :compute
|
|
498
|
-
menu.choice "Edit domain provider", :domain
|
|
499
|
-
menu.choice "Edit apps", :apps
|
|
500
|
-
menu.choice "Edit database", :database
|
|
501
|
-
menu.choice "Edit environment variables", :env
|
|
502
|
-
menu.choice "Start over", :restart
|
|
503
|
-
menu.choice "Cancel (discard)", :cancel
|
|
504
|
-
end
|
|
505
|
-
|
|
506
|
-
case action
|
|
103
|
+
case @prompt.select("What would you like to do?", SUMMARY_ACTIONS)
|
|
507
104
|
when :save
|
|
508
105
|
save_config
|
|
509
106
|
return
|
|
510
107
|
when :cancel
|
|
511
108
|
return if @prompt.yes?("Discard all changes?")
|
|
512
109
|
when :app_name
|
|
513
|
-
|
|
110
|
+
@data[:name] = Steps::AppName.new(@prompt, test_mode: @test_mode)
|
|
111
|
+
.call(existing: @data[:name])
|
|
514
112
|
when :compute
|
|
515
|
-
|
|
113
|
+
@data[:compute] = Steps::Compute.new(@prompt, test_mode: @test_mode).call
|
|
516
114
|
when :domain
|
|
517
|
-
|
|
115
|
+
@domain_step = Steps::Domain.new(@prompt, test_mode: @test_mode)
|
|
116
|
+
@data[:domain] = @domain_step.call
|
|
518
117
|
when :apps
|
|
519
118
|
edit_apps
|
|
520
119
|
when :database
|
|
521
|
-
|
|
120
|
+
collect_database
|
|
522
121
|
when :env
|
|
523
|
-
|
|
122
|
+
collect_env
|
|
524
123
|
when :restart
|
|
525
|
-
|
|
526
|
-
@data = { "application" => {} }
|
|
527
|
-
@cloudflare_client = nil
|
|
528
|
-
@cloudflare_zones = nil
|
|
529
|
-
step_app_name
|
|
530
|
-
step_compute_provider
|
|
531
|
-
step_domain_provider
|
|
532
|
-
step_apps
|
|
533
|
-
step_database
|
|
534
|
-
step_env
|
|
535
|
-
end
|
|
124
|
+
restart_wizard
|
|
536
125
|
end
|
|
537
126
|
end
|
|
538
127
|
end
|
|
539
128
|
|
|
540
129
|
def edit_apps
|
|
541
130
|
loop do
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
choices = app_names.map { |name| { name:, value: name } }
|
|
131
|
+
choices = @data[:apps].keys.map { |name| { name:, value: name } }
|
|
545
132
|
choices << { name: "Add new app", value: :add }
|
|
546
133
|
choices << { name: "Done", value: :done }
|
|
547
134
|
|
|
@@ -549,195 +136,157 @@ module Nvoi
|
|
|
549
136
|
|
|
550
137
|
case selected
|
|
551
138
|
when :add
|
|
552
|
-
|
|
139
|
+
name, config = Steps::App.new(@prompt, test_mode: @test_mode).call(
|
|
140
|
+
zones: @domain_step&.zones || [],
|
|
141
|
+
cloudflare_client: @domain_step&.client
|
|
142
|
+
)
|
|
143
|
+
@data[:apps][name] = config
|
|
553
144
|
when :done
|
|
554
145
|
return
|
|
555
146
|
else
|
|
556
|
-
|
|
557
|
-
app_action = @prompt.select("#{selected}:") do |menu|
|
|
558
|
-
menu.choice "Edit", :edit
|
|
559
|
-
menu.choice "Delete", :delete
|
|
560
|
-
menu.choice "Back", :back
|
|
561
|
-
end
|
|
562
|
-
|
|
563
|
-
case app_action
|
|
564
|
-
when :edit
|
|
565
|
-
edit_single_app(selected)
|
|
566
|
-
when :delete
|
|
567
|
-
@data["application"]["app"].delete(selected) if @prompt.yes?("Delete #{selected}?")
|
|
568
|
-
end
|
|
147
|
+
edit_single_app(selected)
|
|
569
148
|
end
|
|
570
149
|
end
|
|
571
150
|
end
|
|
572
151
|
|
|
573
152
|
def edit_single_app(name)
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
new_config["command"] = command unless command.to_s.empty?
|
|
593
|
-
new_config["port"] = port if port && port > 0
|
|
594
|
-
|
|
595
|
-
# Domain selection only if port is set (web-facing)
|
|
596
|
-
if port && port > 0 && @cloudflare_zones&.any?
|
|
597
|
-
domain, subdomain = prompt_domain_selection
|
|
598
|
-
if domain
|
|
599
|
-
new_config["domain"] = domain
|
|
600
|
-
new_config["subdomain"] = subdomain unless subdomain.to_s.empty?
|
|
601
|
-
end
|
|
153
|
+
action = @prompt.select("#{name}:") do |menu|
|
|
154
|
+
menu.choice "Edit", :edit
|
|
155
|
+
menu.choice "Delete", :delete
|
|
156
|
+
menu.choice "Back", :back
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
case action
|
|
160
|
+
when :edit
|
|
161
|
+
new_name, config = Steps::App.new(@prompt, test_mode: @test_mode).call(
|
|
162
|
+
existing_name: name,
|
|
163
|
+
existing: @data[:apps][name],
|
|
164
|
+
zones: @domain_step&.zones || [],
|
|
165
|
+
cloudflare_client: @domain_step&.client
|
|
166
|
+
)
|
|
167
|
+
@data[:apps].delete(name) if new_name != name
|
|
168
|
+
@data[:apps][new_name] = config
|
|
169
|
+
when :delete
|
|
170
|
+
@data[:apps].delete(name) if @prompt.yes?("Delete #{name}?")
|
|
602
171
|
end
|
|
172
|
+
end
|
|
603
173
|
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
@prompt.ask("Pre-run command (optional):", default: existing_pre_run)
|
|
607
|
-
else
|
|
608
|
-
@prompt.ask("Pre-run command (optional):")
|
|
609
|
-
end
|
|
610
|
-
new_config["pre_run_command"] = pre_run unless pre_run.to_s.empty?
|
|
174
|
+
def restart_wizard
|
|
175
|
+
return unless @prompt.yes?("This will clear all data. Continue?")
|
|
611
176
|
|
|
612
|
-
|
|
613
|
-
@
|
|
614
|
-
|
|
177
|
+
@data = default_data
|
|
178
|
+
@domain_step = nil
|
|
179
|
+
collect_all
|
|
615
180
|
end
|
|
616
181
|
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
182
|
+
# ─── Summary Display ───
|
|
183
|
+
|
|
184
|
+
def show_welcome
|
|
185
|
+
box "NVOI Quick Setup"
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def show_summary
|
|
189
|
+
section "Summary"
|
|
190
|
+
|
|
191
|
+
rows = [
|
|
192
|
+
["Application", @data[:name]],
|
|
193
|
+
["Provider", format_provider],
|
|
194
|
+
["Domain", format_domain],
|
|
195
|
+
["Apps", format_apps],
|
|
196
|
+
["Database", @data[:database]&.dig("adapter") || "none"],
|
|
197
|
+
["Env/Secrets", "#{@data[:env].size + @data[:secrets].size} variables"]
|
|
198
|
+
]
|
|
199
|
+
|
|
200
|
+
table(rows:)
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def format_provider
|
|
204
|
+
return "not configured" unless @data[:compute]
|
|
205
|
+
|
|
206
|
+
provider_name = @data[:compute].keys.first
|
|
207
|
+
cfg = @data[:compute][provider_name]
|
|
208
|
+
|
|
209
|
+
info = case provider_name
|
|
210
|
+
when "hetzner" then "#{cfg["server_type"]} @ #{cfg["server_location"]}"
|
|
211
|
+
when "aws" then "#{cfg["instance_type"]} @ #{cfg["region"]}"
|
|
212
|
+
when "scaleway" then "#{cfg["server_type"]} @ #{cfg["zone"]}"
|
|
213
|
+
else "configured"
|
|
634
214
|
end
|
|
635
215
|
|
|
636
|
-
|
|
637
|
-
|
|
216
|
+
"#{provider_name} (#{info})"
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def format_domain
|
|
220
|
+
@data[:domain] ? "Cloudflare configured" : "Cloudflare not configured"
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def format_apps
|
|
224
|
+
return "none" if @data[:apps].empty?
|
|
638
225
|
|
|
639
|
-
@data[
|
|
226
|
+
@data[:apps].map do |name, cfg|
|
|
227
|
+
if cfg["domain"]
|
|
228
|
+
fqdn = cfg["subdomain"] ? "#{cfg["subdomain"]}.#{cfg["domain"]}" : cfg["domain"]
|
|
229
|
+
"#{name} (#{fqdn})"
|
|
230
|
+
else
|
|
231
|
+
name
|
|
232
|
+
end
|
|
233
|
+
end.join(", ")
|
|
640
234
|
end
|
|
641
235
|
|
|
236
|
+
# ─── Save ───
|
|
237
|
+
|
|
642
238
|
def save_config
|
|
643
239
|
with_spinner("Generating SSH keys...") do
|
|
644
|
-
|
|
645
|
-
result = ConfigApi.init(
|
|
646
|
-
name: @data["application"]["name"],
|
|
647
|
-
environment: "production"
|
|
648
|
-
)
|
|
240
|
+
result = Configuration::Builder.init(name: @data[:name], environment: "production")
|
|
649
241
|
|
|
650
242
|
if result.failure?
|
|
651
243
|
raise Errors::ConfigError, "Failed to initialize: #{result.error_message}"
|
|
652
244
|
end
|
|
653
245
|
|
|
654
|
-
# Now we need to apply all our config on top
|
|
655
|
-
# Decrypt the init result, merge our data, re-encrypt
|
|
656
246
|
yaml = Utils::Crypto.decrypt(result.config, result.master_key)
|
|
657
247
|
init_data = YAML.safe_load(yaml, permitted_classes: [Symbol])
|
|
658
248
|
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
init_data["application"]["ssh_keys"] = YAML.safe_load(yaml)["application"]["ssh_keys"]
|
|
662
|
-
|
|
663
|
-
# Write files
|
|
664
|
-
config_path = File.join(".", Utils::DEFAULT_ENCRYPTED_FILE)
|
|
665
|
-
key_path = File.join(".", Utils::DEFAULT_KEY_FILE)
|
|
666
|
-
|
|
667
|
-
final_yaml = YAML.dump(init_data)
|
|
668
|
-
encrypted = Utils::Crypto.encrypt(final_yaml, result.master_key)
|
|
669
|
-
|
|
670
|
-
File.binwrite(config_path, encrypted)
|
|
671
|
-
File.write(key_path, "#{result.master_key}\n", perm: 0o600)
|
|
672
|
-
|
|
673
|
-
update_gitignore
|
|
249
|
+
final_data = build_final_config(init_data)
|
|
250
|
+
write_config_files(final_data, result.master_key)
|
|
674
251
|
end
|
|
675
252
|
|
|
676
|
-
puts
|
|
677
|
-
success
|
|
678
|
-
success
|
|
253
|
+
output.puts
|
|
254
|
+
success "Created #{Utils::DEFAULT_ENCRYPTED_FILE}"
|
|
255
|
+
success "Created #{Utils::DEFAULT_KEY_FILE}"
|
|
679
256
|
end
|
|
680
257
|
|
|
681
|
-
def
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
258
|
+
def build_final_config(init_data)
|
|
259
|
+
app_data = {
|
|
260
|
+
"name" => @data[:name],
|
|
261
|
+
"ssh_keys" => init_data["application"]["ssh_keys"],
|
|
262
|
+
"servers" => { "main" => { "master" => true, "count" => 1 } },
|
|
263
|
+
"app" => @data[:apps],
|
|
264
|
+
"env" => @data[:env],
|
|
265
|
+
"secrets" => @data[:secrets]
|
|
266
|
+
}
|
|
685
267
|
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
268
|
+
app_data["compute_provider"] = @data[:compute] if @data[:compute]
|
|
269
|
+
app_data["domain_provider"] = @data[:domain] if @data[:domain]
|
|
270
|
+
app_data["database"] = @data[:database] if @data[:database]
|
|
689
271
|
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
loop do
|
|
693
|
-
value = mask ? @prompt.mask(message) : @prompt.ask(message) { |q| q.required true }
|
|
694
|
-
|
|
695
|
-
begin
|
|
696
|
-
yield(value) if block_given?
|
|
697
|
-
return value
|
|
698
|
-
rescue Errors::ValidationError, Errors::AuthenticationError => e
|
|
699
|
-
retries += 1
|
|
700
|
-
if retries >= MAX_RETRIES
|
|
701
|
-
error("Failed after #{MAX_RETRIES} attempts: #{e.message}")
|
|
702
|
-
raise
|
|
703
|
-
end
|
|
704
|
-
warn("#{e.message}. Please try again. (#{retries}/#{MAX_RETRIES})")
|
|
705
|
-
end
|
|
272
|
+
if @data[:volumes]
|
|
273
|
+
app_data["servers"]["main"]["volumes"] = @data[:volumes]
|
|
706
274
|
end
|
|
707
|
-
end
|
|
708
275
|
|
|
709
|
-
|
|
710
|
-
pastel.bold("─── #{title} ───")
|
|
276
|
+
{ "application" => app_data }
|
|
711
277
|
end
|
|
712
278
|
|
|
713
|
-
def
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
return result
|
|
717
|
-
end
|
|
279
|
+
def write_config_files(data, master_key)
|
|
280
|
+
config_path = File.join(".", Utils::DEFAULT_ENCRYPTED_FILE)
|
|
281
|
+
key_path = File.join(".", Utils::DEFAULT_KEY_FILE)
|
|
718
282
|
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
begin
|
|
722
|
-
result = yield
|
|
723
|
-
spinner.success("done")
|
|
724
|
-
result
|
|
725
|
-
rescue StandardError => e
|
|
726
|
-
spinner.error("failed")
|
|
727
|
-
raise e
|
|
728
|
-
end
|
|
729
|
-
end
|
|
283
|
+
final_yaml = YAML.dump(data)
|
|
284
|
+
encrypted = Utils::Crypto.encrypt(final_yaml, master_key)
|
|
730
285
|
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
end
|
|
286
|
+
File.binwrite(config_path, encrypted)
|
|
287
|
+
File.write(key_path, "#{master_key}\n", perm: 0o600)
|
|
734
288
|
|
|
735
|
-
|
|
736
|
-
warn "#{pastel.red("✗")} #{msg}"
|
|
737
|
-
end
|
|
738
|
-
|
|
739
|
-
def pastel
|
|
740
|
-
@pastel ||= Pastel.new
|
|
289
|
+
update_gitignore
|
|
741
290
|
end
|
|
742
291
|
|
|
743
292
|
def update_gitignore
|
|
@@ -755,6 +304,11 @@ module Nvoi
|
|
|
755
304
|
additions.each { |e| f.puts e }
|
|
756
305
|
end
|
|
757
306
|
end
|
|
307
|
+
|
|
308
|
+
def show_next_steps
|
|
309
|
+
output.puts
|
|
310
|
+
output.puts "Next: #{pastel.cyan("nvoi deploy")}"
|
|
311
|
+
end
|
|
758
312
|
end
|
|
759
313
|
end
|
|
760
314
|
end
|