This commit introduces a comprehensive implementation and refactoring of the container engine module. Key enhancements include the addition of abstract classes and the implementation of container engines. New features: - Developed `DockerEngine` and `PodmanEngine` classes, deriving from `AbstractContainerEngine` to provide modular functionality. - Added a CLI `--container-runtime` option, allowing users to select their preferred runtime, defaulting to Docker if unspecified. - Introduced a `container_runtime` property in `DocMachine::Build::Config` for improved runtime management. Refactoring: - Refactored code to replace hardcoded Docker commands with method calls to `AbstractContainerEngine`, promoting code reuse and abstraction. Signed-off-by: Glenn Y. Rolland <glenux@glenux.net>
213 lines
7.3 KiB
Crystal
213 lines
7.3 KiB
Crystal
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
|