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
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Nvoi
|
|
4
|
+
class Cli
|
|
5
|
+
module Delete
|
|
6
|
+
module Steps
|
|
7
|
+
# TeardownVolume handles volume deletion after detachment
|
|
8
|
+
class TeardownVolume
|
|
9
|
+
def initialize(config, provider, log)
|
|
10
|
+
@config = config
|
|
11
|
+
@provider = provider
|
|
12
|
+
@log = log
|
|
13
|
+
@namer = config.namer
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def run
|
|
17
|
+
volume_names = collect_volume_names
|
|
18
|
+
return if volume_names.empty?
|
|
19
|
+
|
|
20
|
+
@log.info "Deleting %d volume(s)", volume_names.size
|
|
21
|
+
|
|
22
|
+
volume_names.each do |vol_name|
|
|
23
|
+
delete_volume(vol_name)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def collect_volume_names
|
|
30
|
+
names = []
|
|
31
|
+
|
|
32
|
+
@config.deploy.application.servers.each do |server_name, server_config|
|
|
33
|
+
next unless server_config.volumes && !server_config.volumes.empty?
|
|
34
|
+
|
|
35
|
+
server_config.volumes.each_key do |vol_name|
|
|
36
|
+
names << @namer.server_volume_name(server_name, vol_name)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
names
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def delete_volume(vol_name)
|
|
44
|
+
@log.info "Deleting volume: %s", vol_name
|
|
45
|
+
|
|
46
|
+
volume = @provider.get_volume_by_name(vol_name)
|
|
47
|
+
unless volume
|
|
48
|
+
@log.info "Volume not found: %s", vol_name
|
|
49
|
+
return
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
@provider.delete_volume(volume.id)
|
|
53
|
+
@log.success "Volume deleted: %s", vol_name
|
|
54
|
+
rescue StandardError => e
|
|
55
|
+
@log.warning "Failed to delete volume %s: %s", vol_name, e.message
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Nvoi
|
|
4
|
+
class Cli
|
|
5
|
+
module Deploy
|
|
6
|
+
# Command orchestrates the full deployment process
|
|
7
|
+
class Command
|
|
8
|
+
def initialize(options)
|
|
9
|
+
@options = options
|
|
10
|
+
@log = Nvoi.logger
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def run
|
|
14
|
+
@log.info "Deploy CLI %s", VERSION
|
|
15
|
+
|
|
16
|
+
# Load configuration
|
|
17
|
+
config_path = resolve_config_path
|
|
18
|
+
working_dir = @options[:dir] || "."
|
|
19
|
+
dockerfile_path = @options[:dockerfile_path] || File.join(working_dir, "Dockerfile")
|
|
20
|
+
|
|
21
|
+
@config = Utils::ConfigLoader.load(config_path)
|
|
22
|
+
|
|
23
|
+
# Apply branch override if specified
|
|
24
|
+
apply_branch_override if @options[:branch]
|
|
25
|
+
|
|
26
|
+
# Initialize cloud provider
|
|
27
|
+
@provider = External::Cloud.for(@config)
|
|
28
|
+
validate_provider_config
|
|
29
|
+
|
|
30
|
+
@log.info "Using %s Cloud provider", @config.provider_name
|
|
31
|
+
@log.info "Starting deployment"
|
|
32
|
+
@log.separator
|
|
33
|
+
|
|
34
|
+
# Step 1: Provision infrastructure (network, servers)
|
|
35
|
+
main_server_ip = provision_infrastructure
|
|
36
|
+
|
|
37
|
+
# Step 2: Configure Cloudflare tunnels
|
|
38
|
+
tunnels = configure_tunnels
|
|
39
|
+
|
|
40
|
+
# Step 3: Deploy application
|
|
41
|
+
deploy_application(main_server_ip, tunnels, working_dir)
|
|
42
|
+
|
|
43
|
+
# Success
|
|
44
|
+
@log.separator
|
|
45
|
+
@log.success "Deployment complete"
|
|
46
|
+
|
|
47
|
+
# Log service URLs
|
|
48
|
+
tunnels.each do |tunnel|
|
|
49
|
+
@log.info "Service %s: https://%s", tunnel.service_name, tunnel.hostname
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
def resolve_config_path
|
|
56
|
+
config_path = @options[:config] || "deploy.enc"
|
|
57
|
+
working_dir = @options[:dir]
|
|
58
|
+
|
|
59
|
+
if config_path == "deploy.enc" && working_dir && working_dir != "."
|
|
60
|
+
File.join(working_dir, "deploy.enc")
|
|
61
|
+
else
|
|
62
|
+
config_path
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def apply_branch_override
|
|
67
|
+
branch = @options[:branch]
|
|
68
|
+
return if branch.nil? || branch.empty?
|
|
69
|
+
|
|
70
|
+
override = Objects::ConfigOverride.new(branch:)
|
|
71
|
+
override.apply(@config)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def validate_provider_config
|
|
75
|
+
case @config.provider_name
|
|
76
|
+
when "hetzner"
|
|
77
|
+
h = @config.hetzner
|
|
78
|
+
@provider.validate_credentials
|
|
79
|
+
@provider.validate_instance_type(h.server_type)
|
|
80
|
+
@provider.validate_region(h.server_location)
|
|
81
|
+
when "aws"
|
|
82
|
+
a = @config.aws
|
|
83
|
+
@provider.validate_credentials
|
|
84
|
+
@provider.validate_instance_type(a.instance_type)
|
|
85
|
+
@provider.validate_region(a.region)
|
|
86
|
+
when "scaleway"
|
|
87
|
+
s = @config.scaleway
|
|
88
|
+
@provider.validate_credentials
|
|
89
|
+
@provider.validate_instance_type(s.server_type)
|
|
90
|
+
@provider.validate_region(s.zone)
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def provision_infrastructure
|
|
95
|
+
require_relative "steps/provision_network"
|
|
96
|
+
require_relative "steps/provision_server"
|
|
97
|
+
require_relative "steps/provision_volume"
|
|
98
|
+
require_relative "steps/setup_k3s"
|
|
99
|
+
|
|
100
|
+
# Step 1: Provision network and firewall
|
|
101
|
+
network, firewall = Steps::ProvisionNetwork.new(@config, @provider, @log).run
|
|
102
|
+
|
|
103
|
+
# Step 2: Provision servers
|
|
104
|
+
main_server_ip = Steps::ProvisionServer.new(@config, @provider, @log, network, firewall).run
|
|
105
|
+
|
|
106
|
+
# Step 3: Provision and mount volumes
|
|
107
|
+
Steps::ProvisionVolume.new(@config, @provider, @log).run
|
|
108
|
+
|
|
109
|
+
# Step 4: Setup K3s cluster
|
|
110
|
+
Steps::SetupK3s.new(@config, @provider, @log, main_server_ip).run
|
|
111
|
+
|
|
112
|
+
main_server_ip
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def configure_tunnels
|
|
116
|
+
require_relative "steps/configure_tunnel"
|
|
117
|
+
Steps::ConfigureTunnel.new(@config, @log).run
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def deploy_application(server_ip, tunnels, working_dir)
|
|
121
|
+
require_relative "steps/build_image"
|
|
122
|
+
require_relative "steps/deploy_service"
|
|
123
|
+
require_relative "steps/cleanup_images"
|
|
124
|
+
|
|
125
|
+
ssh = External::Ssh.new(server_ip, @config.ssh_key_path)
|
|
126
|
+
|
|
127
|
+
# Acquire deployment lock
|
|
128
|
+
acquire_lock(ssh)
|
|
129
|
+
|
|
130
|
+
begin
|
|
131
|
+
# Build and push image
|
|
132
|
+
timestamp = Time.now.strftime("%Y%m%d%H%M%S")
|
|
133
|
+
image_tag = @config.namer.image_tag(timestamp)
|
|
134
|
+
|
|
135
|
+
Steps::BuildImage.new(@config, ssh, @log).run(working_dir, image_tag)
|
|
136
|
+
|
|
137
|
+
# Deploy all services
|
|
138
|
+
Steps::DeployService.new(@config, ssh, tunnels, @log).run(image_tag, timestamp)
|
|
139
|
+
|
|
140
|
+
# Cleanup old images
|
|
141
|
+
Steps::CleanupImages.new(@config, ssh, @log).run(timestamp)
|
|
142
|
+
ensure
|
|
143
|
+
release_lock(ssh)
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def acquire_lock(ssh)
|
|
148
|
+
lock_file = @config.namer.deployment_lock_file_path
|
|
149
|
+
|
|
150
|
+
output = ssh.execute("test -f #{lock_file} && cat #{lock_file} || echo ''")
|
|
151
|
+
output = output.strip
|
|
152
|
+
|
|
153
|
+
unless output.empty?
|
|
154
|
+
timestamp = output.to_i
|
|
155
|
+
if timestamp > 0
|
|
156
|
+
lock_time = Time.at(timestamp)
|
|
157
|
+
age = Time.now - lock_time
|
|
158
|
+
|
|
159
|
+
if age < Utils::Constants::STALE_DEPLOYMENT_LOCK_AGE
|
|
160
|
+
raise Errors::DeploymentError.new(
|
|
161
|
+
"lock",
|
|
162
|
+
"deployment already in progress (started #{age.round}s ago). Wait or remove lock file: #{lock_file}"
|
|
163
|
+
)
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
@log.warning "Removing stale deployment lock (age: #{age.round}s)"
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
ssh.execute("echo #{Time.now.to_i} > #{lock_file}")
|
|
171
|
+
@log.info "Deployment lock acquired: %s", lock_file
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def release_lock(ssh)
|
|
175
|
+
lock_file = @config.namer.deployment_lock_file_path
|
|
176
|
+
@log.info "Releasing deployment lock"
|
|
177
|
+
ssh.execute("rm -f #{lock_file}")
|
|
178
|
+
rescue StandardError
|
|
179
|
+
# Ignore errors during lock release
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Nvoi
|
|
4
|
+
class Cli
|
|
5
|
+
module Deploy
|
|
6
|
+
module Steps
|
|
7
|
+
# BuildImage handles Docker image building and pushing to cluster
|
|
8
|
+
class BuildImage
|
|
9
|
+
def initialize(config, ssh, log)
|
|
10
|
+
@config = config
|
|
11
|
+
@ssh = ssh
|
|
12
|
+
@log = log
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def run(working_dir, image_tag)
|
|
16
|
+
@log.info "Building Docker image: %s", image_tag
|
|
17
|
+
|
|
18
|
+
containerd = External::Containerd.new(@ssh)
|
|
19
|
+
containerd.build_and_deploy_image(working_dir, image_tag, cache_from: @config.namer.latest_image_tag)
|
|
20
|
+
|
|
21
|
+
@log.success "Image built: %s", image_tag
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Nvoi
|
|
4
|
+
class Cli
|
|
5
|
+
module Deploy
|
|
6
|
+
module Steps
|
|
7
|
+
# CleanupImages handles cleanup of old container images
|
|
8
|
+
class CleanupImages
|
|
9
|
+
def initialize(config, ssh, log)
|
|
10
|
+
@config = config
|
|
11
|
+
@ssh = ssh
|
|
12
|
+
@log = log
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def run(current_tag)
|
|
16
|
+
keep_count = @config.keep_count_value
|
|
17
|
+
prefix = @config.container_prefix
|
|
18
|
+
|
|
19
|
+
@log.info "Cleaning up old images (keeping %d)", keep_count
|
|
20
|
+
|
|
21
|
+
containerd = External::Containerd.new(@ssh)
|
|
22
|
+
|
|
23
|
+
# List all images
|
|
24
|
+
all_tags = containerd.list_images("#{prefix}:*")
|
|
25
|
+
|
|
26
|
+
# Sort by tag (timestamp), keep newest
|
|
27
|
+
sorted_tags = all_tags.sort.reverse
|
|
28
|
+
keep_tags = sorted_tags.take(keep_count)
|
|
29
|
+
|
|
30
|
+
# Make sure current tag is kept
|
|
31
|
+
keep_tags << current_tag unless keep_tags.include?(current_tag)
|
|
32
|
+
keep_tags << "latest"
|
|
33
|
+
|
|
34
|
+
containerd.cleanup_old_images(prefix, keep_tags.uniq)
|
|
35
|
+
|
|
36
|
+
@log.success "Old images cleaned up"
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Nvoi
|
|
4
|
+
class Cli
|
|
5
|
+
module Deploy
|
|
6
|
+
module Steps
|
|
7
|
+
# ConfigureTunnel handles Cloudflare tunnel setup for services
|
|
8
|
+
class ConfigureTunnel
|
|
9
|
+
def initialize(config, log)
|
|
10
|
+
@config = config
|
|
11
|
+
@log = log
|
|
12
|
+
|
|
13
|
+
cf = config.cloudflare
|
|
14
|
+
@cf_client = External::Dns::Cloudflare.new(cf.api_token, cf.account_id)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def run
|
|
18
|
+
@log.info "Configuring Cloudflare tunnels"
|
|
19
|
+
|
|
20
|
+
tunnels = []
|
|
21
|
+
|
|
22
|
+
@config.deploy.application.app.each do |service_name, service_config|
|
|
23
|
+
next unless service_config.domain && !service_config.domain.empty?
|
|
24
|
+
next unless service_config.port && service_config.port.positive?
|
|
25
|
+
|
|
26
|
+
tunnel_info = configure_service_tunnel(service_name, service_config)
|
|
27
|
+
tunnels << tunnel_info
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
@log.success "All tunnels configured (%d)", tunnels.size
|
|
31
|
+
tunnels
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
def configure_service_tunnel(service_name, service_config)
|
|
37
|
+
tunnel_name = @config.namer.tunnel_name(service_name)
|
|
38
|
+
hostnames = Utils::Namer.build_hostnames(service_config.subdomain, service_config.domain)
|
|
39
|
+
primary_hostname = hostnames.first
|
|
40
|
+
|
|
41
|
+
# Service URL points to the NGINX Ingress Controller
|
|
42
|
+
service_url = "http://ingress-nginx-controller.ingress-nginx.svc.cluster.local:80"
|
|
43
|
+
|
|
44
|
+
tunnel = setup_tunnel(tunnel_name, hostnames, service_url, service_config.domain)
|
|
45
|
+
|
|
46
|
+
Objects::Tunnel::Info.new(
|
|
47
|
+
service_name:,
|
|
48
|
+
hostname: primary_hostname,
|
|
49
|
+
tunnel_id: tunnel.tunnel_id,
|
|
50
|
+
tunnel_token: tunnel.tunnel_token
|
|
51
|
+
)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def setup_tunnel(tunnel_name, hostnames, service_url, domain)
|
|
55
|
+
@log.info "Setting up tunnel: %s -> %s", tunnel_name, hostnames.join(", ")
|
|
56
|
+
|
|
57
|
+
# Find or create tunnel
|
|
58
|
+
tunnel = @cf_client.find_tunnel(tunnel_name)
|
|
59
|
+
|
|
60
|
+
if tunnel
|
|
61
|
+
@log.info "Using existing tunnel: %s", tunnel_name
|
|
62
|
+
else
|
|
63
|
+
@log.info "Creating new tunnel: %s", tunnel_name
|
|
64
|
+
tunnel = @cf_client.create_tunnel(tunnel_name)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Get tunnel token
|
|
68
|
+
token = tunnel.token
|
|
69
|
+
if token.nil? || token.empty?
|
|
70
|
+
token = @cf_client.get_tunnel_token(tunnel.id)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Configure tunnel ingress for all hostnames
|
|
74
|
+
@log.info "Configuring tunnel ingress: %s -> %s", hostnames.join(", "), service_url
|
|
75
|
+
@cf_client.update_tunnel_configuration(tunnel.id, hostnames, service_url)
|
|
76
|
+
|
|
77
|
+
# Verify configuration propagated
|
|
78
|
+
@log.info "Verifying tunnel configuration..."
|
|
79
|
+
@cf_client.verify_tunnel_configuration(tunnel.id, hostnames, service_url, Utils::Constants::TUNNEL_CONFIG_VERIFY_ATTEMPTS)
|
|
80
|
+
|
|
81
|
+
# Create DNS records for all hostnames
|
|
82
|
+
zone = @cf_client.find_zone(domain)
|
|
83
|
+
raise Errors::CloudflareError, "zone not found: #{domain}" unless zone
|
|
84
|
+
|
|
85
|
+
tunnel_cname = "#{tunnel.id}.cfargotunnel.com"
|
|
86
|
+
hostnames.each do |hostname|
|
|
87
|
+
@log.info "Creating DNS CNAME record: %s", hostname
|
|
88
|
+
@cf_client.create_or_update_dns_record(zone.id, hostname, "CNAME", tunnel_cname, proxied: true)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
@log.success "Tunnel configured: %s", tunnel_name
|
|
92
|
+
|
|
93
|
+
Objects::Tunnel::Info.new(
|
|
94
|
+
tunnel_id: tunnel.id,
|
|
95
|
+
tunnel_token: token
|
|
96
|
+
)
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|