#!/usr/bin/env ruby require_relative "../lib/harbr" class HarbrCLI < Thor no_commands do def display_containers_table(containers) return puts "No containers available." if containers.empty? # Define headers based on Container attributes headers = ["Name", "Host Header", "IP", "Port"] rows = containers.map do |container| [container.name, container.host_header, container.ip.nil? ? "127.0.0.1" : container.ip, container.port] end table = ::Terminal::Table.new(headings: headers, rows: rows) puts "" puts "Harbr Containers" puts "" puts table puts "" end def check_and_create_directory(path) unless Dir.exist?(path) puts "Creating directory: #{path}" FileUtils.mkdir_p(path) end end def command_exists?(command) system("command -v #{command} > /dev/null 2>&1") end def write_to_file(path, contents) dirname = File.dirname(path) FileUtils.mkdir_p(dirname) unless File.directory?(dirname) File.write(path, contents) end def install_with_snap(package) puts "Installing #{package} using Snap..." system("sudo snap install #{package}") or raise "Failed to install #{package}" end def install_with_apt(package) puts "Installing #{package} using apt..." system("sudo apt install #{package}") or raise "Failed to install #{package}" end def scan_for_containers Dir.glob("/var/harbr/containers/*").each_with_object({}) do |container_path, hash| next unless File.directory?(container_path) container_name = File.basename(container_path) versions = Dir.glob("#{container_path}/versions/*").select { |path| File.directory?(path) } versions.each { |version_path| hash["#{container_name}/versions/#{File.basename(version_path)}"] = true } end end PLACED_FILE = "/var/harbr/.placed" def place(container, version) placed_containers = File.exist?(PLACED_FILE) ? File.read(PLACED_FILE).split("\n") : [] if placed_containers.include?("#{container}/#{version}") puts "Container '#{container}', Version '#{version}' is already placed." else puts "Placing container: '#{container}', Version: '#{version}'" run_jobs(container, version) File.open(PLACED_FILE, "a") { |file| file.puts("#{container}/#{version}") } end end def check sleep_times = [1, 3, 5, 8, 23] begin result = yield if block_given? unless result sleep_times.each do |time| result = yield if block_given? break if result sleep(time) end end rescue => e puts "Error: #{e.message}" end end def run_jobs(container, version) puts "Running tasks for container: #{container}, version: #{version}" Harbr::Job.perform_async(container, version, "next") puts "deploy next version #{version} of #{container}" end def create_traefik_config(containers) `rm -rf "/etc/traefik/harbr.toml"` config = { "http" => { "routers" => { "traefik-dashboard" => { "rule" => "Host(`traefik.harbr.zero2one.ee`)", "service" => "api@internal" } }, "services" => {} } } containers.each do |container| container.ip = "127.0.0.1" config["http"]["routers"]["#{container.name}-router"] = { "rule" => "Host(`#{container.host_header}`)", "service" => "#{container.name}-service" } config["http"]["services"]["#{container.name}-service"] = { "loadBalancer" => { "servers" => [{"url" => "http://#{container.ip}:#{container.port}"}] } } end File.write("/etc/traefik/harbr.toml", TomlRB.dump(config)) puts "Traefik configuration written to /etc/traefik/harbr.toml" end end desc "destroy", "Destroy a container and remove all traces" def destroy(name) puts "Destroying container: #{name}" container_repo = Harbr::Container::Repository.new ["live.#{name}", "next.#{name}", name].each do |container_name| container_repo.get_by_name(container_name).each do |container| `port release #{container.port}` puts "released port #{container.port} successfully." container_repo.delete(container) end `rm -rf /etc/service/#{container_name}` `rm -rf /etc/sv/harbr/#{container_name}` puts "Container #{container_name} destroyed successfully." end `rm -rf /var/harbr/containers/#{name}` `rm -rf /var/log/harbr/#{name}` create_traefik_config(container_repo.all) end desc "logs", "Show logs" def logs exec "tail -f /var/log/harbr/current" end desc "peek CONTAINER", "Peek in a given container" method_option :live, type: :boolean, aliases: "-l", desc: "Process in live mode" method_option :next, type: :boolean, default: true, aliases: "-n", desc: "Process in next mode" def peek(container) container_repo = Harbr::Container::Repository.new container = container_repo.find_by_name(container) if container exec "tail -f /var/log/harbr/#{container.name}/live/current" if options[:live] exec "tail -f /var/log/harbr/#{container.name}/next/current" if options[:next] else puts "Container not recognized" end end desc "setup", "Set up Harbr environment" def setup # Check and create /var/harbr directory check_and_create_directory("/var/harbr/containers") # Check for Ruby, Traefik, and runit, and install if necessary install_with_snap("ruby") unless command_exists?("ruby") install_with_snap("traefik") unless command_exists?("traefik") install_with_apt("runit") unless command_exists?("runit") run_script = <<~SCRIPT #!/bin/sh exec 2>&1 harbr monitor SCRIPT log_script = <<~SCRIPT #!/bin/sh exec svlogd -tt /var/log/harbr/ SCRIPT write_to_file("/etc/sv/harbr/run", run_script) write_to_file("/etc/sv/harbr/log/run", log_script) `chmod +x /etc/sv/harbr/run` `chmod +x /etc/sv/harbr/log/run` `mkdir -p /var/log/harbr` `ln -sf /etc/sv/harbr /etc/service/harbr` `sv start harbr` puts "Harbr service started successfully." puts "Setup completed successfully." end desc "containers", "show all containers" def containers container_repo = Harbr::Container::Repository.new display_containers_table(container_repo.all) end desc "deploy", "deploy a container to production" def deploy(name) /versions\/(?\d*)/ =~ `ls -l /var/harbr/containers/#{name}/next` version = $1 raise "Ooops! next version not found!" if version.nil? Harbr::Job.perform_async(name, version, "live") /versions\/(?\d*)/ =~ `ls -l /var/harbr/containers/#{name}/live` `ln -sf /var/harbr/containers/#{name}/version/#{$1} /var/harbr/containers/#{name}/rollback` puts "deploy version #{version} of #{name} to live environment" end desc "rollback", "rollback last deploy" def rollback(name) Dir.chdir("/var/harbr/containers/#{name}") do if File.exist?("rollback") Dir.chdir("/var/harbr/containers/#{name}/") do /(?:. * -> (?:.*))/ =~ `ls -l /var/harbr/containers/#{name}/rollback` `rm -r live` `ln -sf #{$2} live` `sv restart live.#{name}` end puts "rollback successful" end end end desc "update", "update to the latest version of harbr" def update system "gem update harbr && sv stop harbr && sv start harbr" end desc "monitor", "Monitor /var/harbr/comtainers for new container versions" def monitor puts "version: (#{Harbr::VERSION})" puts "Monitoring /var/harbr/containers for new container versions..." last_known_state = {} loop do current_state = scan_for_containers new_versions = current_state.keys - last_known_state.keys new_versions.each do |container_version| container, version = container_version.split("/versions/") place(container, version) end last_known_state = current_state sleep 3 # Poll every 10 seconds end end end HarbrCLI.start(ARGV)