nvoi 0.1.7 → 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/todos/buckets.md +41 -0
- data/Gemfile.lock +1 -1
- 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/deploy_service.rb +3 -13
- data/lib/nvoi/external/containerd.rb +1 -48
- data/lib/nvoi/external/ssh.rb +0 -12
- data/lib/nvoi/external/ssh_tunnel.rb +100 -0
- data/lib/nvoi/version.rb +1 -1
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 29c741e5b25ff6ab371cd67fa720bbfbe45d2edbb3f59cf060cbe22aadde790e
|
|
4
|
+
data.tar.gz: dd4af8ac050b8ce0d95ed5a55a69f0e778ad015074df9860e33dce878f026334
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7764ee8e3a9bb39b3059798c49f2e8c8c42c9cd15e69ff0187d7bea84f6b8eea4e36c2d3d4d18db0ba90e3fdf52f4214981311a255e188d84c3c36cbdc6ea3aa
|
|
7
|
+
data.tar.gz: 1b4d265fe116bb22967fcdba974cc0aea8d29fffb98f4ed41acc003eba4cd2a8de07218d04eb034e8ee1c37b06831c8334db88c63ba323ee0ef25b898148057e
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Scaleway Bucket Automation
|
|
2
|
+
|
|
3
|
+
Automate bucket provisioning for new tenants/environments.
|
|
4
|
+
|
|
5
|
+
## API
|
|
6
|
+
|
|
7
|
+
No separate management API - use S3-compatible REST API directly via `aws-sdk-s3` gem.
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
require "aws-sdk-s3"
|
|
11
|
+
|
|
12
|
+
client = Aws::S3::Client.new(
|
|
13
|
+
region: "fr-par",
|
|
14
|
+
endpoint: "https://s3.fr-par.scw.cloud",
|
|
15
|
+
access_key_id: Rails.application.credentials.dig(:scaleway, :access_key_id),
|
|
16
|
+
secret_access_key: Rails.application.credentials.dig(:scaleway, :secret_key)
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
# Create bucket
|
|
20
|
+
client.create_bucket(bucket: "tenant-#{tenant_slug}-#{Rails.env}")
|
|
21
|
+
|
|
22
|
+
# Set CORS
|
|
23
|
+
client.put_bucket_cors(
|
|
24
|
+
bucket: bucket_name,
|
|
25
|
+
cors_configuration: {
|
|
26
|
+
cors_rules: [{
|
|
27
|
+
allowed_origins: ["https://notiplus.com", "https://*.notiplus.com"],
|
|
28
|
+
allowed_methods: ["GET", "PUT"],
|
|
29
|
+
allowed_headers: ["*"],
|
|
30
|
+
max_age_seconds: 3000
|
|
31
|
+
}]
|
|
32
|
+
}
|
|
33
|
+
)
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Tasks
|
|
37
|
+
|
|
38
|
+
- [ ] Create `Scaleway::BucketService` to handle create/configure
|
|
39
|
+
- [ ] Add rake task for provisioning new environment buckets
|
|
40
|
+
- [ ] Hook into tenant creation flow (if multi-tenant)
|
|
41
|
+
- [ ] Add lifecycle rules for old/temp files cleanup
|
data/Gemfile.lock
CHANGED
|
@@ -123,24 +123,40 @@ module Nvoi
|
|
|
123
123
|
require_relative "steps/cleanup_images"
|
|
124
124
|
|
|
125
125
|
ssh = External::Ssh.new(server_ip, @config.ssh_key_path)
|
|
126
|
+
registry_port = Utils::Constants::REGISTRY_PORT
|
|
126
127
|
|
|
127
|
-
#
|
|
128
|
-
|
|
128
|
+
# Start SSH tunnel to registry
|
|
129
|
+
registry_tunnel = External::SshTunnel.new(
|
|
130
|
+
ip: server_ip,
|
|
131
|
+
user: "deploy",
|
|
132
|
+
key_path: @config.ssh_key_path,
|
|
133
|
+
local_port: registry_port,
|
|
134
|
+
remote_port: registry_port
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
registry_tunnel.start
|
|
129
138
|
|
|
130
139
|
begin
|
|
131
|
-
#
|
|
132
|
-
|
|
133
|
-
|
|
140
|
+
# Acquire deployment lock
|
|
141
|
+
acquire_lock(ssh)
|
|
142
|
+
|
|
143
|
+
begin
|
|
144
|
+
# Build and push image via tunnel
|
|
145
|
+
timestamp = Time.now.strftime("%Y%m%d%H%M%S")
|
|
146
|
+
image_tag = @config.namer.image_tag(timestamp)
|
|
134
147
|
|
|
135
|
-
|
|
148
|
+
registry_tag = Steps::BuildImage.new(@config, @log).run(working_dir, image_tag)
|
|
136
149
|
|
|
137
|
-
|
|
138
|
-
|
|
150
|
+
# Deploy all services (image already in registry)
|
|
151
|
+
Steps::DeployService.new(@config, ssh, tunnels, @log).run(registry_tag, timestamp)
|
|
139
152
|
|
|
140
|
-
|
|
141
|
-
|
|
153
|
+
# Cleanup old images
|
|
154
|
+
Steps::CleanupImages.new(@config, ssh, @log).run(timestamp)
|
|
155
|
+
ensure
|
|
156
|
+
release_lock(ssh)
|
|
157
|
+
end
|
|
142
158
|
ensure
|
|
143
|
-
|
|
159
|
+
registry_tunnel.stop
|
|
144
160
|
end
|
|
145
161
|
end
|
|
146
162
|
|
|
@@ -4,22 +4,64 @@ module Nvoi
|
|
|
4
4
|
class Cli
|
|
5
5
|
module Deploy
|
|
6
6
|
module Steps
|
|
7
|
-
# BuildImage handles Docker image building and pushing to
|
|
7
|
+
# BuildImage handles Docker image building and pushing to registry
|
|
8
8
|
class BuildImage
|
|
9
|
-
def initialize(config,
|
|
9
|
+
def initialize(config, log)
|
|
10
10
|
@config = config
|
|
11
|
-
@ssh = ssh
|
|
12
11
|
@log = log
|
|
13
12
|
end
|
|
14
13
|
|
|
14
|
+
# Build locally and push to registry via SSH tunnel
|
|
15
|
+
# Returns the registry tag for use in k8s deployments
|
|
15
16
|
def run(working_dir, image_tag)
|
|
16
17
|
@log.info "Building Docker image: %s", image_tag
|
|
17
18
|
|
|
18
|
-
|
|
19
|
-
|
|
19
|
+
build_image(working_dir, image_tag)
|
|
20
|
+
registry_tag = push_to_registry(image_tag)
|
|
20
21
|
|
|
21
|
-
@log.success "Image built: %s",
|
|
22
|
+
@log.success "Image built and pushed: %s", registry_tag
|
|
23
|
+
registry_tag
|
|
22
24
|
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def build_image(working_dir, tag)
|
|
29
|
+
cache_from = @config.namer.latest_image_tag
|
|
30
|
+
cache_args = "--cache-from #{cache_from}"
|
|
31
|
+
|
|
32
|
+
build_cmd = [
|
|
33
|
+
"cd #{working_dir} &&",
|
|
34
|
+
"DOCKER_BUILDKIT=1 docker build",
|
|
35
|
+
"--platform linux/amd64",
|
|
36
|
+
cache_args,
|
|
37
|
+
"--build-arg BUILDKIT_INLINE_CACHE=1",
|
|
38
|
+
"-t #{tag} ."
|
|
39
|
+
].join(" ")
|
|
40
|
+
|
|
41
|
+
unless system("bash", "-c", build_cmd)
|
|
42
|
+
raise Errors::SshError, "docker build failed"
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Tag as :latest for next build's cache
|
|
46
|
+
system("docker", "tag", tag, cache_from)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def push_to_registry(tag)
|
|
50
|
+
registry_port = Utils::Constants::REGISTRY_PORT
|
|
51
|
+
registry_tag = "localhost:#{registry_port}/#{@config.container_prefix}:#{tag.split(':').last}"
|
|
52
|
+
|
|
53
|
+
@log.info "Tagging for registry: %s", registry_tag
|
|
54
|
+
unless system("docker", "tag", tag, registry_tag)
|
|
55
|
+
raise Errors::SshError, "docker tag failed"
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
@log.info "Pushing to registry via SSH tunnel..."
|
|
59
|
+
unless system("docker", "push", registry_tag)
|
|
60
|
+
raise Errors::SshError, "docker push failed - is the SSH tunnel active?"
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
registry_tag
|
|
64
|
+
end
|
|
23
65
|
end
|
|
24
66
|
end
|
|
25
67
|
end
|
|
@@ -22,10 +22,9 @@ module Nvoi
|
|
|
22
22
|
@kubectl = External::Kubectl.new(ssh)
|
|
23
23
|
end
|
|
24
24
|
|
|
25
|
-
def run(
|
|
26
|
-
#
|
|
27
|
-
|
|
28
|
-
push_to_registry(image_tag, registry_tag)
|
|
25
|
+
def run(registry_tag, timestamp)
|
|
26
|
+
# Image is already in registry (pushed via SSH tunnel in BuildImage step)
|
|
27
|
+
@log.info "Using image from registry: %s", registry_tag
|
|
29
28
|
|
|
30
29
|
# Gather env vars
|
|
31
30
|
first_service = @config.deploy.application.app.keys.first
|
|
@@ -65,15 +64,6 @@ module Nvoi
|
|
|
65
64
|
|
|
66
65
|
private
|
|
67
66
|
|
|
68
|
-
def push_to_registry(local_tag, registry_tag)
|
|
69
|
-
@log.info "Pushing to in-cluster registry: %s", registry_tag
|
|
70
|
-
|
|
71
|
-
@ssh.execute("sudo ctr -n k8s.io images tag #{local_tag} #{registry_tag}")
|
|
72
|
-
@ssh.execute("sudo ctr -n k8s.io images push --plain-http #{registry_tag}")
|
|
73
|
-
|
|
74
|
-
@log.success "Image pushed to registry"
|
|
75
|
-
end
|
|
76
|
-
|
|
77
67
|
def deploy_app_secret(env_vars)
|
|
78
68
|
secret_name = @namer.app_secret_name
|
|
79
69
|
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
module Nvoi
|
|
4
4
|
module External
|
|
5
5
|
# Containerd manages container operations on remote servers via containerd/ctr
|
|
6
|
+
# Used for image listing and cleanup on the remote server
|
|
6
7
|
class Containerd
|
|
7
8
|
attr_reader :ssh
|
|
8
9
|
|
|
@@ -10,54 +11,6 @@ module Nvoi
|
|
|
10
11
|
@ssh = ssh
|
|
11
12
|
end
|
|
12
13
|
|
|
13
|
-
# Build image locally, save to tar, rsync to remote, load with containerd
|
|
14
|
-
def build_and_deploy_image(path, tag, cache_from: nil)
|
|
15
|
-
cache_args = cache_from ? "--cache-from #{cache_from}" : ""
|
|
16
|
-
local_build_cmd = "cd #{path} && DOCKER_BUILDKIT=1 docker build --platform linux/amd64 #{cache_args} --build-arg BUILDKIT_INLINE_CACHE=1 -t #{tag} ."
|
|
17
|
-
|
|
18
|
-
unless system("bash", "-c", local_build_cmd)
|
|
19
|
-
raise Errors::SshError, "local build failed"
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
# Tag as :latest for next build's cache
|
|
23
|
-
system("docker", "tag", tag, cache_from) if cache_from
|
|
24
|
-
|
|
25
|
-
tar_file = "/tmp/#{tag.tr(':', '_')}.tar"
|
|
26
|
-
unless system("docker", "save", tag, "-o", tar_file)
|
|
27
|
-
raise Errors::SshError, "docker save failed"
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
begin
|
|
31
|
-
remote_tar_path = "/tmp/#{tag.tr(':', '_')}.tar"
|
|
32
|
-
rsync_cmd = [
|
|
33
|
-
"rsync", "-avz",
|
|
34
|
-
"-e", "ssh -i #{@ssh.ssh_key} -o StrictHostKeyChecking=no",
|
|
35
|
-
tar_file,
|
|
36
|
-
"#{@ssh.user}@#{@ssh.ip}:#{remote_tar_path}"
|
|
37
|
-
]
|
|
38
|
-
|
|
39
|
-
unless system(*rsync_cmd)
|
|
40
|
-
raise Errors::SshError, "rsync failed"
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
Nvoi.logger.info "Importing image into containerd..."
|
|
44
|
-
@ssh.execute("sudo ctr -n k8s.io images import #{remote_tar_path}")
|
|
45
|
-
|
|
46
|
-
full_image_ref = "docker.io/library/#{tag}"
|
|
47
|
-
|
|
48
|
-
begin
|
|
49
|
-
@ssh.execute("sudo ctr -n k8s.io images tag #{full_image_ref} #{tag}")
|
|
50
|
-
rescue Errors::SshCommandError => e
|
|
51
|
-
list_output = @ssh.execute("sudo ctr -n k8s.io images ls") rescue ""
|
|
52
|
-
raise Errors::SshError, "failed to tag imported image: #{e.message}\nAvailable images:\n#{list_output}"
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
@ssh.execute_ignore_errors("rm #{remote_tar_path}")
|
|
56
|
-
ensure
|
|
57
|
-
File.delete(tar_file) if File.exist?(tar_file)
|
|
58
|
-
end
|
|
59
|
-
end
|
|
60
|
-
|
|
61
14
|
def list_images(filter)
|
|
62
15
|
output = @ssh.execute("sudo ctr -n k8s.io images ls -q | grep '#{filter}' | sort -r")
|
|
63
16
|
return [] if output.empty?
|
data/lib/nvoi/external/ssh.rb
CHANGED
|
@@ -62,18 +62,6 @@ module Nvoi
|
|
|
62
62
|
raise Errors::SshCommandError, "SCP download failed: #{output}" unless status.success?
|
|
63
63
|
end
|
|
64
64
|
|
|
65
|
-
def rsync(local_path, remote_path)
|
|
66
|
-
rsync_args = [
|
|
67
|
-
"-avz",
|
|
68
|
-
"-e", "ssh #{build_ssh_args.join(' ')}",
|
|
69
|
-
local_path,
|
|
70
|
-
"#{@user}@#{@ip}:#{remote_path}"
|
|
71
|
-
]
|
|
72
|
-
|
|
73
|
-
output, status = Open3.capture2e("rsync", *rsync_args)
|
|
74
|
-
raise Errors::SshCommandError, "rsync failed: #{output}" unless status.success?
|
|
75
|
-
end
|
|
76
|
-
|
|
77
65
|
private
|
|
78
66
|
|
|
79
67
|
def build_ssh_args
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "net/ssh"
|
|
4
|
+
|
|
5
|
+
module Nvoi
|
|
6
|
+
module External
|
|
7
|
+
# SshTunnel manages SSH port forwarding using net-ssh
|
|
8
|
+
class SshTunnel
|
|
9
|
+
attr_reader :local_port, :remote_port
|
|
10
|
+
|
|
11
|
+
def initialize(ip:, user:, key_path:, local_port:, remote_port:)
|
|
12
|
+
@ip = ip
|
|
13
|
+
@user = user
|
|
14
|
+
@key_path = key_path
|
|
15
|
+
@local_port = local_port
|
|
16
|
+
@remote_port = remote_port
|
|
17
|
+
@session = nil
|
|
18
|
+
@thread = nil
|
|
19
|
+
@running = false
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def start
|
|
23
|
+
Nvoi.logger.info "Starting SSH tunnel: localhost:%d -> %s:%d", @local_port, @ip, @remote_port
|
|
24
|
+
|
|
25
|
+
@session = Net::SSH.start(
|
|
26
|
+
@ip,
|
|
27
|
+
@user,
|
|
28
|
+
keys: [@key_path],
|
|
29
|
+
non_interactive: true,
|
|
30
|
+
verify_host_key: :never
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
@session.forward.local(@local_port, "localhost", @remote_port)
|
|
34
|
+
@running = true
|
|
35
|
+
|
|
36
|
+
@thread = Thread.new do
|
|
37
|
+
Thread.current.report_on_exception = false
|
|
38
|
+
@session.loop { @running }
|
|
39
|
+
rescue IOError, Net::SSH::Disconnect, Errno::EBADF
|
|
40
|
+
# Session closed during shutdown, exit gracefully
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Wait for tunnel to establish
|
|
44
|
+
sleep 0.3
|
|
45
|
+
verify_tunnel!
|
|
46
|
+
|
|
47
|
+
Nvoi.logger.success "SSH tunnel established"
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def stop
|
|
51
|
+
return unless @running
|
|
52
|
+
|
|
53
|
+
Nvoi.logger.info "Stopping SSH tunnel"
|
|
54
|
+
@running = false
|
|
55
|
+
|
|
56
|
+
# Give the event loop a moment to see @running = false
|
|
57
|
+
sleep 0.1
|
|
58
|
+
|
|
59
|
+
begin
|
|
60
|
+
@session&.forward&.cancel_local(@local_port)
|
|
61
|
+
rescue StandardError
|
|
62
|
+
# Ignore errors during cleanup
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
begin
|
|
66
|
+
@session&.close
|
|
67
|
+
rescue StandardError
|
|
68
|
+
# Ignore errors during cleanup
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Wait for thread to exit gracefully
|
|
72
|
+
@thread&.join(1)
|
|
73
|
+
|
|
74
|
+
@session = nil
|
|
75
|
+
@thread = nil
|
|
76
|
+
|
|
77
|
+
Nvoi.logger.success "SSH tunnel closed"
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def alive?
|
|
81
|
+
@running && @thread&.alive? && @session && !@session.closed?
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
private
|
|
85
|
+
|
|
86
|
+
def verify_tunnel!
|
|
87
|
+
unless alive?
|
|
88
|
+
raise Errors::SshError, "SSH tunnel failed to start"
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Verify the port is actually listening
|
|
92
|
+
require "socket"
|
|
93
|
+
socket = TCPSocket.new("localhost", @local_port)
|
|
94
|
+
socket.close
|
|
95
|
+
rescue Errno::ECONNREFUSED
|
|
96
|
+
raise Errors::SshError, "SSH tunnel started but port #{@local_port} not accessible"
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
data/lib/nvoi/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: nvoi
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.8
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- NVOI
|
|
@@ -204,6 +204,7 @@ files:
|
|
|
204
204
|
- ".claude/todo/scaleway.impl.md"
|
|
205
205
|
- ".claude/todo/scaleway.reference.md"
|
|
206
206
|
- ".claude/todos.md"
|
|
207
|
+
- ".claude/todos/buckets.md"
|
|
207
208
|
- ".rubocop.yml"
|
|
208
209
|
- Gemfile
|
|
209
210
|
- Gemfile.lock
|
|
@@ -384,6 +385,7 @@ files:
|
|
|
384
385
|
- lib/nvoi/external/dns/cloudflare.rb
|
|
385
386
|
- lib/nvoi/external/kubectl.rb
|
|
386
387
|
- lib/nvoi/external/ssh.rb
|
|
388
|
+
- lib/nvoi/external/ssh_tunnel.rb
|
|
387
389
|
- lib/nvoi/objects/config_override.rb
|
|
388
390
|
- lib/nvoi/objects/configuration.rb
|
|
389
391
|
- lib/nvoi/objects/database.rb
|