require "path" require "file_utils" require "socket" require "./module" require "./config" require "../common/network" require "../container/abstract_container_engine" require "../container/docker_engine" require "../container/podman_engine" module DocMachine::Build class Run Log = DocMachine::Build::Log.for("run") # Instance variable for the container engine property container_engine : DocMachine::Container::AbstractContainerEngine @basehash : String @docker_name : String @docker_opts : Array(String) @process : Process? def initialize(@config : DocMachine::Build::Config) data = "#{@config.data_dir}:#{@config.port}" @basehash = Digest::SHA256.hexdigest(data)[0..6] @docker_name = "docmachine-#{@basehash}" @docker_opts = [] of String @process = nil # Initialize the container engine based on configuration @container_engine = ( if @config.container_runtime == "podman" DocMachine::Container::PodmanEngine.new else DocMachine::Container::DockerEngine.new end ) end # Cleanup environment # Create directories # Setup permissions def prepare() Log.info { "basedir = #{@config.data_dir}" } Log.info { "container_image = #{@config.image_tag}" } Log.info { "container_runtime = #{@config.container_runtime}" } Log.info { "action = #{@config.action}" } self._pull_image() self._avoid_duplicates() unless @config.enable_multiple end private def _avoid_duplicates Log.info { "Multiple Instances: stopping duplicate containers (for #{@docker_name})" } container_id = @container_engine.find_container_id(@docker_name) Log.info { "Multiple Instances: container_name: #{@docker_name}" } Log.info { "Multiple Instances: container_id: #{container_id || "-"}" } if !container_id.empty? @container_engine.kill_container(@docker_name) @container_engine.remove_container(@docker_name) end end def _pull_image # FIXME: add option to force update data_cache_dir = if ENV["XDG_CACHE_HOME"]? Path[ENV["XDG_CACHE_HOME"], "docmachine"] else Path[ENV["HOME"], ".cache", "docmachine"] end ## Build cache if it doesn't exist data_cache_file = data_cache_dir / "image.tar" Log.info { "Checking cache #{data_cache_file}..." } if !File.exists? data_cache_file.to_s Log.info { "Downloading #{@config.image_tag} image..." } @container_engine.pull_image(@config.image_tag) Log.info { "Building cache for image (#{data_cache_dir})" } FileUtils.mkdir_p(data_cache_dir) status = @container_engine.save_image(@config.image_tag, data_cache_file.to_s) else Log.info { "Cache already exists. Skipping." } end if @config.enable_cache Log.info { "Loading #{@config.image_tag} image from cache..." } status = @container_engine.load_image(data_cache_file.to_s) else Log.info { "Loading #{@config.image_tag} image from local registry..." } # FIXME: check that local image exists end end def start() # Start with default uid/gid ext_uid = %x{id -u}.strip.to_i ext_gid = %x{id -g}.strip.to_i # ...but use subuid/subgid if available File.each_line("/etc/subuid") do |line| split = line.split(":") next if split[0] != %x{id -u -n}.strip subuid = split[1].to_i ext_uid += subuid - 1 end File.each_line("/etc/subgid") do |line| split = line.split(":") next if split[0] != %x{id -g -n}.strip subgid = split[1].to_i ext_gid += subgid - 1 end Log.info { "ext uid: #{ext_uid}" } Log.info { "ext gid: #{ext_gid}" } docker_opts = [] of String docker_opts << "-i" # Add tty support docker_opts << "-t" if @config.enable_tty # Add container name docker_opts.concat ["--name", @docker_name] docker_opts << "--rm" docker_opts << "--shm-size=1gb" # docker_opts << "--privileged" docker_opts.concat ["-e", "EXT_UID=#{ext_uid}"] docker_opts.concat ["-e", "EXT_GID=#{ext_gid}"] docker_opts.concat ["-v", "#{@config.data_dir}/docs:/app/docs"] docker_opts.concat ["-v", "#{@config.data_dir}/slides:/app/slides"] docker_opts.concat ["-v", "#{@config.data_dir}/images:/app/images"] docker_opts.concat ["-v", "#{@config.data_dir}/_build:/app/_build"] ## Detect Marp SCSS if File.exists?("#{@config.data_dir}/.marp/theme.scss") docker_opt_marp_theme = ["-v", "#{@config.data_dir}/.marp:/app/.marp"] docker_opts.concat docker_opt_marp_theme Log.info { "Theme: detected Marp files. Adding option to command line (#{docker_opt_marp_theme})" } else Log.info { "Theme: no theme detected. Using default files" } end ## Detect Mkdocs configuration - old format (full) if File.exists?("#{@config.data_dir}/mkdocs.yml") Log.info { "Docs: detected mkdocs.yml file. Please rename to mkdocs-patch.yml" } exit 1 end ## Detect Mkdocs configuration - new format (patch) if File.exists?("#{@config.data_dir}/mkdocs-patch.yml") docker_opt_mkdocs_config = ["-v", "#{@config.data_dir}/mkdocs-patch.yml:/app/mkdocs-patch.yml"] docker_opts.concat docker_opt_mkdocs_config Log.info { "Docs: detected mkdocs-patch.yml file. Adding option to command line (#{docker_opt_mkdocs_config})" } else Log.info { "Docs: no mkdocs-patch.yml detected. Using default files" } end ## Detect docs if Dir.exists?("#{@config.data_dir}/docs") Log.info { "Docs: detected docs directory." } mkdocs_port = Network.find_port(@config.port) docker_opt_mkdocs_port = ["-p", "#{mkdocs_port}:5100"] docker_opts.concat docker_opt_mkdocs_port Log.notice { "Using port #{mkdocs_port} for docs" } Log.info { "Docs: Adding option to command line (#{docker_opt_mkdocs_port})" } else Log.info { "Docs: no docs detected." } end ## Detect slides if Dir.exists?("#{@config.data_dir}/slides") Log.info { "Slides: detected slides directory." } marp_port = Network.find_port(@config.port + 100) docker_opt_marp_port = ["-p", "#{marp_port}:5200"] docker_opts.concat docker_opt_marp_port Log.info { "Slides: Adding option to command line (#{docker_opt_marp_port})" } Log.notice { "Slides: Using port #{marp_port} for slides" } else Log.info { "Slides: no slides directory detected." } end docker_opts << @config.image_tag docker_opts << @config.action Log.info { docker_str = [@config.container_runtime].concat(docker_opts).join(" ").colorize(:yellow) "#{@config.container_runtime.capitalize}: #{docker_str}" } @process = @container_engine.run_container( @config.image_tag, @config.action, @docker_name, docker_opts, @config.enable_tty ) end def wait() process = @process return if process.nil? Signal::INT.trap do Log.warn { "Received CTRL-C" } @container_engine.kill_container(@docker_name) end process.wait end end end