#' Render Containerized R Markdown Documents
#'
#' @description
#' Render R Markdown documents using Docker.
#'
#' @details
#' Before using this function, please run \code{\link{lift}} on the
#' RMD document first to generate the \code{Dockerfile}.
#'
#' After a successful rendering, you will be able to clean up the
#' Docker image with \code{\link{prune_image}}.
#'
#' Please see \code{vignette('liftr-intro')} for details of the extended
#' YAML metadata format and system requirements for writing and rendering
#' containerized R Markdown documents.
#'
#' @param input Input file to render in Docker container.
#' @param tag Docker image name to build, sent as docker argument \code{-t}.
#' If not specified, it will use the same name as the input file.
#' @param container_name Docker container name to run.
#' If not specified, will use a randomly generated name.
#' @param cache Logical. Controls the \code{--no-cache} argument
#' in \code{docker run}. Setting this to be \code{TRUE} can accelerate
#' the rendering speed substantially for repeated/interactive rendering
#' since the Docker image layers will be cached, with only the changed
#' (knitr related) image layer being updated. Default is \code{TRUE}.
#' @param build_args A character string specifying additional
#' \code{docker build} arguments. For example,
#' \code{--pull=true -m="1024m" --memory-swap="-1"}.
#' @param run_args A character string specifying additional
#' \code{docker run} arguments. For example, \code{--privileged=true}.
#' @param prune Logical. Should we clean up all dangling containers,
#' volumes, networks, and images in case the rendering was not successful?
#' Default is \code{TRUE}.
#' @param prune_info Logical. Should we save the Docker container and
#' image information to a YAML file (name ended with \code{.docker.yml})
#' for manual pruning or inspections later? Default is \code{TRUE}.
#' @param dry_run Preview the Docker commands but do not run them?
#' Useful for debugging purposes. Default is \code{FALSE}.
#' @param ... Additional arguments passed to
#' \code{\link[rmarkdown]{render}}.
#'
#' @return
#' \itemize{
#' \item A list containing the image name, container name,
#' and Docker commands will be returned.
#' \item An YAML file ending with \code{.docker.yml} storing the
#' image name, container name, and Docker commands for rendering
#' this document will be written to the directory of the input file.
#' \item The rendered output will be written to the directory of the
#' input file.
#' }
#'
#' @export render_docker
#'
#' @importFrom rmarkdown render
#' @importFrom yaml as.yaml
#'
#' @examples
## Included in \dontrun{} since users need Docker installed to run them.
#' # copy example file
#' dir_example = paste0(tempdir(), "/liftr-tidyverse/")
#' dir.create(dir_example)
#' file.copy(system.file("examples/liftr-tidyverse.Rmd", package = "liftr"), dir_example)
#'
#' # containerization
#' input = paste0(dir_example, "liftr-tidyverse.Rmd")
#' lift(input)
#'
#' \dontrun{
#' # print the Docker commands first
#' render_docker(input, dry_run = TRUE)
#'
#' # render the document with Docker
#' render_docker(input)
#'
#' # view rendered document
#' browseURL(paste0(dir_example, "liftr-tidyverse.pdf"))
#'
#' # remove the generated Docker image
#' prune_image(paste0(dir_example, "liftr-tidyverse.docker.yml"))}
render_docker = function(
input = NULL, tag = NULL, container_name = NULL,
cache = TRUE, build_args = NULL, run_args = NULL,
prune = TRUE, prune_info = TRUE, dry_run = FALSE, ...) {
if (is.null(input))
stop('missing input file')
if (!file.exists(normalizePath(input)))
stop('input file does not exist')
# docker build
dockerfile_path = paste0(file_dir(input), '/Dockerfile')
if (!file.exists(dockerfile_path))
stop('Cannot find Dockerfile in the same directory of input file,
please containerize the R Markdown document via lift() first.')
if (Sys.which('docker') == '')
stop('Cannot find `docker` on system search path,
please ensure we can use `docker` from shell')
image_name = ifelse(is.null(tag), file_name_sans(input), tag)
cache = paste0("--no-cache=", ifelse(cache, "false", "true"))
docker_build_cmd = paste0(
"docker build ", cache, " --rm=true ",
build_args, " -t=\"", image_name, "\" ",
file_dir(dockerfile_path))
# docker run
container_name = ifelse(
is.null(container_name),
paste0('liftr_container_', uuid()),
container_name)
docker_run_cmd_base = paste0(
"docker run --rm ", run_args,
" --name \"", container_name,
"\" -u `id -u $USER` -v \"",
file_dir(dockerfile_path), ":", "/liftrroot/\" ",
image_name,
" Rscript -e \"library('knitr');library('rmarkdown');",
"library('shiny');setwd('/liftrroot/');")
# process additional arguments passed to rmarkdown::render()
dots_arg = list(...)
if (length(dots_arg) == 0L) {
docker_run_cmd = paste0(
docker_run_cmd_base, "render(input = '",
file_name(input), "')\"")
} else {
if (!is.null(dots_arg$input))
stop('input can only be specified once')
if (!is.null(dots_arg$output_file) |
!is.null(dots_arg$output_dir) |
!is.null(dots_arg$intermediates_dir)) {
stop('`output_file`, `output_dir`, and `intermediates_dir`
are not supported to be changed now, we will consider
this in the next versions.')
}
dots_arg$input = file_name(input)
tmp = tempfile()
dput(dots_arg, file = tmp)
render_args = paste0(readLines(tmp), collapse = '\n')
render_cmd = paste0("do.call(render, ", render_args, ')')
docker_run_cmd = paste0(docker_run_cmd_base, render_cmd, "\"")
}
# output container and image info before rendering
res = list(
'container_name' = container_name,
'image_name' = image_name,
'docker_build_cmd' = docker_build_cmd,
'docker_run_cmd' = docker_run_cmd)
# run docker commands or only return the commands
if (!dry_run) {
# write the docker container info to a file
if (prune_info) {
writeLines(as.yaml(res), con = paste0(
file_dir(input), '/', file_name_sans(input), '.docker.yml'))
}
# render
system(docker_build_cmd)
system(docker_run_cmd)
# cleanup dangling containers, images, volumes, and networks
if (prune) {
cat('Cleaning up...\n')
on.exit(system('docker system prune --force'))
}
}
res
}
#' @rdname render_docker
#' @export drender
drender = function(...) {
.Deprecated('render_docker')
}
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.