Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions Rakefile
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,10 @@ task :cfg => :bash do
sh 'install -m 755 ruby/bin/cfg "$HOME/bin/cfg"'
end

task :sandbox => :cfg do
sh 'install -m 755 ruby/bin/sandbox "$HOME/bin/sandbox"'
end

task :test_cfg do
begin
require 'rspec/core/rake_task'
Expand All @@ -325,3 +329,21 @@ task :test_cfg do
end
Rake::Task[:_run_cfg_specs].invoke
end

task :test_sandbox do
begin
require 'rspec/core/rake_task'
rescue LoadError
puts "Warning: rspec not installed, skipping sandbox tests"
next
end
test_files = Dir.glob("ruby/lib/sandbox/spec/*_spec.rb")
if test_files.empty?
puts "No sandbox spec files found"
next
end
RSpec::Core::RakeTask.new(:_run_sandbox_specs) do |t|
t.pattern = test_files
end
Rake::Task[:_run_sandbox_specs].invoke
end
6 changes: 6 additions & 0 deletions ruby/bin/sandbox
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#!/usr/bin/env ruby
# frozen_string_literal: true

$LOAD_PATH.unshift("#{Dir.home}/.local/lib/dotfiles")
require 'sandbox'
Sandbox::CLI.run(ARGV)
33 changes: 33 additions & 0 deletions ruby/lib/sandbox.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# frozen_string_literal: true

module Sandbox
class Error < StandardError; end
class BackendError < Error; end
class BackendConflictError < Error; end
class BackendNotRunningError < Error; end
class MissingBackendError < Error; end
class CommandError < Error
attr_reader :status, :stdout, :stderr

def initialize(message, status:, stdout: nil, stderr: nil)
super(message)
@status = status
@stdout = stdout
@stderr = stderr
end
end
end

require_relative 'sandbox/name'
require_relative 'sandbox/connection_info'
require_relative 'sandbox/spinner'
require_relative 'sandbox/command_runner'
require_relative 'sandbox/bash_runner'
require_relative 'sandbox/backend'
require_relative 'sandbox/ssh_config'
require_relative 'sandbox/backend_selector'
require_relative 'sandbox/backends/container'
require_relative 'sandbox/backends/kvm'
require_relative 'sandbox/backends/hcloud'
require_relative 'sandbox/backends/proxy'
require_relative 'sandbox/cli'
21 changes: 21 additions & 0 deletions ruby/lib/sandbox/backend.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# frozen_string_literal: true

module Sandbox
module Backend
REQUIRED_METHODS = %i[
backend_start
backend_stop
backend_enter
backend_is_running
backend_get_ssh_port
backend_get_ip
].freeze

def self.ensure!(backend)
missing = REQUIRED_METHODS.reject { |method| backend.respond_to?(method) }
return if missing.empty?

raise Sandbox::BackendError, "Backend #{backend.class} missing methods: #{missing.join(', ')}"
end
end
end
26 changes: 26 additions & 0 deletions ruby/lib/sandbox/backend_selector.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# frozen_string_literal: true

module Sandbox
class BackendSelector
def initialize(backends:, sandbox_name:)
@backends = backends
@sandbox_name = sandbox_name
end

def detect_running_backend
running = @backends.select { |_name, backend| backend.backend_is_running(@sandbox_name) }
return nil if running.empty?
if running.size > 1
raise Sandbox::BackendConflictError, 'ERROR: Multiple backends running'
end

running.keys.first
end

def select_backend(explicit_backend)
return explicit_backend if explicit_backend

detect_running_backend || 'container'
end
end
end
61 changes: 61 additions & 0 deletions ruby/lib/sandbox/backends/container.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# frozen_string_literal: true

module Sandbox
module Backends
class Container
attr_writer :proxy_enabled

def initialize(bash_runner:, command_runner:, proxy: false)
@bash_runner = bash_runner
@command_runner = command_runner
@proxy = proxy
end

def backend_start(name, use_pty: false)
@bash_runner.call('start_container_sandbox', name, env: proxy_env, use_pty: use_pty)
end

def backend_stop(name)
@bash_runner.call('stop_container_sandbox', name, env: proxy_env)
end

def backend_enter(name)
engine = @bash_runner.call('detect_container_engine', capture: true).strip
raise Sandbox::BackendError, 'Error: Neither podman nor docker found' if engine.empty?

cmd = [engine, 'exec', '-it', '-e', "TERM=#{ENV.fetch('TERM', 'xterm-256color')}", '-u', 'dev',
'-w', '/home/dev/workspace', name, 'zsh']
@command_runner.exec(cmd)
end

def backend_is_running(name)
@bash_runner.call('is_container_running', name)
true
rescue Sandbox::CommandError
false
end

def backend_get_ssh_port(name)
@bash_runner.call('get_container_ssh_port', name, capture: true).strip
end

def backend_get_ip(_name)
'localhost'
end

def list
@bash_runner.call('list_container_sandboxes')
end

def backend_stop_all
@bash_runner.call('stop_all_container_sandboxes')
end

private

