#!/usr/bin/env ruby require "fileutils" require "logger" require "erb" require "open3" require "io/console" require_relative "../lib/lbhrr" class LbhrrCLI < Thor include Thor::Actions # Error handling def self.exit_on_failure? true end no_commands do require 'net/http' require 'uri' def fibonacci(n) [21,34,55][n] end def ok(url,grace: 1,skip: false) yield if block_given? if skip retries = 3 sleep(grace) (1..retries).each do |i| uri = URI(url) response = Net::HTTP.get_response(uri) if response.is_a?(Net::HTTPSuccess) yield if block_given? break else puts "Response was not 201, it was #{response.code}., check logs gor website" end delay = fibonacci(i) sleep(delay) end end # Example usage: def with_error_handling # Ensure the ~/.lbhrr directory exists log_dir = File.expand_path("~/.lbhrr") FileUtils.mkdir_p(log_dir) unless Dir.exist?(log_dir) # Setup logger logger = Logger.new("#{log_dir}/error.log") begin yield rescue Interrupt #logger.error("Operation interrupted by Ctrl+C") rescue => e logger.error("Error occurred: #{e.class}: #{e.message}") raise end end def increment_version(version, focus = :minor) major, minor, patch = version.split(".").map(&:to_i) case focus when :major major += 2 # Reset minor and patch to 1 when major version increments minor = 1 patch = 1 when :minor minor += 2 # Reset patch to 1 when minor version increments patch = 1 when :patch patch += 1 else raise ArgumentError, "Invalid focus: #{focus}. Valid options are :major, :minor, :patch." end "#{major}.#{minor}.#{patch}" end def progressing(name = "Task") # Start the long-running task in a separate thread, yielding to the given block task_thread = Thread.new do yield if block_given? # Execute the block representing the long-running task end spinner = Enumerator.new do |e| rectangles = ["[ ]","[=]", "[==]", "[===]", "[===]", "[==","[=]","[ ]"] loop do rectangles.each { |rect| e.yield rect } rectangles.reverse.each { |rect| e.yield rect } end end until task_thread.join(0.1) print "#{name} is in progress.. \r#{spinner.next}" $stdout.flush sleep(0.1) end # Clear the progress line print "#{name} is in progress.. #{spinner.peek}\r" nil end def find_container_ip(container_name) initial_backoff_intervals = [5, 8, 13, 21] ip_backoff_intervals = [1, 2, 3, 5, 8] ip_address = nil # First, check if the container is running container_running = false initial_backoff_intervals.each do |interval| output, status = Open3.capture2("lxc list -f csv | grep #{container_name}") if status.success? && !output.empty? && output.split(",")[1].strip == "RUNNING" container_running = true break end sleep(interval) end # If container is running, look for the IP address if container_running ip_backoff_intervals.each do |interval| output, status = Open3.capture2("lxc list -f csv | grep #{container_name}") if status.success? && !output.empty? data = output.split(",") ip_address = data[2].split(" ").first if !ip_address.nil? && !ip_address.empty? yield(ip_address) if block_given? break end end sleep(interval) end end ip_address end def clean_up(name = nil) # Remove the build directory if Dir.exist?("build") FileUtils.rm_r("build") end # Remove gems on the container `lxc exec #{name} -- rm -rf /var/gems/*` end def modify_manifest file_path = "config/manifest.yml" # Update the path if necessary # Read the YAML file if File.exist?(file_path) yaml_content = YAML.load_file(file_path) # Yield the hash to the block for modification yield(yaml_content) if block_given? # Write the changes back to the YAML file File.write(file_path, yaml_content.to_yaml) else puts "File not found: #{file_path}" end end def notify_terminal(message, title) `terminal-notifier -message "#{message}" -title "#{title}" -contentImage "crane.png"` end def containerized?(name) `lxc list -c n -f csv`.include? name end def create_container(name, image = "panamax") return if containerized? name config = load_configuration # Set default image if not specified image = config["image"].nil? ? "panamax" : config["image"] `lxc launch #{image} #{name}` unless containerized? name find_container_ip(name) do |ip_address| `harbr container add #{name} #{ip_address}` if config["host"].nil? `harbr host add #{name} 9292 #{name}.harbr.zero2one.ee` `harbr host add #{name} 9393 next.#{name}.harbr.zero2one.ee` else `harbr host add #{name} 9292 #{config["host"]}` `harbr host add #{name} 9393 next.#{config["host"]}` end `harbr commit` end end # Method to process ERB templates and write to build directory def process_template(template_path, target_path, binding) content = ERB.new(File.read(template_path)).result(binding) File.write(target_path, content) end def manifest? File.exist? File.join(Dir.pwd, "config", "manifest.yml") end def commit(new_message) # Ensure we are in a git repository system("git rev-parse --git-dir > /dev/null 2>&1") or raise "Not a git repository" # commit changes with the new message `git commit -am '#{new_message}'` rescue => e puts "Error during commit amend: #{e.message}" end def amend_last_commit(new_message) # Ensure we are in a git repository system("git rev-parse --git-dir > /dev/null 2>&1") or raise "Not a git repository" # Amend the last commit with the new message system("git commit --amend -m \"#{new_message}\"") or raise "Failed to amend the last commit" puts "Successfully amended the last commit." rescue => e puts "Error during commit amend: #{e.message}" end def load_configuration global_config_dir = File.expand_path("~/.config/harbr") global_config_path = File.join(global_config_dir, "manifest.yml") local_config_path = File.join(Dir.pwd, "config", "manifest.yml") # Ensure global configuration exists unless File.exist?(global_config_path) FileUtils.mkdir_p(global_config_dir) unless Dir.exist?(global_config_dir) File.write(global_config_path, create_example_global_config) end # Ensure local configuration exists unless File.exist?(local_config_path) end # Load and merge configurations global_config = YAML.load_file(global_config_path) || {} if File.exist? local_config_path local_config = YAML.load_file(local_config_path) || {} global_config.merge!(local_config) end global_config end desc "containerize", "Create a container for an app" def containerize config = load_configuration name = config["name"] image = config["image"] if manifest? create_container(name, image) end end desc "rollback", "rollback an application using the configuration from config/manifest.yml" def rollback(name = nil) with_error_handling do progressing("Roll back") do config = load_configuration name = name.nil? ? config["name"] : name rolled_back = config["rolledback"] if config["rolledback"] == true puts "No rollback available" else `lxc exec #{name} -- bash -c 'if [ -d /var/www/app/container/live ]; then rm /var/www/app/container/live; fi'` `lxc exec #{name} -- mv /var/www/app/container/rollback /var/www/app/container/live` `lxc exec #{name} -- bash -c 'if [ -d /var/www/app/container/rollback ]; then rm /var/www/app/container/rollback; fi'` `lxc exec #{name} -- sv restart live` modify_manifest do |config| config["live"] = config["rollback"] config["rolledback"] = true end end commit("Rolled back #{name} to version #{config["rollback"]} on live") ok("https://#{config['host']}/ok",grace: 15) do notify_terminal("#{name} version #{config['rollback']} is up on live!", "Lbhhr") end end end end end desc "assemble", "Build an application using the configuration from config/manifest.yml into a ruby gem" def assemble(env = "next", version = nil) config = load_configuration name = config["name"] image = config["image"] containerize # Check if we're in the root directory if manifest? # Create the build directory if it doesn't exist if Dir.exist?("build") FileUtils.rm_r("build") end FileUtils.mkdir_p("build/src") # Copy all files and folders to the build directory Dir.glob("*").each do |item| next if item == "build" # Skip the build directory itself next if item == ".git" # Skip the .git directory next if item == "tmp" # Skip the config directory FileUtils.cp_r(item, "build/src/#{item}") end # Parameters for gemspec gem_name = config["name"] gem_version = version.nil? ? config["version"] : version # Ensure the build directory exists FileUtils.mkdir_p("build/lib") # Define the template files and their target locations template_dir = File.expand_path(File.join(File.dirname(__FILE__), "../", "templates")) templates = { "#{template_dir}/lib/panamax.rb.erb" => "build/lib/#{gem_name}.rb", "#{template_dir}/lib/rubygems_plugin.rb.erb" => "build/lib/rubygems_plugin.rb", "#{template_dir}/panamax.gemspec.erb" => "build/#{gem_name}.gemspec" } # Process each template templates.each do |template, target| process_template(template, target, binding) end `cd build && gem build #{gem_name}.gemspec` notify_terminal("Assembled #{name}", "Lbhhr") else puts "config/manifest file not found. Please ensure you are in the root directory of your project." end end desc "drop", "Destroy an app and remove all traces" def drop(name = nil) with_error_handling do return unless yes?("Are you sure you want to drop this app? This action cannot be undone. (y/n)") # Load configuration and retrieve the name if not provided config = load_configuration unless name name ||= config["name"] progressing("Drop") do if manifest? # Check if the LXC container exists if `lxc list`.include?(name) # Commands to run if the container exists `lxc delete #{name} --force` `harbr container delete #{name}` `harbr commit` end end end end end desc "logs", "Show logs for the container" method_option :live, type: :boolean, aliases: "-l", desc: "Process in live mode" def logs(name = nil) with_error_handling do config = load_configuration name = config["name"] if options[:live] exec "lxc exec #{name} -- tail -f /var/log/container/live/current" else exec "lxc exec #{name} -- tail -f /var/log/container/next/current" end end end desc "hoist", "Deploy an application using the configuration from config/manifest.yml to next" method_option :current, type: :boolean, aliases: "-c", desc: "curret version" method_option :skipok, type: :boolean, aliases: "-so", desc: "curret version" method_option :major, type: :boolean, aliases: "-ma", desc: "Increment major version" method_option :msg, type: :boolean, aliases: "-msg", desc: "set the commit message" method_option :minor, type: :boolean, aliases: "-mi", desc: "Increment minor version" method_option :feature, type: :boolean, aliases: "-fea", desc: "Increment minor version" method_option :fix, type: :boolean, aliases: "-f", desc: "Increment minor version" method_option :release, type: :boolean, aliases: "-rel", desc: "Increment minor version" method_option :patch, type: :boolean, aliases: "-pa", desc: "Increment minor version" def hoist(name = nil) with_error_handling do #clean up the build directory if Dir.exist?("build") FileUtils.rm_r("build") end config = load_configuration msg =nil if options[:msg] msg = ask("commit massage: ") end focus = nil focus = if options[:major] || options[:release] "major" elsif options[:minor] || options[:feature] "minor" elsif options[:patch] || options[:fix] "patch" else "patch" end version = increment_version(config["version"], focus.to_sym) if options[:current] version = config['version'] end modify_manifest do |config| config["version"] = version end progressing("Hoist") do name = config["name"] gem = "#{name}-#{version}.gem" assemble "next", version `lxc file push -rp build/#{gem} #{name}/var/gems/` `lxc exec #{name} -- /root/.rbenv/shims/gem install /var/gems/#{gem} --no-document` `lxc exec #{name} -- sv restart next` `lxc exec #{name} -- sv restart next.ws` modify_manifest do |config| config["next"] = version end clean_up(name) change = "RELEASE #{version}" commit("#{change}\n\n#{msg}\n\n Hoisted #{name} version #{version}") ok("https://next.#{config['host']}/ok",grace: 25,skip:options[:skipok]) do notify_terminal(" #{name} version #{version} is up on next!", "Lbhhr") end end end end desc "deploy", "Deploy an application using the configuration from config/manifest.yml to live" def deploy(name = nil) with_error_handling do progressing("Deploy") do config = load_configuration name = name.nil? ? config["name"] : name if config["next"].nil? puts "No deploymnt available" else `lxc exec #{name} -- bash -c 'if [ -d /var/www/app/container/rollback ]; then rm -rf /var/www/app/container/rollback; fi'` `lxc exec #{name} -- mv /var/www/app/container/live /var/www/app/container/rollback` `lxc exec #{name} -- [ -d "/var/www/app/container/next" ] && cp -r /var/www/app/container/next /var/www/app/container/live` `lxc exec #{name} -- sv restart live` `lxc exec #{name} -- sv restart ws` modify_manifest do |config| config["rollback"] = config["live"] config["live"] = config["next"] config["rolledback"] = false end end commit("Deployed #{name} version #{config["live"]} to live") ok("https://#{config['host']}/ok",grace: 15) do notify_terminal(" #{name} version #{version} is up on live!", "Lbhhr") end end end end desc "version", "Display the version of Lbhrr" def version with_error_handling do puts "lbhrr version #{Lbhrr::VERSION}" end end desc "update", "Display the version of Lbhrr" def update with_error_handling do `gem update lbhrr --conservative` end end end LbhrrCLI.start(ARGV)