nvoi 0.1.6 → 0.1.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.claude/todo/refactor/12-cli-db-command.md +128 -0
- data/.claude/todo/refactor-execution/00-entrypoint.md +49 -0
- data/.claude/todo/refactor-execution/01-objects.md +42 -0
- data/.claude/todo/refactor-execution/02-utils.md +41 -0
- data/.claude/todo/refactor-execution/03-external-cloud.md +38 -0
- data/.claude/todo/refactor-execution/04-external-dns.md +35 -0
- data/.claude/todo/refactor-execution/05-external-other.md +46 -0
- data/.claude/todo/refactor-execution/06-cli-deploy.md +45 -0
- data/.claude/todo/refactor-execution/07-cli-delete.md +43 -0
- data/.claude/todo/refactor-execution/08-cli-exec.md +30 -0
- data/.claude/todo/refactor-execution/09-cli-credentials.md +34 -0
- data/.claude/todo/refactor-execution/10-cli-db.md +31 -0
- data/.claude/todo/refactor-execution/11-cli-router.md +44 -0
- data/.claude/todo/refactor-execution/12-cleanup.md +120 -0
- data/.claude/todo/refactor-execution/_monitoring-strategy.md +126 -0
- data/.claude/todos/buckets.md +41 -0
- data/.claude/todos.md +550 -0
- data/Gemfile +5 -0
- data/Gemfile.lock +35 -4
- data/Rakefile +1 -1
- data/ingest +0 -0
- data/lib/nvoi/cli/config/command.rb +219 -0
- data/lib/nvoi/cli/delete/steps/teardown_dns.rb +12 -11
- data/lib/nvoi/cli/deploy/command.rb +27 -11
- data/lib/nvoi/cli/deploy/steps/build_image.rb +48 -6
- data/lib/nvoi/cli/deploy/steps/configure_tunnel.rb +15 -13
- data/lib/nvoi/cli/deploy/steps/deploy_service.rb +8 -15
- data/lib/nvoi/cli/deploy/steps/setup_k3s.rb +10 -1
- data/lib/nvoi/cli/logs/command.rb +66 -0
- data/lib/nvoi/cli/onboard/command.rb +761 -0
- data/lib/nvoi/cli/unlock/command.rb +72 -0
- data/lib/nvoi/cli.rb +257 -0
- data/lib/nvoi/config_api/actions/app.rb +30 -30
- data/lib/nvoi/config_api/actions/compute_provider.rb +31 -31
- data/lib/nvoi/config_api/actions/database.rb +42 -42
- data/lib/nvoi/config_api/actions/domain_provider.rb +40 -0
- data/lib/nvoi/config_api/actions/env.rb +12 -12
- data/lib/nvoi/config_api/actions/init.rb +67 -0
- data/lib/nvoi/config_api/actions/secret.rb +12 -12
- data/lib/nvoi/config_api/actions/server.rb +35 -35
- data/lib/nvoi/config_api/actions/service.rb +52 -0
- data/lib/nvoi/config_api/actions/volume.rb +18 -18
- data/lib/nvoi/config_api/base.rb +15 -21
- data/lib/nvoi/config_api/result.rb +5 -5
- data/lib/nvoi/config_api.rb +51 -28
- data/lib/nvoi/external/cloud/aws.rb +26 -1
- data/lib/nvoi/external/cloud/hetzner.rb +27 -1
- data/lib/nvoi/external/cloud/scaleway.rb +32 -6
- data/lib/nvoi/external/containerd.rb +1 -44
- data/lib/nvoi/external/dns/cloudflare.rb +34 -16
- data/lib/nvoi/external/ssh.rb +0 -12
- data/lib/nvoi/external/ssh_tunnel.rb +100 -0
- data/lib/nvoi/objects/configuration.rb +20 -0
- data/lib/nvoi/utils/namer.rb +9 -0
- data/lib/nvoi/utils/retry.rb +1 -1
- data/lib/nvoi/version.rb +1 -1
- data/lib/nvoi.rb +16 -0
- data/templates/app-ingress.yaml.erb +3 -1
- metadata +27 -1
|
@@ -6,26 +6,26 @@ module Nvoi
|
|
|
6
6
|
class SetEnv < Base
|
|
7
7
|
protected
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
def mutate(data, key:, value:)
|
|
10
|
+
raise ArgumentError, "key is required" if key.nil? || key.to_s.empty?
|
|
11
|
+
raise ArgumentError, "value is required" if value.nil?
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
app(data)["env"] ||= {}
|
|
14
|
+
app(data)["env"][key.to_s] = value.to_s
|
|
15
|
+
end
|
|
16
16
|
end
|
|
17
17
|
|
|
18
18
|
class DeleteEnv < Base
|
|
19
19
|
protected
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
def mutate(data, key:)
|
|
22
|
+
raise ArgumentError, "key is required" if key.nil? || key.to_s.empty?
|
|
23
23
|
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
env = app(data)["env"] || {}
|
|
25
|
+
raise Errors::ConfigValidationError, "env '#{key}' not found" unless env.key?(key.to_s)
|
|
26
26
|
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
env.delete(key.to_s)
|
|
28
|
+
end
|
|
29
29
|
end
|
|
30
30
|
end
|
|
31
31
|
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Nvoi
|
|
4
|
+
module ConfigApi
|
|
5
|
+
module Actions
|
|
6
|
+
class Init
|
|
7
|
+
def call(name:, environment: "production")
|
|
8
|
+
raise ArgumentError, "name is required" if name.nil? || name.to_s.empty?
|
|
9
|
+
|
|
10
|
+
# Generate master key
|
|
11
|
+
master_key = Utils::Crypto.generate_key
|
|
12
|
+
|
|
13
|
+
# Generate SSH keypair (reuse existing utility)
|
|
14
|
+
private_key, public_key = Utils::ConfigLoader.generate_keypair
|
|
15
|
+
|
|
16
|
+
# Build initial config
|
|
17
|
+
config_data = {
|
|
18
|
+
"application" => {
|
|
19
|
+
"name" => name.to_s,
|
|
20
|
+
"environment" => environment.to_s,
|
|
21
|
+
"domain_provider" => {},
|
|
22
|
+
"compute_provider" => {},
|
|
23
|
+
"servers" => {},
|
|
24
|
+
"app" => {},
|
|
25
|
+
"services" => {},
|
|
26
|
+
"env" => {},
|
|
27
|
+
"secrets" => {},
|
|
28
|
+
"ssh_keys" => {
|
|
29
|
+
"private_key" => private_key,
|
|
30
|
+
"public_key" => public_key
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
# Encrypt config
|
|
36
|
+
yaml = YAML.dump(config_data)
|
|
37
|
+
encrypted_config = Utils::Crypto.encrypt(yaml, master_key)
|
|
38
|
+
|
|
39
|
+
InitResult.new(
|
|
40
|
+
config: encrypted_config,
|
|
41
|
+
master_key:,
|
|
42
|
+
ssh_public_key: public_key
|
|
43
|
+
)
|
|
44
|
+
rescue ArgumentError => e
|
|
45
|
+
InitResult.new(error_type: :invalid_args, error_message: e.message)
|
|
46
|
+
rescue Errors::ConfigError => e
|
|
47
|
+
InitResult.new(error_type: :config_error, error_message: e.message)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
class InitResult
|
|
52
|
+
attr_reader :config, :master_key, :ssh_public_key, :error_type, :error_message
|
|
53
|
+
|
|
54
|
+
def initialize(config: nil, master_key: nil, ssh_public_key: nil, error_type: nil, error_message: nil)
|
|
55
|
+
@config = config
|
|
56
|
+
@master_key = master_key
|
|
57
|
+
@ssh_public_key = ssh_public_key
|
|
58
|
+
@error_type = error_type
|
|
59
|
+
@error_message = error_message
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def success? = @error_type.nil?
|
|
63
|
+
def failure? = !success?
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -6,26 +6,26 @@ module Nvoi
|
|
|
6
6
|
class SetSecret < Base
|
|
7
7
|
protected
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
def mutate(data, key:, value:)
|
|
10
|
+
raise ArgumentError, "key is required" if key.nil? || key.to_s.empty?
|
|
11
|
+
raise ArgumentError, "value is required" if value.nil?
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
app(data)["secrets"] ||= {}
|
|
14
|
+
app(data)["secrets"][key.to_s] = value.to_s
|
|
15
|
+
end
|
|
16
16
|
end
|
|
17
17
|
|
|
18
18
|
class DeleteSecret < Base
|
|
19
19
|
protected
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
def mutate(data, key:)
|
|
22
|
+
raise ArgumentError, "key is required" if key.nil? || key.to_s.empty?
|
|
23
23
|
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
secrets = app(data)["secrets"] || {}
|
|
25
|
+
raise Errors::ConfigValidationError, "secret '#{key}' not found" unless secrets.key?(key.to_s)
|
|
26
26
|
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
secrets.delete(key.to_s)
|
|
28
|
+
end
|
|
29
29
|
end
|
|
30
30
|
end
|
|
31
31
|
end
|
|
@@ -6,60 +6,60 @@ module Nvoi
|
|
|
6
6
|
class SetServer < Base
|
|
7
7
|
protected
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
def mutate(data, name:, master: false, type: nil, location: nil, count: 1)
|
|
10
|
+
raise ArgumentError, "name is required" if name.nil? || name.to_s.empty?
|
|
11
|
+
raise ArgumentError, "count must be positive" if count && count < 1
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
13
|
+
app(data)["servers"] ||= {}
|
|
14
|
+
app(data)["servers"][name.to_s] = {
|
|
15
|
+
"master" => master,
|
|
16
|
+
"type" => type,
|
|
17
|
+
"location" => location,
|
|
18
|
+
"count" => count
|
|
19
|
+
}.compact
|
|
20
|
+
end
|
|
21
21
|
end
|
|
22
22
|
|
|
23
23
|
class DeleteServer < Base
|
|
24
24
|
protected
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
def mutate(data, name:)
|
|
27
|
+
raise ArgumentError, "name is required" if name.nil? || name.to_s.empty?
|
|
28
28
|
|
|
29
|
-
|
|
30
|
-
|
|
29
|
+
servers = app(data)["servers"] || {}
|
|
30
|
+
raise Errors::ConfigValidationError, "server '#{name}' not found" unless servers.key?(name.to_s)
|
|
31
31
|
|
|
32
|
-
|
|
33
|
-
|
|
32
|
+
servers.delete(name.to_s)
|
|
33
|
+
end
|
|
34
34
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
35
|
+
def validate(data)
|
|
36
|
+
check_orphaned_references(data)
|
|
37
|
+
end
|
|
38
38
|
|
|
39
39
|
private
|
|
40
40
|
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
def check_orphaned_references(data)
|
|
42
|
+
servers = (app(data)["servers"] || {}).keys
|
|
43
43
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
44
|
+
(app(data)["app"] || {}).each do |svc_name, svc|
|
|
45
|
+
(svc["servers"] || []).each do |ref|
|
|
46
|
+
raise Errors::ConfigValidationError, "app.#{svc_name} references non-existent server '#{ref}'" unless servers.include?(ref)
|
|
47
|
+
end
|
|
47
48
|
end
|
|
48
|
-
end
|
|
49
49
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
50
|
+
db = app(data)["database"]
|
|
51
|
+
if db
|
|
52
|
+
(db["servers"] || []).each do |ref|
|
|
53
|
+
raise Errors::ConfigValidationError, "database references non-existent server '#{ref}'" unless servers.include?(ref)
|
|
54
|
+
end
|
|
54
55
|
end
|
|
55
|
-
end
|
|
56
56
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
57
|
+
(app(data)["services"] || {}).each do |svc_name, svc|
|
|
58
|
+
(svc["servers"] || []).each do |ref|
|
|
59
|
+
raise Errors::ConfigValidationError, "services.#{svc_name} references non-existent server '#{ref}'" unless servers.include?(ref)
|
|
60
|
+
end
|
|
60
61
|
end
|
|
61
62
|
end
|
|
62
|
-
end
|
|
63
63
|
end
|
|
64
64
|
end
|
|
65
65
|
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Nvoi
|
|
4
|
+
module ConfigApi
|
|
5
|
+
module Actions
|
|
6
|
+
class SetService < Base
|
|
7
|
+
protected
|
|
8
|
+
|
|
9
|
+
def mutate(data, name:, servers:, image:, port: nil, command: nil, env: nil, mount: nil)
|
|
10
|
+
raise ArgumentError, "name is required" if name.nil? || name.to_s.empty?
|
|
11
|
+
raise ArgumentError, "servers is required" if servers.nil? || servers.empty?
|
|
12
|
+
raise ArgumentError, "servers must be an array" unless servers.is_a?(Array)
|
|
13
|
+
raise ArgumentError, "image is required" if image.nil? || image.to_s.empty?
|
|
14
|
+
|
|
15
|
+
validate_server_refs(data, servers)
|
|
16
|
+
|
|
17
|
+
app(data)["services"] ||= {}
|
|
18
|
+
app(data)["services"][name.to_s] = {
|
|
19
|
+
"servers" => servers.map(&:to_s),
|
|
20
|
+
"image" => image.to_s,
|
|
21
|
+
"port" => port,
|
|
22
|
+
"command" => command,
|
|
23
|
+
"env" => env,
|
|
24
|
+
"mount" => mount
|
|
25
|
+
}.compact
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def validate_server_refs(data, servers)
|
|
31
|
+
defined = (app(data)["servers"] || {}).keys
|
|
32
|
+
servers.each do |ref|
|
|
33
|
+
raise Errors::ConfigValidationError, "server '#{ref}' not found" unless defined.include?(ref.to_s)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
class DeleteService < Base
|
|
39
|
+
protected
|
|
40
|
+
|
|
41
|
+
def mutate(data, name:)
|
|
42
|
+
raise ArgumentError, "name is required" if name.nil? || name.to_s.empty?
|
|
43
|
+
|
|
44
|
+
services = app(data)["services"] || {}
|
|
45
|
+
raise Errors::ConfigValidationError, "service '#{name}' not found" unless services.key?(name.to_s)
|
|
46
|
+
|
|
47
|
+
services.delete(name.to_s)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -6,34 +6,34 @@ module Nvoi
|
|
|
6
6
|
class SetVolume < Base
|
|
7
7
|
protected
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
9
|
+
def mutate(data, server:, name:, size: 10)
|
|
10
|
+
raise ArgumentError, "server is required" if server.nil? || server.to_s.empty?
|
|
11
|
+
raise ArgumentError, "name is required" if name.nil? || name.to_s.empty?
|
|
12
|
+
raise ArgumentError, "size must be positive" if size && size < 1
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
servers = app(data)["servers"] ||= {}
|
|
15
|
+
raise Errors::ConfigValidationError, "server '#{server}' not found" unless servers.key?(server.to_s)
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
17
|
+
servers[server.to_s]["volumes"] ||= {}
|
|
18
|
+
servers[server.to_s]["volumes"][name.to_s] = { "size" => size }
|
|
19
|
+
end
|
|
20
20
|
end
|
|
21
21
|
|
|
22
22
|
class DeleteVolume < Base
|
|
23
23
|
protected
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
25
|
+
def mutate(data, server:, name:)
|
|
26
|
+
raise ArgumentError, "server is required" if server.nil? || server.to_s.empty?
|
|
27
|
+
raise ArgumentError, "name is required" if name.nil? || name.to_s.empty?
|
|
28
28
|
|
|
29
|
-
|
|
30
|
-
|
|
29
|
+
servers = app(data)["servers"] || {}
|
|
30
|
+
raise Errors::ConfigValidationError, "server '#{server}' not found" unless servers.key?(server.to_s)
|
|
31
31
|
|
|
32
|
-
|
|
33
|
-
|
|
32
|
+
volumes = servers[server.to_s]["volumes"] || {}
|
|
33
|
+
raise Errors::ConfigValidationError, "volume '#{name}' not found on server '#{server}'" unless volumes.key?(name.to_s)
|
|
34
34
|
|
|
35
|
-
|
|
36
|
-
|
|
35
|
+
volumes.delete(name.to_s)
|
|
36
|
+
end
|
|
37
37
|
end
|
|
38
38
|
end
|
|
39
39
|
end
|
data/lib/nvoi/config_api/base.rb
CHANGED
|
@@ -2,23 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
module Nvoi
|
|
4
4
|
module ConfigApi
|
|
5
|
+
# Base class for config transformations.
|
|
6
|
+
# Accepts a Hash, returns a Hash. No crypto - caller handles that.
|
|
5
7
|
class Base
|
|
6
|
-
def initialize(
|
|
7
|
-
@
|
|
8
|
-
@master_key = master_key
|
|
8
|
+
def initialize(data)
|
|
9
|
+
@data = data
|
|
9
10
|
end
|
|
10
11
|
|
|
11
12
|
def call(**args)
|
|
12
|
-
yaml = Utils::Crypto.decrypt(@encrypted_config, @master_key)
|
|
13
|
-
@data = YAML.safe_load(yaml, permitted_classes: [Symbol])
|
|
14
|
-
|
|
15
13
|
mutate(@data, **args)
|
|
16
14
|
validate(@data)
|
|
17
|
-
|
|
18
|
-
new_yaml = YAML.dump(@data)
|
|
19
|
-
Result.success(Utils::Crypto.encrypt(new_yaml, @master_key))
|
|
20
|
-
rescue Errors::DecryptionError, Errors::InvalidKeyError => e
|
|
21
|
-
Result.failure(:decryption_error, e.message)
|
|
15
|
+
Result.success(@data)
|
|
22
16
|
rescue Errors::ConfigValidationError => e
|
|
23
17
|
Result.failure(:validation_error, e.message)
|
|
24
18
|
rescue ArgumentError => e
|
|
@@ -27,18 +21,18 @@ module Nvoi
|
|
|
27
21
|
|
|
28
22
|
protected
|
|
29
23
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
24
|
+
def mutate(_data, **_args)
|
|
25
|
+
raise NotImplementedError
|
|
26
|
+
end
|
|
33
27
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
28
|
+
def validate(_data)
|
|
29
|
+
# Subclasses can override to add validation
|
|
30
|
+
# Default: no validation (lightweight actions like set_env don't need full config validation)
|
|
31
|
+
end
|
|
38
32
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
33
|
+
def app(data)
|
|
34
|
+
data["application"] ||= {}
|
|
35
|
+
end
|
|
42
36
|
end
|
|
43
37
|
end
|
|
44
38
|
end
|
|
@@ -3,18 +3,18 @@
|
|
|
3
3
|
module Nvoi
|
|
4
4
|
module ConfigApi
|
|
5
5
|
class Result
|
|
6
|
-
attr_reader :
|
|
6
|
+
attr_reader :data, :error_type, :error_message
|
|
7
7
|
|
|
8
|
-
def self.success(
|
|
9
|
-
new(
|
|
8
|
+
def self.success(data)
|
|
9
|
+
new(data:)
|
|
10
10
|
end
|
|
11
11
|
|
|
12
12
|
def self.failure(type, message)
|
|
13
13
|
new(error_type: type, error_message: message)
|
|
14
14
|
end
|
|
15
15
|
|
|
16
|
-
def initialize(
|
|
17
|
-
@
|
|
16
|
+
def initialize(data: nil, error_type: nil, error_message: nil)
|
|
17
|
+
@data = data
|
|
18
18
|
@error_type = error_type
|
|
19
19
|
@error_message = error_message
|
|
20
20
|
end
|
data/lib/nvoi/config_api.rb
CHANGED
|
@@ -3,67 +3,90 @@
|
|
|
3
3
|
module Nvoi
|
|
4
4
|
module ConfigApi
|
|
5
5
|
class << self
|
|
6
|
+
# Init (creates new config - special case, handles crypto)
|
|
7
|
+
def init(**args)
|
|
8
|
+
Actions::Init.new.call(**args)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# Domain Provider
|
|
12
|
+
def set_domain_provider(data, **args)
|
|
13
|
+
Actions::SetDomainProvider.new(data).call(**args)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def delete_domain_provider(data)
|
|
17
|
+
Actions::DeleteDomainProvider.new(data).call
|
|
18
|
+
end
|
|
19
|
+
|
|
6
20
|
# Compute Provider
|
|
7
|
-
def set_compute_provider(
|
|
8
|
-
Actions::SetComputeProvider.new(
|
|
21
|
+
def set_compute_provider(data, **args)
|
|
22
|
+
Actions::SetComputeProvider.new(data).call(**args)
|
|
9
23
|
end
|
|
10
24
|
|
|
11
|
-
def delete_compute_provider(
|
|
12
|
-
Actions::DeleteComputeProvider.new(
|
|
25
|
+
def delete_compute_provider(data)
|
|
26
|
+
Actions::DeleteComputeProvider.new(data).call
|
|
13
27
|
end
|
|
14
28
|
|
|
15
29
|
# Server
|
|
16
|
-
def set_server(
|
|
17
|
-
Actions::SetServer.new(
|
|
30
|
+
def set_server(data, **args)
|
|
31
|
+
Actions::SetServer.new(data).call(**args)
|
|
18
32
|
end
|
|
19
33
|
|
|
20
|
-
def delete_server(
|
|
21
|
-
Actions::DeleteServer.new(
|
|
34
|
+
def delete_server(data, **args)
|
|
35
|
+
Actions::DeleteServer.new(data).call(**args)
|
|
22
36
|
end
|
|
23
37
|
|
|
24
38
|
# Volume
|
|
25
|
-
def set_volume(
|
|
26
|
-
Actions::SetVolume.new(
|
|
39
|
+
def set_volume(data, **args)
|
|
40
|
+
Actions::SetVolume.new(data).call(**args)
|
|
27
41
|
end
|
|
28
42
|
|
|
29
|
-
def delete_volume(
|
|
30
|
-
Actions::DeleteVolume.new(
|
|
43
|
+
def delete_volume(data, **args)
|
|
44
|
+
Actions::DeleteVolume.new(data).call(**args)
|
|
31
45
|
end
|
|
32
46
|
|
|
33
47
|
# App
|
|
34
|
-
def set_app(
|
|
35
|
-
Actions::SetApp.new(
|
|
48
|
+
def set_app(data, **args)
|
|
49
|
+
Actions::SetApp.new(data).call(**args)
|
|
36
50
|
end
|
|
37
51
|
|
|
38
|
-
def delete_app(
|
|
39
|
-
Actions::DeleteApp.new(
|
|
52
|
+
def delete_app(data, **args)
|
|
53
|
+
Actions::DeleteApp.new(data).call(**args)
|
|
40
54
|
end
|
|
41
55
|
|
|
42
56
|
# Database
|
|
43
|
-
def set_database(
|
|
44
|
-
Actions::SetDatabase.new(
|
|
57
|
+
def set_database(data, **args)
|
|
58
|
+
Actions::SetDatabase.new(data).call(**args)
|
|
45
59
|
end
|
|
46
60
|
|
|
47
|
-
def delete_database(
|
|
48
|
-
Actions::DeleteDatabase.new(
|
|
61
|
+
def delete_database(data)
|
|
62
|
+
Actions::DeleteDatabase.new(data).call
|
|
49
63
|
end
|
|
50
64
|
|
|
51
65
|
# Secret
|
|
52
|
-
def set_secret(
|
|
53
|
-
Actions::SetSecret.new(
|
|
66
|
+
def set_secret(data, **args)
|
|
67
|
+
Actions::SetSecret.new(data).call(**args)
|
|
54
68
|
end
|
|
55
69
|
|
|
56
|
-
def delete_secret(
|
|
57
|
-
Actions::DeleteSecret.new(
|
|
70
|
+
def delete_secret(data, **args)
|
|
71
|
+
Actions::DeleteSecret.new(data).call(**args)
|
|
58
72
|
end
|
|
59
73
|
|
|
60
74
|
# Env
|
|
61
|
-
def set_env(
|
|
62
|
-
Actions::SetEnv.new(
|
|
75
|
+
def set_env(data, **args)
|
|
76
|
+
Actions::SetEnv.new(data).call(**args)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def delete_env(data, **args)
|
|
80
|
+
Actions::DeleteEnv.new(data).call(**args)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Service
|
|
84
|
+
def set_service(data, **args)
|
|
85
|
+
Actions::SetService.new(data).call(**args)
|
|
63
86
|
end
|
|
64
87
|
|
|
65
|
-
def
|
|
66
|
-
Actions::
|
|
88
|
+
def delete_service(data, **args)
|
|
89
|
+
Actions::DeleteService.new(data).call(**args)
|
|
67
90
|
end
|
|
68
91
|
end
|
|
69
92
|
end
|
|
@@ -229,7 +229,7 @@ module Nvoi
|
|
|
229
229
|
end
|
|
230
230
|
|
|
231
231
|
def wait_for_server(server_id, max_attempts)
|
|
232
|
-
server = Utils::Retry.poll(max_attempts
|
|
232
|
+
server = Utils::Retry.poll(max_attempts:, interval: 5) do
|
|
233
233
|
resp = @client.describe_instances(instance_ids: [server_id])
|
|
234
234
|
|
|
235
235
|
if resp.reservations.any? && resp.reservations[0].instances.any?
|
|
@@ -344,6 +344,31 @@ module Nvoi
|
|
|
344
344
|
raise Errors::ValidationError, "aws credentials invalid: #{e.message}"
|
|
345
345
|
end
|
|
346
346
|
|
|
347
|
+
# List available instance types for onboarding
|
|
348
|
+
def list_instance_types
|
|
349
|
+
# Common instance types (full list is huge)
|
|
350
|
+
common_types = %w[t3.micro t3.small t3.medium t3.large t3.xlarge m5.large m5.xlarge c5.large c5.xlarge]
|
|
351
|
+
resp = @client.describe_instance_types(instance_types: common_types)
|
|
352
|
+
resp.instance_types.map do |t|
|
|
353
|
+
{
|
|
354
|
+
name: t.instance_type,
|
|
355
|
+
vcpus: t.v_cpu_info.default_v_cpus,
|
|
356
|
+
memory: t.memory_info.size_in_mi_b
|
|
357
|
+
}
|
|
358
|
+
end
|
|
359
|
+
rescue StandardError
|
|
360
|
+
# Fallback to static list if API fails
|
|
361
|
+
common_types.map { |t| { name: t, vcpus: nil, memory: nil } }
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
# List available regions for onboarding
|
|
365
|
+
def list_regions
|
|
366
|
+
resp = @client.describe_regions
|
|
367
|
+
resp.regions.map do |r|
|
|
368
|
+
{ name: r.region_name, endpoint: r.endpoint }
|
|
369
|
+
end
|
|
370
|
+
end
|
|
371
|
+
|
|
347
372
|
private
|
|
348
373
|
|
|
349
374
|
def find_vpc_by_name(name)
|
|
@@ -136,7 +136,7 @@ module Nvoi
|
|
|
136
136
|
end
|
|
137
137
|
|
|
138
138
|
def wait_for_server(server_id, max_attempts)
|
|
139
|
-
server = Utils::Retry.poll(max_attempts
|
|
139
|
+
server = Utils::Retry.poll(max_attempts:, interval: Utils::Constants::SERVER_READY_INTERVAL) do
|
|
140
140
|
s = get("/servers/#{server_id.to_i}")["server"]
|
|
141
141
|
to_server(s) if s["status"] == "running"
|
|
142
142
|
end
|
|
@@ -243,6 +243,32 @@ module Nvoi
|
|
|
243
243
|
raise Errors::ValidationError, "hetzner credentials invalid: #{e.message}"
|
|
244
244
|
end
|
|
245
245
|
|
|
246
|
+
# List available server types for onboarding
|
|
247
|
+
def list_server_types
|
|
248
|
+
get("/server_types")["server_types"].map do |t|
|
|
249
|
+
{
|
|
250
|
+
name: t["name"],
|
|
251
|
+
description: t["description"],
|
|
252
|
+
cores: t["cores"],
|
|
253
|
+
memory: t["memory"],
|
|
254
|
+
disk: t["disk"],
|
|
255
|
+
price: t.dig("prices", 0, "price_monthly", "gross")
|
|
256
|
+
}
|
|
257
|
+
end
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
# List available locations for onboarding
|
|
261
|
+
def list_locations
|
|
262
|
+
get("/locations")["locations"].map do |l|
|
|
263
|
+
{
|
|
264
|
+
name: l["name"],
|
|
265
|
+
city: l["city"],
|
|
266
|
+
country: l["country"],
|
|
267
|
+
description: l["description"]
|
|
268
|
+
}
|
|
269
|
+
end
|
|
270
|
+
end
|
|
271
|
+
|
|
246
272
|
private
|
|
247
273
|
|
|
248
274
|
def get(path)
|