def proxy_env
(@proxy || @proxy_enabled) ? { 'PROXY' => 'true' } : {}
end
end
end
end
80 changes: 80 additions & 0 deletions ruby/lib/sandbox/backends/hcloud.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# frozen_string_literal: true

module Sandbox
module Backends
class Hcloud
def initialize(bash_runner:, command_runner:, sync: false)
@bash_runner = bash_runner
@command_runner = command_runner
@sync = sync
end

def backend_start(name, use_pty: false)
env = {}
env['SYNC'] = 'true' if @sync
@bash_runner.call('start_hcloud_sandbox', name, env: env, use_pty: use_pty)
end

def backend_stop(name)
@bash_runner.call('stop_hcloud_sandbox', name)
end

def backend_enter(name)
if ssh_alias_setup?(name)
@command_runner.exec(['ssh', name])
else
ip = server_ip(name)
raise Sandbox::BackendError, 'Error: Could not determine server IP' if ip.nil? || ip.empty?

cmd = ['ssh', '-o', 'UserKnownHostsFile=/dev/null', '-o', 'StrictHostKeyChecking=no', "dev@#{ip}"]
@command_runner.exec(cmd)
end
end

def backend_is_running(name)
@bash_runner.call('is_hcloud_running', name)
true
rescue Sandbox::CommandError
false
end

def backend_get_ssh_port(name)
@bash_runner.call('get_hcloud_ssh_port', name, capture: true).strip
end

def backend_get_ip(name)
@bash_runner.call('get_hcloud_ip', name, capture: true).strip
end

def list
@bash_runner.call('list_hcloud_sandboxes')
end

def backend_stop_all
@bash_runner.call('stop_all_hcloud_sandboxes')
end

def state_dir(name)
@bash_runner.call('hcloud_state_dir', name, capture: true).strip
end

private

def ssh_alias_setup?(name)
@bash_runner.call('is_ssh_alias_setup', name)
true
rescue Sandbox::CommandError
false
end

def server_ip(name)
ip = backend_get_ip(name)
return ip unless ip.empty?

state_path = state_dir(name)
ip_file = File.join(state_path, 'server.ip')
File.exist?(ip_file) ? File.read(ip_file).strip : ''
end
end
end
end
65 changes: 65 additions & 0 deletions ruby/lib/sandbox/backends/kvm.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# frozen_string_literal: true

module Sandbox
module Backends
class Kvm
attr_writer :proxy_enabled

def initialize(bash_runner:, command_runner:, proxy: false)
@bash_runner = bash_runner
@command_runner = command_runner
@proxy = proxy
end

def backend_start(name, use_pty: false)
@bash_runner.call('start_kvm_sandbox', name, env: proxy_env, use_pty: use_pty)
end

def backend_stop(name)
@bash_runner.call('stop_kvm_sandbox', name, env: proxy_env)
end

def backend_enter(name)
port = backend_get_ssh_port(name)
raise Sandbox::BackendError, 'Error: Could not determine SSH port' if port.empty?

cmd = ['ssh', '-o', 'UserKnownHostsFile=/dev/null', '-o', 'StrictHostKeyChecking=no',
'-p', port, 'dev@localhost']
@command_runner.exec(cmd)
end

def backend_is_running(name)
@bash_runner.call('is_kvm_running', name)
true
rescue Sandbox::CommandError
false
end

def backend_get_ssh_port(name)
@bash_runner.call('get_kvm_ssh_port', name, capture: true).strip
end

def backend_get_ip(name)
@bash_runner.call('get_kvm_ip', name, capture: true).strip
end

def list
@bash_runner.call('list_kvm_sandboxes')
end

def backend_stop_all
@bash_runner.call('stop_all_kvm_sandboxes')
end

def console_socket(name)
@bash_runner.call('kvm_console_socket', name, capture: true).strip
end

private

def proxy_env
(@proxy || @proxy_enabled) ? { 'PROXY' => 'true' } : {}
end
end
end
end
62 changes: 62 additions & 0 deletions ruby/lib/sandbox/backends/proxy.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# frozen_string_literal: true

module Sandbox
module Backends
class Proxy
attr_reader :backend

def initialize(backend)
@backend = backend
end

def backend_start(name, use_pty: false)
with_proxy { @backend.backend_start(name, use_pty: use_pty) }
end

def backend_stop(name)
with_proxy { @backend.backend_stop(name) }
end

def backend_enter(name)
@backend.backend_enter(name)
end

def backend_is_running(name)
@backend.backend_is_running(name)
end

def backend_get_ssh_port(name)
@backend.backend_get_ssh_port(name)
end

def backend_get_ip(name)
@backend.backend_get_ip(name)
end

def list
@backend.list
end

def console_socket(name)
return @backend.console_socket(name) if @backend.respond_to?(:console_socket)

nil
end

private

def with_proxy
if @backend.respond_to?(:proxy_enabled=)
@backend.proxy_enabled = true
yield
else
yield
end
ensure
if @backend.respond_to?(:proxy_enabled=)
@backend.proxy_enabled = false
end
end
end
end
end
Loading