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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 91c0d6ae24cfc163dd6c00008e5199cfdca4b9864ed6c2b56644b58b90d04ca6
4
- data.tar.gz: 319109891843c14ae0dfe4c66553bdf06a5bf5a1d009855f97f0c6ffc17bccc6
3
+ metadata.gz: 29c741e5b25ff6ab371cd67fa720bbfbe45d2edbb3f59cf060cbe22aadde790e
4
+ data.tar.gz: dd4af8ac050b8ce0d95ed5a55a69f0e778ad015074df9860e33dce878f026334
5
5
  SHA512:
6
- metadata.gz: 34b0c1e73b479bb2e10356b27f397a7c34dc9476bd7244490450163736fc7106bd28143ec91bfba1aa27a16c5b1f15d722cfd09d98d7a4155ce7dd752ff30949
7
- data.tar.gz: 9706c433e608299ac10f71ff257a9d73a0e4f97bab126d3b99d0050474c426dde79fb793204dc55d9369b0654bfba162621106c34d3791fb4e9fb7fed651d8e2
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
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- nvoi (0.1.7)
4
+ nvoi (0.1.8)
5
5
  aws-sdk-ec2 (~> 1.400)
6
6
  faraday (~> 2.7)
7
7
  net-scp (~> 4.0)
@@ -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
- # Acquire deployment lock
128
- acquire_lock(ssh)
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
- # Build and push image
132
- timestamp = Time.now.strftime("%Y%m%d%H%M%S")
133
- image_tag = @config.namer.image_tag(timestamp)
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
- Steps::BuildImage.new(@config, ssh, @log).run(working_dir, image_tag)
148
+ registry_tag = Steps::BuildImage.new(@config, @log).run(working_dir, image_tag)
136
149
 
137
- # Deploy all services
138
- Steps::DeployService.new(@config, ssh, tunnels, @log).run(image_tag, timestamp)
150
+ # Deploy all services (image already in registry)
151
+ Steps::DeployService.new(@config, ssh, tunnels, @log).run(registry_tag, timestamp)
139
152
 
140
- # Cleanup old images
141
- Steps::CleanupImages.new(@config, ssh, @log).run(timestamp)
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
- release_lock(ssh)
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 cluster
7
+ # BuildImage handles Docker image building and pushing to registry
8
8
  class BuildImage
9
- def initialize(config, ssh, log)
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
- containerd = External::Containerd.new(@ssh)
19
- containerd.build_and_deploy_image(working_dir, image_tag, cache_from: @config.namer.latest_image_tag)
19
+ build_image(working_dir, image_tag)
20
+ registry_tag = push_to_registry(image_tag)
20
21
 
21
- @log.success "Image built: %s", image_tag
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(image_tag, timestamp)
26
- # Push to in-cluster registry
27
- registry_tag = "localhost:#{Utils::Constants::REGISTRY_PORT}/#{@config.container_prefix}:#{timestamp}"
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?
@@ -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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Nvoi
4
- VERSION = "0.1.7"
4
+ VERSION = "0.1.8"
5
5
  end
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.7
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