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
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Nvoi
|
|
4
|
-
module Remote
|
|
5
|
-
# SSHExecutor executes commands on remote servers via SSH
|
|
6
|
-
class SSHExecutor
|
|
7
|
-
attr_reader :ip, :ssh_key, :user
|
|
8
|
-
|
|
9
|
-
def initialize(ip, ssh_key)
|
|
10
|
-
@ip = ip
|
|
11
|
-
@ssh_key = ssh_key
|
|
12
|
-
@user = "deploy"
|
|
13
|
-
@strict_mode = ENV["SSH_STRICT_HOST_KEY_CHECKING"] == "true"
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
# Execute runs a command on the remote server
|
|
17
|
-
def execute(command, stream: false)
|
|
18
|
-
ssh_args = build_ssh_args
|
|
19
|
-
ssh_args += ["#{@user}@#{@ip}", command]
|
|
20
|
-
|
|
21
|
-
if stream
|
|
22
|
-
# Stream output to stdout/stderr for interactive commands
|
|
23
|
-
success = system("ssh", *ssh_args)
|
|
24
|
-
raise SSHCommandError, "SSH command failed" unless success
|
|
25
|
-
|
|
26
|
-
""
|
|
27
|
-
else
|
|
28
|
-
# Capture output
|
|
29
|
-
output, status = Open3.capture2e("ssh", *ssh_args)
|
|
30
|
-
|
|
31
|
-
unless status.success?
|
|
32
|
-
raise SSHCommandError, "SSH command failed (exit code: #{status.exitstatus}): #{output}"
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
output.strip
|
|
36
|
-
end
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
# Execute quietly, ignoring errors (useful for optional cleanup)
|
|
40
|
-
def execute_quiet(command)
|
|
41
|
-
execute(command)
|
|
42
|
-
rescue StandardError
|
|
43
|
-
# Ignore errors
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
# Open an interactive SSH shell
|
|
47
|
-
def open_shell
|
|
48
|
-
ssh_args = build_ssh_args
|
|
49
|
-
ssh_args += ["-t", "#{@user}@#{@ip}"]
|
|
50
|
-
|
|
51
|
-
exec("ssh", *ssh_args)
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
private
|
|
55
|
-
|
|
56
|
-
def build_ssh_args
|
|
57
|
-
args = ["-o", "LogLevel=ERROR", "-i", @ssh_key]
|
|
58
|
-
|
|
59
|
-
if @strict_mode
|
|
60
|
-
# Use known_hosts verification
|
|
61
|
-
known_hosts_path = File.join(Dir.home, ".ssh", "known_hosts")
|
|
62
|
-
args += ["-o", "StrictHostKeyChecking=accept-new", "-o", "UserKnownHostsFile=#{known_hosts_path}"]
|
|
63
|
-
else
|
|
64
|
-
# Disable host key checking (default for cloud environments)
|
|
65
|
-
args += ["-o", "StrictHostKeyChecking=no", "-o", "UserKnownHostsFile=/dev/null"]
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
args
|
|
69
|
-
end
|
|
70
|
-
end
|
|
71
|
-
end
|
|
72
|
-
end
|
|
@@ -1,103 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Nvoi
|
|
4
|
-
module Remote
|
|
5
|
-
# MountOptions contains options for mounting a volume
|
|
6
|
-
MountOptions = Struct.new(:device_path, :mount_path, :fs_type, keyword_init: true)
|
|
7
|
-
|
|
8
|
-
# VolumeManager handles volume mount operations via SSH
|
|
9
|
-
class VolumeManager
|
|
10
|
-
def initialize(ssh)
|
|
11
|
-
@ssh = ssh
|
|
12
|
-
end
|
|
13
|
-
|
|
14
|
-
# Mount formats (if needed) and mounts a volume at the specified path
|
|
15
|
-
def mount(opts)
|
|
16
|
-
fs_type = opts.fs_type || "xfs"
|
|
17
|
-
|
|
18
|
-
# Check if already mounted
|
|
19
|
-
return if mounted?(opts.mount_path)
|
|
20
|
-
|
|
21
|
-
# Wait for device to appear (Hetzner attachment is async)
|
|
22
|
-
wait_for_device(opts.device_path)
|
|
23
|
-
|
|
24
|
-
# Check if device has a filesystem
|
|
25
|
-
unless has_filesystem?(opts.device_path)
|
|
26
|
-
format_volume(opts.device_path, fs_type)
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
# Create mount point
|
|
30
|
-
@ssh.execute("sudo mkdir -p #{opts.mount_path}")
|
|
31
|
-
|
|
32
|
-
# Mount the volume
|
|
33
|
-
@ssh.execute("sudo mount #{opts.device_path} #{opts.mount_path}")
|
|
34
|
-
|
|
35
|
-
# Add to fstab for persistence
|
|
36
|
-
add_to_fstab(opts.device_path, opts.mount_path, fs_type)
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
# Unmount a volume
|
|
40
|
-
def unmount(mount_path)
|
|
41
|
-
return unless mounted?(mount_path)
|
|
42
|
-
|
|
43
|
-
@ssh.execute("sudo umount #{mount_path}")
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
# Check if a path is currently mounted
|
|
47
|
-
def mounted?(mount_path)
|
|
48
|
-
output = @ssh.execute("mountpoint -q #{mount_path} && echo 'mounted' || echo 'not_mounted'")
|
|
49
|
-
output.strip == "mounted"
|
|
50
|
-
rescue SSHCommandError
|
|
51
|
-
# mountpoint command might fail if path doesn't exist
|
|
52
|
-
false
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
# Remove a mount entry from /etc/fstab
|
|
56
|
-
def remove_from_fstab(mount_path)
|
|
57
|
-
@ssh.execute("sudo sed -i '\\|#{mount_path}|d' /etc/fstab")
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
private
|
|
61
|
-
|
|
62
|
-
def wait_for_device(device_path, timeout: 60)
|
|
63
|
-
attempts = 0
|
|
64
|
-
max_attempts = timeout / 2
|
|
65
|
-
|
|
66
|
-
loop do
|
|
67
|
-
output = @ssh.execute("test -e #{device_path} && echo 'exists' || echo 'missing'")
|
|
68
|
-
return if output.strip == "exists"
|
|
69
|
-
|
|
70
|
-
attempts += 1
|
|
71
|
-
if attempts >= max_attempts
|
|
72
|
-
raise VolumeError, "Timeout waiting for device #{device_path} to appear"
|
|
73
|
-
end
|
|
74
|
-
|
|
75
|
-
sleep(2)
|
|
76
|
-
end
|
|
77
|
-
end
|
|
78
|
-
|
|
79
|
-
def has_filesystem?(device_path)
|
|
80
|
-
output = @ssh.execute("sudo blkid #{device_path}")
|
|
81
|
-
output.include?("TYPE=")
|
|
82
|
-
rescue SSHCommandError
|
|
83
|
-
# blkid returns error if no filesystem
|
|
84
|
-
false
|
|
85
|
-
end
|
|
86
|
-
|
|
87
|
-
def format_volume(device_path, fs_type)
|
|
88
|
-
@ssh.execute("sudo mkfs.#{fs_type} #{device_path}")
|
|
89
|
-
end
|
|
90
|
-
|
|
91
|
-
def add_to_fstab(device_path, mount_path, fs_type)
|
|
92
|
-
# Check if entry already exists
|
|
93
|
-
output = @ssh.execute("grep -q '#{mount_path}' /etc/fstab && echo 'exists' || echo 'missing'")
|
|
94
|
-
return if output.strip == "exists"
|
|
95
|
-
|
|
96
|
-
# Add fstab entry using UUID for reliability
|
|
97
|
-
cmd = "UUID=$(sudo blkid -s UUID -o value #{device_path}) && " \
|
|
98
|
-
"echo \"UUID=$UUID #{mount_path} #{fs_type} defaults,nofail 0 2\" | sudo tee -a /etc/fstab"
|
|
99
|
-
@ssh.execute(cmd)
|
|
100
|
-
end
|
|
101
|
-
end
|
|
102
|
-
end
|
|
103
|
-
end
|
data/lib/nvoi/service/delete.rb
DELETED
|
@@ -1,234 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Nvoi
|
|
4
|
-
module Service
|
|
5
|
-
# DeleteService handles cleanup of cloud resources
|
|
6
|
-
class DeleteService
|
|
7
|
-
include ProviderHelper
|
|
8
|
-
|
|
9
|
-
attr_accessor :config_dir
|
|
10
|
-
|
|
11
|
-
def initialize(config_path, log)
|
|
12
|
-
@log = log
|
|
13
|
-
|
|
14
|
-
# Load configuration
|
|
15
|
-
@config = Config.load(config_path)
|
|
16
|
-
|
|
17
|
-
# Initialize provider
|
|
18
|
-
@provider = init_provider(@config)
|
|
19
|
-
|
|
20
|
-
# Initialize Cloudflare client
|
|
21
|
-
cf = @config.cloudflare
|
|
22
|
-
@cf_client = Cloudflare::Client.new(cf.api_token, cf.account_id)
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
def run
|
|
26
|
-
@log.info "Using %s Cloud provider", @config.provider_name
|
|
27
|
-
|
|
28
|
-
# Detach volumes first (must happen before server deletion)
|
|
29
|
-
detach_volumes
|
|
30
|
-
|
|
31
|
-
# Delete all servers from all groups
|
|
32
|
-
delete_all_servers
|
|
33
|
-
|
|
34
|
-
# Delete volumes (already detached)
|
|
35
|
-
delete_volumes
|
|
36
|
-
|
|
37
|
-
# Delete firewall
|
|
38
|
-
@log.info "Deleting firewall: %s", @config.firewall_name
|
|
39
|
-
begin
|
|
40
|
-
firewall = @provider.get_firewall_by_name(@config.firewall_name)
|
|
41
|
-
delete_firewall_with_retry(firewall.id) if firewall
|
|
42
|
-
rescue FirewallError => e
|
|
43
|
-
@log.warning "Firewall not found: %s", e.message
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
# Delete network
|
|
47
|
-
@log.info "Deleting network: %s", @config.network_name
|
|
48
|
-
begin
|
|
49
|
-
network = @provider.get_network_by_name(@config.network_name)
|
|
50
|
-
if network
|
|
51
|
-
@provider.delete_network(network.id)
|
|
52
|
-
@log.success "Network deleted"
|
|
53
|
-
end
|
|
54
|
-
rescue NetworkError => e
|
|
55
|
-
@log.warning "Network not found: %s", e.message
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
# Delete Cloudflare resources
|
|
59
|
-
delete_cloudflare_resources
|
|
60
|
-
|
|
61
|
-
@log.success "Cleanup complete"
|
|
62
|
-
end
|
|
63
|
-
|
|
64
|
-
private
|
|
65
|
-
|
|
66
|
-
def delete_firewall_with_retry(firewall_id, max_retries: 5)
|
|
67
|
-
max_retries.times do |i|
|
|
68
|
-
begin
|
|
69
|
-
@provider.delete_firewall(firewall_id)
|
|
70
|
-
@log.success "Firewall deleted"
|
|
71
|
-
return
|
|
72
|
-
rescue StandardError => e
|
|
73
|
-
if i == max_retries - 1
|
|
74
|
-
raise ServiceError, "failed to delete firewall after #{max_retries} attempts: #{e.message}"
|
|
75
|
-
end
|
|
76
|
-
|
|
77
|
-
@log.info "Firewall still in use, waiting 3s before retry (%d/%d)", i + 1, max_retries
|
|
78
|
-
sleep(3)
|
|
79
|
-
end
|
|
80
|
-
end
|
|
81
|
-
end
|
|
82
|
-
|
|
83
|
-
def detach_volumes
|
|
84
|
-
volume_names = collect_volume_names
|
|
85
|
-
return if volume_names.empty?
|
|
86
|
-
|
|
87
|
-
@log.info "Detaching %d volume(s)", volume_names.size
|
|
88
|
-
|
|
89
|
-
volume_names.each do |vol_name|
|
|
90
|
-
begin
|
|
91
|
-
volume = @provider.get_volume_by_name(vol_name)
|
|
92
|
-
next unless volume&.server_id && !volume.server_id.empty?
|
|
93
|
-
|
|
94
|
-
@log.info "Detaching volume: %s", vol_name
|
|
95
|
-
@provider.detach_volume(volume.id)
|
|
96
|
-
@log.success "Volume detached: %s", vol_name
|
|
97
|
-
rescue StandardError => e
|
|
98
|
-
@log.warning "Failed to detach volume %s: %s", vol_name, e.message
|
|
99
|
-
end
|
|
100
|
-
end
|
|
101
|
-
end
|
|
102
|
-
|
|
103
|
-
def delete_volumes
|
|
104
|
-
volume_names = collect_volume_names
|
|
105
|
-
return if volume_names.empty?
|
|
106
|
-
|
|
107
|
-
@log.info "Deleting %d volume(s)", volume_names.size
|
|
108
|
-
|
|
109
|
-
volume_names.each do |vol_name|
|
|
110
|
-
@log.info "Deleting volume: %s", vol_name
|
|
111
|
-
|
|
112
|
-
begin
|
|
113
|
-
volume = @provider.get_volume_by_name(vol_name)
|
|
114
|
-
unless volume
|
|
115
|
-
@log.info "Volume not found: %s", vol_name
|
|
116
|
-
next
|
|
117
|
-
end
|
|
118
|
-
|
|
119
|
-
@provider.delete_volume(volume.id)
|
|
120
|
-
@log.success "Volume deleted: %s", vol_name
|
|
121
|
-
rescue StandardError => e
|
|
122
|
-
@log.warning "Failed to delete volume %s: %s", vol_name, e.message
|
|
123
|
-
end
|
|
124
|
-
end
|
|
125
|
-
end
|
|
126
|
-
|
|
127
|
-
def collect_volume_names
|
|
128
|
-
namer = @config.namer
|
|
129
|
-
names = []
|
|
130
|
-
|
|
131
|
-
# Database volume
|
|
132
|
-
db = @config.deploy.application.database
|
|
133
|
-
names << namer.database_volume_name if db&.volume && !db.volume.empty?
|
|
134
|
-
|
|
135
|
-
# Service volumes
|
|
136
|
-
@config.deploy.application.services.each do |svc_name, svc|
|
|
137
|
-
names << namer.service_volume_name(svc_name, "data") if svc&.volume && !svc.volume.empty?
|
|
138
|
-
end
|
|
139
|
-
|
|
140
|
-
# App volumes
|
|
141
|
-
@config.deploy.application.app.each do |app_name, app|
|
|
142
|
-
next unless app&.volumes && !app.volumes.empty?
|
|
143
|
-
|
|
144
|
-
app.volumes.keys.each do |vol_key|
|
|
145
|
-
names << namer.app_volume_name(app_name, vol_key)
|
|
146
|
-
end
|
|
147
|
-
end
|
|
148
|
-
|
|
149
|
-
names
|
|
150
|
-
end
|
|
151
|
-
|
|
152
|
-
def delete_all_servers
|
|
153
|
-
servers = @config.deploy.application.servers
|
|
154
|
-
return if servers.empty?
|
|
155
|
-
|
|
156
|
-
servers.each do |group_name, group_config|
|
|
157
|
-
next unless group_config
|
|
158
|
-
|
|
159
|
-
count = group_config.count.positive? ? group_config.count : 1
|
|
160
|
-
@log.info "Deleting %d server(s) from group '%s'", count, group_name
|
|
161
|
-
|
|
162
|
-
(1..count).each do |i|
|
|
163
|
-
server_name = @config.namer.server_name(group_name, i)
|
|
164
|
-
@log.info "Deleting server: %s", server_name
|
|
165
|
-
|
|
166
|
-
begin
|
|
167
|
-
server = @provider.find_server(server_name)
|
|
168
|
-
if server
|
|
169
|
-
@provider.delete_server(server.id)
|
|
170
|
-
@log.success "Server deleted: %s", server_name
|
|
171
|
-
end
|
|
172
|
-
rescue StandardError => e
|
|
173
|
-
@log.warning "Failed to delete server %s: %s", server_name, e.message
|
|
174
|
-
end
|
|
175
|
-
end
|
|
176
|
-
end
|
|
177
|
-
end
|
|
178
|
-
|
|
179
|
-
def delete_cloudflare_resources
|
|
180
|
-
@config.deploy.application.app.each do |service_name, service|
|
|
181
|
-
next unless service&.domain && !service.domain.empty?
|
|
182
|
-
next if service.subdomain.nil?
|
|
183
|
-
|
|
184
|
-
delete_tunnel_and_dns(service_name, service.domain, service.subdomain)
|
|
185
|
-
end
|
|
186
|
-
end
|
|
187
|
-
|
|
188
|
-
def delete_tunnel_and_dns(service_name, domain, subdomain)
|
|
189
|
-
tunnel_name = @config.namer.tunnel_name(service_name)
|
|
190
|
-
hostname = build_hostname(subdomain, domain)
|
|
191
|
-
|
|
192
|
-
# Delete tunnel
|
|
193
|
-
@log.info "Deleting Cloudflare tunnel: %s", tunnel_name
|
|
194
|
-
begin
|
|
195
|
-
tunnel = @cf_client.find_tunnel(tunnel_name)
|
|
196
|
-
if tunnel
|
|
197
|
-
@cf_client.delete_tunnel(tunnel.id)
|
|
198
|
-
@log.success "Tunnel deleted: %s", tunnel_name
|
|
199
|
-
end
|
|
200
|
-
rescue StandardError => e
|
|
201
|
-
@log.warning "Failed to delete tunnel: %s", e.message
|
|
202
|
-
end
|
|
203
|
-
|
|
204
|
-
# Delete DNS record
|
|
205
|
-
@log.info "Deleting DNS record: %s", hostname
|
|
206
|
-
begin
|
|
207
|
-
zone = @cf_client.find_zone(domain)
|
|
208
|
-
unless zone
|
|
209
|
-
@log.warning "Zone not found: %s", domain
|
|
210
|
-
return
|
|
211
|
-
end
|
|
212
|
-
|
|
213
|
-
record = @cf_client.find_dns_record(zone.id, hostname, "CNAME")
|
|
214
|
-
if record
|
|
215
|
-
@cf_client.delete_dns_record(zone.id, record.id)
|
|
216
|
-
@log.success "DNS record deleted: %s", hostname
|
|
217
|
-
end
|
|
218
|
-
rescue StandardError => e
|
|
219
|
-
@log.warning "Failed to delete DNS record: %s", e.message
|
|
220
|
-
end
|
|
221
|
-
end
|
|
222
|
-
|
|
223
|
-
# Build hostname from subdomain and domain
|
|
224
|
-
# Supports: "app" -> "app.example.com", "" or "@" -> "example.com", "*" -> "*.example.com"
|
|
225
|
-
def build_hostname(subdomain, domain)
|
|
226
|
-
if subdomain.nil? || subdomain.empty? || subdomain == "@"
|
|
227
|
-
domain
|
|
228
|
-
else
|
|
229
|
-
"#{subdomain}.#{domain}"
|
|
230
|
-
end
|
|
231
|
-
end
|
|
232
|
-
end
|
|
233
|
-
end
|
|
234
|
-
end
|
data/lib/nvoi/service/deploy.rb
DELETED
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Nvoi
|
|
4
|
-
module Service
|
|
5
|
-
# DeployService orchestrates the deployment process
|
|
6
|
-
class DeployService
|
|
7
|
-
include ProviderHelper
|
|
8
|
-
|
|
9
|
-
attr_accessor :config_dir, :dockerfile_path
|
|
10
|
-
|
|
11
|
-
def initialize(config_path, working_dir, log)
|
|
12
|
-
@working_dir = working_dir
|
|
13
|
-
@log = log
|
|
14
|
-
|
|
15
|
-
# Load configuration
|
|
16
|
-
@config = Config.load(config_path)
|
|
17
|
-
|
|
18
|
-
# Initialize provider
|
|
19
|
-
@provider = init_provider(@config)
|
|
20
|
-
|
|
21
|
-
# Validate provider-specific configuration
|
|
22
|
-
validate_provider_config(@config, @provider)
|
|
23
|
-
|
|
24
|
-
@log.info "Using %s Cloud provider", @config.provider_name
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
def run
|
|
28
|
-
@log.info "Starting deployment"
|
|
29
|
-
@log.separator
|
|
30
|
-
|
|
31
|
-
# Step 1: Provision server
|
|
32
|
-
server_ip = provision_server
|
|
33
|
-
raise DeploymentError.new("server provisioning", "failed") unless server_ip
|
|
34
|
-
|
|
35
|
-
# Step 2: Configure tunnels
|
|
36
|
-
tunnels = configure_tunnels
|
|
37
|
-
|
|
38
|
-
# Step 3: Deploy application
|
|
39
|
-
deploy_application(server_ip, tunnels)
|
|
40
|
-
|
|
41
|
-
# Success
|
|
42
|
-
@log.separator
|
|
43
|
-
@log.success "Deployment complete"
|
|
44
|
-
|
|
45
|
-
# Log service URLs
|
|
46
|
-
tunnels.each do |tunnel|
|
|
47
|
-
@log.info "Service %s: https://%s", tunnel.service_name, tunnel.hostname
|
|
48
|
-
end
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
private
|
|
52
|
-
|
|
53
|
-
def provision_server
|
|
54
|
-
# Step 1: Provision all servers (main + workers)
|
|
55
|
-
provisioner = Steps::ServerProvisioner.new(@config, @provider, @log)
|
|
56
|
-
main_server_ip = provisioner.run
|
|
57
|
-
|
|
58
|
-
# Step 2: Provision volumes (create, attach, mount)
|
|
59
|
-
volume_provisioner = Steps::VolumeProvisioner.new(@config, @provider, @log)
|
|
60
|
-
volume_provisioner.run
|
|
61
|
-
|
|
62
|
-
# Step 3: Setup K3s cluster (main server + join workers)
|
|
63
|
-
cluster_setup = Steps::K3sClusterSetup.new(@config, @provider, @log, main_server_ip)
|
|
64
|
-
cluster_setup.run
|
|
65
|
-
|
|
66
|
-
main_server_ip
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
def configure_tunnels
|
|
70
|
-
configurator = Steps::TunnelConfigurator.new(@config, @log)
|
|
71
|
-
configurator.run
|
|
72
|
-
end
|
|
73
|
-
|
|
74
|
-
def deploy_application(server_ip, tunnels)
|
|
75
|
-
app_deployer = Steps::ApplicationDeployer.new(@config, @provider, @working_dir, server_ip, tunnels, @log)
|
|
76
|
-
app_deployer.run
|
|
77
|
-
end
|
|
78
|
-
end
|
|
79
|
-
end
|
|
80
|
-
end
|
data/lib/nvoi/service/exec.rb
DELETED
|
@@ -1,144 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Nvoi
|
|
4
|
-
module Service
|
|
5
|
-
# ExecService handles remote command execution on servers
|
|
6
|
-
class ExecService
|
|
7
|
-
include ProviderHelper
|
|
8
|
-
|
|
9
|
-
def initialize(config_path, log)
|
|
10
|
-
@log = log
|
|
11
|
-
|
|
12
|
-
# Load configuration
|
|
13
|
-
@config = Config.load(config_path)
|
|
14
|
-
|
|
15
|
-
# Initialize provider
|
|
16
|
-
@provider = init_provider(@config)
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
# Run executes a command on a specific server
|
|
20
|
-
def run(command, server_name)
|
|
21
|
-
# Resolve server name (main, worker-1, etc. -> actual server name)
|
|
22
|
-
actual_server_name = resolve_server_name(server_name)
|
|
23
|
-
|
|
24
|
-
# Find server using provider
|
|
25
|
-
server = find_server(actual_server_name)
|
|
26
|
-
|
|
27
|
-
@log.info "Connecting to %s (%s)", actual_server_name, server.public_ipv4
|
|
28
|
-
|
|
29
|
-
# Create SSH executor
|
|
30
|
-
ssh = Remote::SSHExecutor.new(server.public_ipv4, @config.ssh_key_path)
|
|
31
|
-
|
|
32
|
-
# Execute command with streaming output
|
|
33
|
-
@log.info "Executing: %s", command
|
|
34
|
-
output = ssh.execute(command, stream: true)
|
|
35
|
-
|
|
36
|
-
# Output is already streamed, but if there's buffered output, show it
|
|
37
|
-
puts output if !output.empty? && !output.include?("\n")
|
|
38
|
-
|
|
39
|
-
@log.success "Command completed successfully"
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
# RunAll executes a command on all servers (main + workers)
|
|
43
|
-
def run_all(command)
|
|
44
|
-
# Get all server names
|
|
45
|
-
server_names = get_all_server_names
|
|
46
|
-
|
|
47
|
-
raise ServiceError, "no servers found in configuration" if server_names.empty?
|
|
48
|
-
|
|
49
|
-
@log.info "Executing on %d server(s): %s", server_names.size, server_names.join(", ")
|
|
50
|
-
@log.separator
|
|
51
|
-
|
|
52
|
-
# Execute in parallel with threads
|
|
53
|
-
results = {}
|
|
54
|
-
mutex = Mutex.new
|
|
55
|
-
threads = server_names.map do |name|
|
|
56
|
-
Thread.new do
|
|
57
|
-
actual_name = resolve_server_name(name)
|
|
58
|
-
begin
|
|
59
|
-
server = find_server(actual_name)
|
|
60
|
-
ssh = Remote::SSHExecutor.new(server.public_ipv4, @config.ssh_key_path)
|
|
61
|
-
|
|
62
|
-
@log.info "[%s] Executing...", name
|
|
63
|
-
output = ssh.execute(command)
|
|
64
|
-
|
|
65
|
-
# Print output with server prefix
|
|
66
|
-
output.strip.split("\n").each do |line|
|
|
67
|
-
puts "[#{name}] #{line}"
|
|
68
|
-
end
|
|
69
|
-
|
|
70
|
-
mutex.synchronize { results[name] = nil }
|
|
71
|
-
rescue StandardError => e
|
|
72
|
-
@log.error "[%s] Failed: %s", name, e.message
|
|
73
|
-
mutex.synchronize { results[name] = e }
|
|
74
|
-
end
|
|
75
|
-
end
|
|
76
|
-
end
|
|
77
|
-
|
|
78
|
-
threads.each(&:join)
|
|
79
|
-
|
|
80
|
-
@log.separator
|
|
81
|
-
|
|
82
|
-
failures = results.select { |_, err| err }.keys
|
|
83
|
-
if failures.any?
|
|
84
|
-
@log.warning "Command failed on %d server(s): %s", failures.size, failures.join(", ")
|
|
85
|
-
raise ServiceError, "command failed on some servers"
|
|
86
|
-
end
|
|
87
|
-
|
|
88
|
-
@log.success "Command completed successfully on all servers"
|
|
89
|
-
end
|
|
90
|
-
|
|
91
|
-
# OpenShell opens an interactive SSH shell on a specific server
|
|
92
|
-
def open_shell(server_name)
|
|
93
|
-
actual_server_name = resolve_server_name(server_name)
|
|
94
|
-
server = find_server(actual_server_name)
|
|
95
|
-
|
|
96
|
-
@log.info "Opening SSH shell to %s (%s)", actual_server_name, server.public_ipv4
|
|
97
|
-
|
|
98
|
-
ssh = Remote::SSHExecutor.new(server.public_ipv4, @config.ssh_key_path)
|
|
99
|
-
ssh.open_shell
|
|
100
|
-
end
|
|
101
|
-
|
|
102
|
-
private
|
|
103
|
-
|
|
104
|
-
def resolve_server_name(name)
|
|
105
|
-
return @config.server_name if name.nil? || name.empty? || name == "main"
|
|
106
|
-
|
|
107
|
-
# Check if name matches "{group}-{n}" pattern
|
|
108
|
-
parts = name.split("-")
|
|
109
|
-
if parts.length >= 2
|
|
110
|
-
num_str = parts.last
|
|
111
|
-
if num_str.match?(/^\d+$/)
|
|
112
|
-
group_name = parts[0...-1].join("-")
|
|
113
|
-
return @config.namer.server_name(group_name, num_str.to_i)
|
|
114
|
-
end
|
|
115
|
-
end
|
|
116
|
-
|
|
117
|
-
# Assume it's a group name, return first server
|
|
118
|
-
@config.namer.server_name(name, 1)
|
|
119
|
-
end
|
|
120
|
-
|
|
121
|
-
def get_all_server_names
|
|
122
|
-
names = []
|
|
123
|
-
|
|
124
|
-
@config.deploy.application.servers.each do |group_name, group_config|
|
|
125
|
-
next unless group_config
|
|
126
|
-
|
|
127
|
-
count = group_config.count.positive? ? group_config.count : 1
|
|
128
|
-
(1..count).each do |i|
|
|
129
|
-
names << @config.namer.server_name(group_name, i)
|
|
130
|
-
end
|
|
131
|
-
end
|
|
132
|
-
|
|
133
|
-
names
|
|
134
|
-
end
|
|
135
|
-
|
|
136
|
-
def find_server(server_name)
|
|
137
|
-
server = @provider.find_server(server_name)
|
|
138
|
-
raise ServiceError, "server not found: #{server_name}" unless server
|
|
139
|
-
|
|
140
|
-
server
|
|
141
|
-
end
|
|
142
|
-
end
|
|
143
|
-
end
|
|
144
|
-
end
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Nvoi
|
|
4
|
-
module Service
|
|
5
|
-
# Provider initialization helpers
|
|
6
|
-
module ProviderHelper
|
|
7
|
-
def init_provider(config)
|
|
8
|
-
case config.provider_name
|
|
9
|
-
when "hetzner"
|
|
10
|
-
h = config.hetzner
|
|
11
|
-
Providers::Hetzner.new(h.api_token)
|
|
12
|
-
when "aws"
|
|
13
|
-
a = config.aws
|
|
14
|
-
Providers::AWS.new(a.access_key_id, a.secret_access_key, a.region)
|
|
15
|
-
else
|
|
16
|
-
raise ProviderError, "unknown provider: #{config.provider_name}"
|
|
17
|
-
end
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
def validate_provider_config(config, provider)
|
|
21
|
-
case config.provider_name
|
|
22
|
-
when "hetzner"
|
|
23
|
-
h = config.hetzner
|
|
24
|
-
provider.validate_credentials
|
|
25
|
-
provider.validate_instance_type(h.server_type)
|
|
26
|
-
provider.validate_region(h.server_location)
|
|
27
|
-
when "aws"
|
|
28
|
-
a = config.aws
|
|
29
|
-
provider.validate_credentials
|
|
30
|
-
provider.validate_instance_type(a.instance_type)
|
|
31
|
-
provider.validate_region(a.region)
|
|
32
|
-
end
|
|
33
|
-
end
|
|
34
|
-
end
|
|
35
|
-
end
|
|
36
|
-
end
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Nvoi
|
|
4
|
-
module Steps
|
|
5
|
-
# ApplicationDeployer orchestrates the full application deployment
|
|
6
|
-
class ApplicationDeployer
|
|
7
|
-
def initialize(config, provider, working_dir, server_ip, tunnels, log)
|
|
8
|
-
@config = config
|
|
9
|
-
@provider = provider
|
|
10
|
-
@working_dir = working_dir
|
|
11
|
-
@server_ip = server_ip
|
|
12
|
-
@tunnels = tunnels
|
|
13
|
-
@log = log
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
def run
|
|
17
|
-
@log.info "Deploying application"
|
|
18
|
-
|
|
19
|
-
orchestrator = Deployer::Orchestrator.new(@config, @provider, @log)
|
|
20
|
-
orchestrator.run(@server_ip, @tunnels, @working_dir)
|
|
21
|
-
|
|
22
|
-
@log.success "Application deployed"
|
|
23
|
-
end
|
|
24
|
-
end
|
|
25
|
-
end
|
|
26
|
-
end
|