nvoi 0.1.8 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/Gemfile +1 -5
- data/Gemfile.lock +17 -8
- data/Rakefile +1 -1
- data/lib/nvoi/cli/config/command.rb +46 -41
- data/lib/nvoi/cli/credentials/edit/command.rb +20 -20
- data/lib/nvoi/cli/credentials/show/command.rb +1 -1
- data/lib/nvoi/cli/db/command.rb +10 -10
- data/lib/nvoi/cli/delete/command.rb +2 -2
- data/lib/nvoi/cli/deploy/command.rb +2 -2
- data/lib/nvoi/cli/deploy/steps/configure_tunnel.rb +2 -2
- data/lib/nvoi/cli/deploy/steps/provision_server.rb +1 -1
- data/lib/nvoi/cli/deploy/steps/provision_volume.rb +1 -1
- data/lib/nvoi/cli/exec/command.rb +3 -3
- data/lib/nvoi/cli/logs/command.rb +2 -2
- data/lib/nvoi/cli/onboard/command.rb +176 -622
- data/lib/nvoi/cli/onboard/steps/app.rb +108 -0
- data/lib/nvoi/cli/onboard/steps/app_name.rb +26 -0
- data/lib/nvoi/cli/onboard/steps/compute.rb +139 -0
- data/lib/nvoi/cli/onboard/steps/database.rb +97 -0
- data/lib/nvoi/cli/onboard/steps/domain.rb +48 -0
- data/lib/nvoi/cli/onboard/steps/env.rb +67 -0
- data/lib/nvoi/cli/onboard/ui.rb +84 -0
- data/lib/nvoi/cli/unlock/command.rb +2 -2
- data/lib/nvoi/cli.rb +0 -32
- data/lib/nvoi/configuration/app_service.rb +54 -0
- data/lib/nvoi/configuration/application.rb +44 -0
- data/lib/nvoi/configuration/builder.rb +417 -0
- data/lib/nvoi/configuration/database.rb +56 -0
- data/lib/nvoi/configuration/deploy.rb +15 -0
- data/lib/nvoi/{objects/service_spec.rb → configuration/deployment.rb} +4 -3
- data/lib/nvoi/{objects/config_override.rb → configuration/override.rb} +4 -4
- data/lib/nvoi/configuration/providers.rb +78 -0
- data/lib/nvoi/configuration/result.rb +43 -0
- data/lib/nvoi/configuration/root.rb +234 -0
- data/lib/nvoi/configuration/server.rb +39 -0
- data/lib/nvoi/configuration/service.rb +62 -0
- data/lib/nvoi/external/cloud/aws.rb +12 -12
- data/lib/nvoi/external/cloud/hetzner.rb +7 -7
- data/lib/nvoi/external/cloud/scaleway.rb +7 -7
- data/lib/nvoi/external/cloud/types.rb +42 -0
- data/lib/nvoi/external/database/mysql.rb +1 -1
- data/lib/nvoi/external/database/postgres.rb +1 -1
- data/lib/nvoi/external/database/provider.rb +1 -1
- data/lib/nvoi/external/database/sqlite.rb +1 -1
- data/lib/nvoi/external/database/types.rb +55 -0
- data/lib/nvoi/external/dns/cloudflare.rb +6 -6
- data/lib/nvoi/external/dns/types.rb +24 -0
- data/lib/nvoi/utils/config_loader.rb +12 -12
- data/lib/nvoi/utils/credential_store.rb +4 -4
- data/lib/nvoi/utils/env_resolver.rb +3 -3
- data/lib/nvoi/utils/namer.rb +2 -2
- data/lib/nvoi/utils/presence.rb +23 -0
- data/lib/nvoi/version.rb +1 -1
- data/lib/nvoi.rb +2 -17
- metadata +95 -58
- data/.claude/todo/refactor/00-overview.md +0 -171
- data/.claude/todo/refactor/01-objects.md +0 -96
- data/.claude/todo/refactor/02-utils.md +0 -143
- data/.claude/todo/refactor/03-external-cloud.md +0 -164
- data/.claude/todo/refactor/04-external-dns.md +0 -104
- data/.claude/todo/refactor/05-external.md +0 -133
- data/.claude/todo/refactor/06-cli.md +0 -123
- data/.claude/todo/refactor/07-cli-deploy-command.md +0 -177
- data/.claude/todo/refactor/08-cli-deploy-steps.md +0 -201
- data/.claude/todo/refactor/09-cli-delete-command.md +0 -169
- data/.claude/todo/refactor/10-cli-exec-command.md +0 -157
- data/.claude/todo/refactor/11-cli-credentials-command.md +0 -190
- data/.claude/todo/refactor/12-cli-db-command.md +0 -128
- data/.claude/todo/refactor/_target.md +0 -79
- data/.claude/todo/refactor-execution/00-entrypoint.md +0 -49
- data/.claude/todo/refactor-execution/01-objects.md +0 -42
- data/.claude/todo/refactor-execution/02-utils.md +0 -41
- data/.claude/todo/refactor-execution/03-external-cloud.md +0 -38
- data/.claude/todo/refactor-execution/04-external-dns.md +0 -35
- data/.claude/todo/refactor-execution/05-external-other.md +0 -46
- data/.claude/todo/refactor-execution/06-cli-deploy.md +0 -45
- data/.claude/todo/refactor-execution/07-cli-delete.md +0 -43
- data/.claude/todo/refactor-execution/08-cli-exec.md +0 -30
- data/.claude/todo/refactor-execution/09-cli-credentials.md +0 -34
- data/.claude/todo/refactor-execution/10-cli-db.md +0 -31
- data/.claude/todo/refactor-execution/11-cli-router.md +0 -44
- data/.claude/todo/refactor-execution/12-cleanup.md +0 -120
- data/.claude/todo/refactor-execution/_monitoring-strategy.md +0 -126
- data/.claude/todo/scaleway.impl.md +0 -644
- data/.claude/todo/scaleway.reference.md +0 -520
- data/.claude/todos/buckets.md +0 -41
- data/.claude/todos.md +0 -550
- data/ingest +0 -0
- data/lib/nvoi/config_api/actions/app.rb +0 -53
- data/lib/nvoi/config_api/actions/compute_provider.rb +0 -55
- data/lib/nvoi/config_api/actions/database.rb +0 -70
- data/lib/nvoi/config_api/actions/domain_provider.rb +0 -40
- data/lib/nvoi/config_api/actions/env.rb +0 -32
- data/lib/nvoi/config_api/actions/init.rb +0 -67
- data/lib/nvoi/config_api/actions/secret.rb +0 -32
- data/lib/nvoi/config_api/actions/server.rb +0 -66
- data/lib/nvoi/config_api/actions/service.rb +0 -52
- data/lib/nvoi/config_api/actions/volume.rb +0 -40
- data/lib/nvoi/config_api/base.rb +0 -38
- data/lib/nvoi/config_api/result.rb +0 -26
- data/lib/nvoi/config_api.rb +0 -93
- data/lib/nvoi/objects/configuration.rb +0 -483
- data/lib/nvoi/objects/database.rb +0 -56
- data/lib/nvoi/objects/dns.rb +0 -14
- data/lib/nvoi/objects/firewall.rb +0 -11
- data/lib/nvoi/objects/network.rb +0 -11
- data/lib/nvoi/objects/server.rb +0 -14
- data/lib/nvoi/objects/tunnel.rb +0 -14
- data/lib/nvoi/objects/volume.rb +0 -17
|
@@ -0,0 +1,417 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
module Nvoi
|
|
5
|
+
module Configuration
|
|
6
|
+
# Builder for constructing and modifying config data hashes
|
|
7
|
+
# Replaces ConfigApi with a cleaner, chainable interface
|
|
8
|
+
class Builder
|
|
9
|
+
COMPUTE_PROVIDERS = %w[hetzner aws scaleway].freeze
|
|
10
|
+
DOMAIN_PROVIDERS = %w[cloudflare].freeze
|
|
11
|
+
DATABASE_ADAPTERS = %w[postgres postgresql mysql sqlite sqlite3].freeze
|
|
12
|
+
|
|
13
|
+
attr_reader :data
|
|
14
|
+
|
|
15
|
+
def initialize(data = nil)
|
|
16
|
+
@data = data || { "application" => {} }
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# ─── Class Methods ───
|
|
20
|
+
|
|
21
|
+
def self.from_hash(data)
|
|
22
|
+
new(data)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def self.init(name:, environment: "production")
|
|
26
|
+
raise ArgumentError, "name is required" if name.blank?
|
|
27
|
+
|
|
28
|
+
master_key = Utils::Crypto.generate_key
|
|
29
|
+
private_key, public_key = Utils::ConfigLoader.generate_keypair
|
|
30
|
+
|
|
31
|
+
builder = new
|
|
32
|
+
builder.name(name)
|
|
33
|
+
builder.environment(environment)
|
|
34
|
+
builder.ssh_keys(private_key, public_key)
|
|
35
|
+
|
|
36
|
+
yaml = YAML.dump(builder.to_h)
|
|
37
|
+
encrypted_config = Utils::Crypto.encrypt(yaml, master_key)
|
|
38
|
+
|
|
39
|
+
Result::Init.new(
|
|
40
|
+
config: encrypted_config,
|
|
41
|
+
master_key:,
|
|
42
|
+
ssh_public_key: public_key
|
|
43
|
+
)
|
|
44
|
+
rescue ArgumentError => e
|
|
45
|
+
Result::Init.new(error_type: :invalid_args, error_message: e.message)
|
|
46
|
+
rescue Errors::ConfigError => e
|
|
47
|
+
Result::Init.new(error_type: :config_error, error_message: e.message)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# ─── Basic Setters ───
|
|
51
|
+
|
|
52
|
+
def name(n)
|
|
53
|
+
app["name"] = n.to_s
|
|
54
|
+
self
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def environment(e)
|
|
58
|
+
app["environment"] = e.to_s
|
|
59
|
+
self
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def ssh_keys(private_key, public_key)
|
|
63
|
+
app["ssh_keys"] = {
|
|
64
|
+
"private_key" => private_key,
|
|
65
|
+
"public_key" => public_key
|
|
66
|
+
}
|
|
67
|
+
self
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# ─── Compute Provider ───
|
|
71
|
+
|
|
72
|
+
def compute_provider(provider, **opts)
|
|
73
|
+
validate_presence!(provider, "provider")
|
|
74
|
+
validate_inclusion!(provider.to_s, COMPUTE_PROVIDERS, "provider")
|
|
75
|
+
|
|
76
|
+
app["compute_provider"] = { provider.to_s => build_compute_config(provider.to_s, opts) }
|
|
77
|
+
wrap_success
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def remove_compute_provider
|
|
81
|
+
app["compute_provider"] = {}
|
|
82
|
+
wrap_success
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# ─── Domain Provider ───
|
|
86
|
+
|
|
87
|
+
def domain_provider(provider, **opts)
|
|
88
|
+
validate_presence!(provider, "provider")
|
|
89
|
+
validate_inclusion!(provider.to_s, DOMAIN_PROVIDERS, "provider")
|
|
90
|
+
|
|
91
|
+
app["domain_provider"] = { provider.to_s => build_domain_config(provider.to_s, opts) }
|
|
92
|
+
wrap_success
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def remove_domain_provider
|
|
96
|
+
app["domain_provider"] = {}
|
|
97
|
+
wrap_success
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# ─── Server ───
|
|
101
|
+
|
|
102
|
+
def server(name, master: false, type: nil, location: nil, count: 1)
|
|
103
|
+
validate_presence!(name, "name")
|
|
104
|
+
validate_positive!(count, "count") if count
|
|
105
|
+
|
|
106
|
+
servers[name.to_s] = {
|
|
107
|
+
"master" => master,
|
|
108
|
+
"type" => type,
|
|
109
|
+
"location" => location,
|
|
110
|
+
"count" => count
|
|
111
|
+
}.compact
|
|
112
|
+
wrap_success
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def remove_server(name)
|
|
116
|
+
validate_presence!(name, "name")
|
|
117
|
+
validate_exists!(servers, name.to_s, "server")
|
|
118
|
+
check_server_references(name.to_s)
|
|
119
|
+
|
|
120
|
+
servers.delete(name.to_s)
|
|
121
|
+
wrap_success
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# ─── Volume ───
|
|
125
|
+
|
|
126
|
+
def volume(server_name, name, size: 10)
|
|
127
|
+
validate_presence!(server_name, "server")
|
|
128
|
+
validate_presence!(name, "name")
|
|
129
|
+
validate_positive!(size, "size") if size
|
|
130
|
+
validate_exists!(servers, server_name.to_s, "server")
|
|
131
|
+
|
|
132
|
+
servers[server_name.to_s]["volumes"] ||= {}
|
|
133
|
+
servers[server_name.to_s]["volumes"][name.to_s] = { "size" => size }
|
|
134
|
+
wrap_success
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def remove_volume(server_name, name)
|
|
138
|
+
validate_presence!(server_name, "server")
|
|
139
|
+
validate_presence!(name, "name")
|
|
140
|
+
validate_exists!(servers, server_name.to_s, "server")
|
|
141
|
+
|
|
142
|
+
volumes = servers[server_name.to_s]["volumes"] || {}
|
|
143
|
+
validate_exists!(volumes, name.to_s, "volume")
|
|
144
|
+
|
|
145
|
+
volumes.delete(name.to_s)
|
|
146
|
+
wrap_success
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# ─── App ───
|
|
150
|
+
|
|
151
|
+
def app_entry(name, servers:, domain: nil, subdomain: nil, port: nil, command: nil, pre_run_command: nil, env: nil, mounts: nil)
|
|
152
|
+
validate_presence!(name, "name")
|
|
153
|
+
validate_servers_array!(servers)
|
|
154
|
+
validate_server_refs!(servers)
|
|
155
|
+
|
|
156
|
+
apps[name.to_s] = {
|
|
157
|
+
"servers" => servers.map(&:to_s),
|
|
158
|
+
"domain" => domain,
|
|
159
|
+
"subdomain" => subdomain,
|
|
160
|
+
"port" => port,
|
|
161
|
+
"command" => command,
|
|
162
|
+
"pre_run_command" => pre_run_command,
|
|
163
|
+
"env" => env,
|
|
164
|
+
"mounts" => mounts
|
|
165
|
+
}.compact
|
|
166
|
+
wrap_success
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def remove_app(name)
|
|
170
|
+
validate_presence!(name, "name")
|
|
171
|
+
validate_exists!(apps, name.to_s, "app")
|
|
172
|
+
|
|
173
|
+
apps.delete(name.to_s)
|
|
174
|
+
wrap_success
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# ─── Database ───
|
|
178
|
+
|
|
179
|
+
def database(servers:, adapter:, image: nil, url: nil, user: nil, password: nil, database_name: nil, mount: nil, path: nil)
|
|
180
|
+
validate_servers_array!(servers)
|
|
181
|
+
validate_presence!(adapter, "adapter")
|
|
182
|
+
validate_inclusion!(adapter.to_s.downcase, DATABASE_ADAPTERS, "adapter")
|
|
183
|
+
validate_server_refs!(servers)
|
|
184
|
+
|
|
185
|
+
secrets = build_database_secrets(adapter, user, password, database_name)
|
|
186
|
+
|
|
187
|
+
app["database"] = {
|
|
188
|
+
"servers" => servers.map(&:to_s),
|
|
189
|
+
"adapter" => adapter.to_s,
|
|
190
|
+
"image" => image,
|
|
191
|
+
"url" => url,
|
|
192
|
+
"secrets" => secrets.empty? ? nil : secrets,
|
|
193
|
+
"mount" => mount,
|
|
194
|
+
"path" => path
|
|
195
|
+
}.compact
|
|
196
|
+
wrap_success
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def remove_database
|
|
200
|
+
app.delete("database")
|
|
201
|
+
wrap_success
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# ─── Service ───
|
|
205
|
+
|
|
206
|
+
def service(name, servers:, image:, port: nil, command: nil, env: nil, mount: nil)
|
|
207
|
+
validate_presence!(name, "name")
|
|
208
|
+
validate_servers_array!(servers)
|
|
209
|
+
validate_presence!(image, "image")
|
|
210
|
+
validate_server_refs!(servers)
|
|
211
|
+
|
|
212
|
+
services[name.to_s] = {
|
|
213
|
+
"servers" => servers.map(&:to_s),
|
|
214
|
+
"image" => image.to_s,
|
|
215
|
+
"port" => port,
|
|
216
|
+
"command" => command,
|
|
217
|
+
"env" => env,
|
|
218
|
+
"mount" => mount
|
|
219
|
+
}.compact
|
|
220
|
+
wrap_success
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def remove_service(name)
|
|
224
|
+
validate_presence!(name, "name")
|
|
225
|
+
validate_exists!(services, name.to_s, "service")
|
|
226
|
+
|
|
227
|
+
services.delete(name.to_s)
|
|
228
|
+
wrap_success
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
# ─── Secret ───
|
|
232
|
+
|
|
233
|
+
def secret(key, value)
|
|
234
|
+
validate_presence!(key, "key")
|
|
235
|
+
raise ArgumentError, "value is required" if value.nil?
|
|
236
|
+
|
|
237
|
+
secrets[key.to_s] = value.to_s
|
|
238
|
+
wrap_success
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
def remove_secret(key)
|
|
242
|
+
validate_presence!(key, "key")
|
|
243
|
+
validate_exists!(secrets, key.to_s, "secret")
|
|
244
|
+
|
|
245
|
+
secrets.delete(key.to_s)
|
|
246
|
+
wrap_success
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
# ─── Env ───
|
|
250
|
+
|
|
251
|
+
def env(key, value)
|
|
252
|
+
validate_presence!(key, "key")
|
|
253
|
+
raise ArgumentError, "value is required" if value.nil?
|
|
254
|
+
|
|
255
|
+
env_vars[key.to_s] = value.to_s
|
|
256
|
+
wrap_success
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
def remove_env(key)
|
|
260
|
+
validate_presence!(key, "key")
|
|
261
|
+
validate_exists!(env_vars, key.to_s, "env")
|
|
262
|
+
|
|
263
|
+
env_vars.delete(key.to_s)
|
|
264
|
+
wrap_success
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
# ─── Output ───
|
|
268
|
+
|
|
269
|
+
def to_h
|
|
270
|
+
@data
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
def to_yaml
|
|
274
|
+
YAML.dump(@data)
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
private
|
|
278
|
+
|
|
279
|
+
def app
|
|
280
|
+
@data["application"] ||= {}
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
def servers
|
|
284
|
+
app["servers"] ||= {}
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
def apps
|
|
288
|
+
app["app"] ||= {}
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
def services
|
|
292
|
+
app["services"] ||= {}
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
def secrets
|
|
296
|
+
app["secrets"] ||= {}
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
def env_vars
|
|
300
|
+
app["env"] ||= {}
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
# ─── Validation Helpers ───
|
|
304
|
+
|
|
305
|
+
def validate_presence!(value, field)
|
|
306
|
+
raise ArgumentError, "#{field} is required" if value.blank?
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
def validate_inclusion!(value, list, field)
|
|
310
|
+
raise ArgumentError, "#{field} must be one of: #{list.join(', ')}" unless list.include?(value)
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
def validate_positive!(value, field)
|
|
314
|
+
raise ArgumentError, "#{field} must be positive" if value && value < 1
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
def validate_exists!(hash, key, type)
|
|
318
|
+
raise Errors::ConfigValidationError, "#{type} '#{key}' not found" unless hash.key?(key)
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
def validate_servers_array!(server_refs)
|
|
322
|
+
raise ArgumentError, "servers is required" if server_refs.to_a.empty?
|
|
323
|
+
raise ArgumentError, "servers must be an array" unless server_refs.is_a?(Array)
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
def validate_server_refs!(server_refs)
|
|
327
|
+
defined = servers.keys
|
|
328
|
+
server_refs.each do |ref|
|
|
329
|
+
raise Errors::ConfigValidationError, "server '#{ref}' not found" unless defined.include?(ref.to_s)
|
|
330
|
+
end
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
def check_server_references(server_name)
|
|
334
|
+
apps.each do |app_name, cfg|
|
|
335
|
+
if (cfg["servers"] || []).include?(server_name)
|
|
336
|
+
raise Errors::ConfigValidationError, "app.#{app_name} references server '#{server_name}'"
|
|
337
|
+
end
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
db = app["database"]
|
|
341
|
+
if db && (db["servers"] || []).include?(server_name)
|
|
342
|
+
raise Errors::ConfigValidationError, "database references server '#{server_name}'"
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
services.each do |svc_name, cfg|
|
|
346
|
+
if (cfg["servers"] || []).include?(server_name)
|
|
347
|
+
raise Errors::ConfigValidationError, "services.#{svc_name} references server '#{server_name}'"
|
|
348
|
+
end
|
|
349
|
+
end
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
# ─── Config Builders ───
|
|
353
|
+
|
|
354
|
+
def build_compute_config(provider, opts)
|
|
355
|
+
case provider
|
|
356
|
+
when "hetzner"
|
|
357
|
+
{
|
|
358
|
+
"api_token" => opts[:api_token],
|
|
359
|
+
"server_type" => opts[:server_type],
|
|
360
|
+
"server_location" => opts[:server_location]
|
|
361
|
+
}.compact
|
|
362
|
+
when "aws"
|
|
363
|
+
{
|
|
364
|
+
"access_key_id" => opts[:access_key_id],
|
|
365
|
+
"secret_access_key" => opts[:secret_access_key],
|
|
366
|
+
"region" => opts[:region],
|
|
367
|
+
"instance_type" => opts[:instance_type]
|
|
368
|
+
}.compact
|
|
369
|
+
when "scaleway"
|
|
370
|
+
{
|
|
371
|
+
"secret_key" => opts[:secret_key],
|
|
372
|
+
"project_id" => opts[:project_id],
|
|
373
|
+
"zone" => opts[:zone],
|
|
374
|
+
"server_type" => opts[:server_type]
|
|
375
|
+
}.compact
|
|
376
|
+
end
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
def build_domain_config(provider, opts)
|
|
380
|
+
case provider
|
|
381
|
+
when "cloudflare"
|
|
382
|
+
{
|
|
383
|
+
"api_token" => opts[:api_token],
|
|
384
|
+
"account_id" => opts[:account_id]
|
|
385
|
+
}.compact
|
|
386
|
+
end
|
|
387
|
+
end
|
|
388
|
+
|
|
389
|
+
def build_database_secrets(adapter, user, password, database_name)
|
|
390
|
+
case adapter.to_s.downcase
|
|
391
|
+
when "postgres", "postgresql"
|
|
392
|
+
{
|
|
393
|
+
"POSTGRES_USER" => user,
|
|
394
|
+
"POSTGRES_PASSWORD" => password,
|
|
395
|
+
"POSTGRES_DB" => database_name
|
|
396
|
+
}.compact
|
|
397
|
+
when "mysql"
|
|
398
|
+
{
|
|
399
|
+
"MYSQL_USER" => user,
|
|
400
|
+
"MYSQL_PASSWORD" => password,
|
|
401
|
+
"MYSQL_DATABASE" => database_name
|
|
402
|
+
}.compact
|
|
403
|
+
else
|
|
404
|
+
{}
|
|
405
|
+
end
|
|
406
|
+
end
|
|
407
|
+
|
|
408
|
+
def wrap_success
|
|
409
|
+
Result.success(@data)
|
|
410
|
+
end
|
|
411
|
+
|
|
412
|
+
def wrap_failure(type, message)
|
|
413
|
+
Result.failure(type, message)
|
|
414
|
+
end
|
|
415
|
+
end
|
|
416
|
+
end
|
|
417
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Nvoi
|
|
4
|
+
module Configuration
|
|
5
|
+
# Database defines database configuration
|
|
6
|
+
class Database
|
|
7
|
+
attr_accessor :servers, :adapter, :url, :image, :mount, :secrets, :path
|
|
8
|
+
|
|
9
|
+
def initialize(data = nil)
|
|
10
|
+
data ||= {}
|
|
11
|
+
@servers = data["servers"] || []
|
|
12
|
+
@adapter = data["adapter"]
|
|
13
|
+
@url = data["url"]
|
|
14
|
+
@image = data["image"]
|
|
15
|
+
@mount = data["mount"] || {}
|
|
16
|
+
@secrets = data["secrets"] || {}
|
|
17
|
+
@path = data["path"]
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def postgres?
|
|
21
|
+
@adapter&.downcase&.start_with?("postgres")
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def mysql?
|
|
25
|
+
@adapter&.downcase == "mysql"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def sqlite?
|
|
29
|
+
@adapter&.downcase&.start_with?("sqlite")
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def to_service_spec(namer)
|
|
33
|
+
return nil if @adapter&.downcase&.start_with?("sqlite")
|
|
34
|
+
|
|
35
|
+
port = case @adapter&.downcase
|
|
36
|
+
when "mysql" then 3306
|
|
37
|
+
else 5432
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
image = @image || Utils::Constants::DATABASE_IMAGES[@adapter&.downcase]
|
|
41
|
+
|
|
42
|
+
Configuration::Deployment.new(
|
|
43
|
+
name: namer.database_service_name,
|
|
44
|
+
image:,
|
|
45
|
+
port:,
|
|
46
|
+
env: nil,
|
|
47
|
+
mounts: @mount,
|
|
48
|
+
replicas: 1,
|
|
49
|
+
stateful_set: true,
|
|
50
|
+
secrets: @secrets,
|
|
51
|
+
servers: @servers
|
|
52
|
+
)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Nvoi
|
|
4
|
+
module Configuration
|
|
5
|
+
# Deploy represents the root deployment configuration
|
|
6
|
+
class Deploy
|
|
7
|
+
attr_accessor :application
|
|
8
|
+
|
|
9
|
+
def initialize(data = nil)
|
|
10
|
+
data ||= {}
|
|
11
|
+
@application = Application.new(data["application"])
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Nvoi
|
|
4
|
-
module
|
|
5
|
-
#
|
|
6
|
-
|
|
4
|
+
module Configuration
|
|
5
|
+
# Deployment is the normalized service definition ready for deployment
|
|
6
|
+
# Created by Configuration::Database and Configuration::Service
|
|
7
|
+
class Deployment
|
|
7
8
|
attr_accessor :name, :image, :port, :command, :env, :mounts, :replicas,
|
|
8
9
|
:healthcheck, :stateful_set, :secrets, :servers
|
|
9
10
|
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Nvoi
|
|
4
|
-
module
|
|
5
|
-
#
|
|
6
|
-
class
|
|
4
|
+
module Configuration
|
|
5
|
+
# Override allows CLI to override app name and subdomain for branch deployments
|
|
6
|
+
class Override
|
|
7
7
|
BRANCH_PATTERN = /\A[a-z0-9-]+\z/
|
|
8
8
|
|
|
9
9
|
attr_reader :branch
|
|
@@ -32,7 +32,7 @@ module Nvoi
|
|
|
32
32
|
private
|
|
33
33
|
|
|
34
34
|
def validate!(branch)
|
|
35
|
-
raise ArgumentError, "--branch value required"
|
|
35
|
+
raise ArgumentError, "--branch value required" if branch.blank?
|
|
36
36
|
raise ArgumentError, "invalid branch format (lowercase alphanumeric and hyphens only)" unless branch.match?(BRANCH_PATTERN)
|
|
37
37
|
end
|
|
38
38
|
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Nvoi
|
|
4
|
+
module Configuration
|
|
5
|
+
module Providers
|
|
6
|
+
# DomainProvider contains domain provider configuration
|
|
7
|
+
class DomainProvider
|
|
8
|
+
attr_accessor :cloudflare
|
|
9
|
+
|
|
10
|
+
def initialize(data = nil)
|
|
11
|
+
data ||= {}
|
|
12
|
+
@cloudflare = data["cloudflare"] ? Cloudflare.new(data["cloudflare"]) : nil
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# ComputeProvider contains compute provider configuration
|
|
17
|
+
class ComputeProvider
|
|
18
|
+
attr_accessor :hetzner, :aws, :scaleway
|
|
19
|
+
|
|
20
|
+
def initialize(data = nil)
|
|
21
|
+
data ||= {}
|
|
22
|
+
@hetzner = data["hetzner"] ? Hetzner.new(data["hetzner"]) : nil
|
|
23
|
+
@aws = data["aws"] ? AwsCfg.new(data["aws"]) : nil
|
|
24
|
+
@scaleway = data["scaleway"] ? Scaleway.new(data["scaleway"]) : nil
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Cloudflare contains Cloudflare-specific configuration
|
|
29
|
+
class Cloudflare
|
|
30
|
+
attr_accessor :api_token, :account_id
|
|
31
|
+
|
|
32
|
+
def initialize(data = nil)
|
|
33
|
+
data ||= {}
|
|
34
|
+
@api_token = data["api_token"]
|
|
35
|
+
@account_id = data["account_id"]
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Hetzner contains Hetzner-specific configuration
|
|
40
|
+
class Hetzner
|
|
41
|
+
attr_accessor :api_token, :server_type, :server_location
|
|
42
|
+
|
|
43
|
+
def initialize(data = nil)
|
|
44
|
+
data ||= {}
|
|
45
|
+
@api_token = data["api_token"]
|
|
46
|
+
@server_type = data["server_type"]
|
|
47
|
+
@server_location = data["server_location"]
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# AwsCfg contains AWS-specific configuration
|
|
52
|
+
class AwsCfg
|
|
53
|
+
attr_accessor :access_key_id, :secret_access_key, :region, :instance_type
|
|
54
|
+
|
|
55
|
+
def initialize(data = nil)
|
|
56
|
+
data ||= {}
|
|
57
|
+
@access_key_id = data["access_key_id"]
|
|
58
|
+
@secret_access_key = data["secret_access_key"]
|
|
59
|
+
@region = data["region"]
|
|
60
|
+
@instance_type = data["instance_type"]
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Scaleway contains Scaleway-specific configuration
|
|
65
|
+
class Scaleway
|
|
66
|
+
attr_accessor :secret_key, :project_id, :zone, :server_type
|
|
67
|
+
|
|
68
|
+
def initialize(data = nil)
|
|
69
|
+
data ||= {}
|
|
70
|
+
@secret_key = data["secret_key"]
|
|
71
|
+
@project_id = data["project_id"]
|
|
72
|
+
@zone = data["zone"] || "fr-par-1"
|
|
73
|
+
@server_type = data["server_type"]
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Nvoi
|
|
4
|
+
module Configuration
|
|
5
|
+
# Result wrapper for Builder operations
|
|
6
|
+
class Result
|
|
7
|
+
attr_reader :data, :error_type, :error_message
|
|
8
|
+
|
|
9
|
+
def initialize(data: nil, error_type: nil, error_message: nil)
|
|
10
|
+
@data = data
|
|
11
|
+
@error_type = error_type
|
|
12
|
+
@error_message = error_message
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def success? = @error_type.nil?
|
|
16
|
+
def failure? = !success?
|
|
17
|
+
|
|
18
|
+
def self.success(data)
|
|
19
|
+
new(data:)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def self.failure(type, message)
|
|
23
|
+
new(error_type: type, error_message: message)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Result for init operations (includes encryption artifacts)
|
|
27
|
+
class Init
|
|
28
|
+
attr_reader :config, :master_key, :ssh_public_key, :error_type, :error_message
|
|
29
|
+
|
|
30
|
+
def initialize(config: nil, master_key: nil, ssh_public_key: nil, error_type: nil, error_message: nil)
|
|
31
|
+
@config = config
|
|
32
|
+
@master_key = master_key
|
|
33
|
+
@ssh_public_key = ssh_public_key
|
|
34
|
+
@error_type = error_type
|
|
35
|
+
@error_message = error_message
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def success? = @error_type.nil?
|
|
39
|
+
def failure? = !success?
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|