#' Render Dockerized R Markdown Documents #' #' Render dockerized R Markdown documents using Docker containers. #' Rabix tools/workflows will be ran if there is a \code{Rabixfile} #' generated by \link{lift} under the same directory. #' #' Before using \code{drender()}, run \link{lift} on the document #' first to generate \code{Dockerfile}. #' See \code{vignette('liftr-intro')} for details about the extended #' YAML front-matter metadata format and system requirements for #' rendering dockerized 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 build_args A character string specifying additional #' \code{docker build} arguments. For example, #' \code{--pull=true -m="1024m" --memory-swap="-1"}. #' @param container_name Docker container name to run. #' If not specified, we will generate and use a random name. #' @param clean default FALSE, if TRUE, clean all containers before build. #' @param reset Should we cleanup the Docker container and #' Docker image after getting the rendered result? #' @param prebuild a command line string to call before docker build #' @param cache default TRUE, if FALSE, build with --no-cache=true #' @param rm default FALSE, if TRUE build with --rm #' @param port default 80 map to shiny's 3838 service #' @param browseURL FALSE, if TRUE open shiny app in the browser. #' @param privileged TRUE, with '--privileged=true' #' @param dind TRUE run with " -v /var/run/docker.sock:/var/run/docker.sock" #' @param ... Additional arguments passed to #' \code{\link[rmarkdown]{render}}. #' #' @return Rendered file is written to the same directory of the input file. #' A character vector with the image name and container name will be #' returned. You will be able to manage them with \code{docker} #' commands later if \code{reset = FALSE}. #' #' @export drender #' #' @importFrom rmarkdown render #' #' @examples ## Included in \dontrun{} since users need ## Docker and Rabix installed to run them. #' # 1. Dockerized R Markdown document #' # Docker is required to run the example, #' # so make sure we can use `docker` in shell. #' dir_docker = paste0(tempdir(), '/drender_docker/') #' dir.create(dir_docker) #' file.copy(system.file("examples/docker.Rmd", package = "liftr"), dir_docker) #' docker_input = paste0(dir_docker, "docker.Rmd") #' lift(docker_input) #' \dontrun{ #' drender(docker_input) #' # view rendered document #' browseURL(paste0(dir_docker, "docker.html"))} #' #' # 2. Dockerized R Markdown document with Rabix options #' # Docker and Rabix are required to run the example, #' # so make sure we can use `docker` and `rabix` in shell. #' dir_rabix = paste0(tempdir(), '/drender_rabix/') #' dir.create(dir_rabix) #' file.copy(system.file("template/rabix.Rmd", package = "liftr"), dir_rabix) #' rabix_input = paste0(dir_rabix, "rabix.Rmd") #' lift(rabix_input) #' \dontrun{ #' drender(rabix_input) #' # view rendered document #' browseURL(paste0(dir_rabix, "rabix.html"))} drender = function (input = NULL, tag = NULL, build_args = NULL, container_name = NULL, reset = FALSE, clean = FALSE, cache = TRUE, rm = FALSE, prebuild = NULL, port = 80, browseURL = FALSE, privileged = TRUE, dind = TRUE, ...) { if (is.null(input)) stop('missing input file') if (!file.exists(normalizePath(input))) stop('input file does not exist') if(clean){ message("cleaning containers...") system("docker stop $(docker ps -a -q)") system("docker rm $(docker ps -a -q)") } # run rabix first if Rabixfile is found rabixfile_path = paste0(file_dir(input), '/Rabixfile') if (file.exists(rabixfile_path)) { if (Sys.which('rabix') == '') stop('Cannot find `rabix` on system search path, please ensure we can use `rabix` from shell') rabix_cmd = paste0(readLines(rabixfile_path), collapse = '\n') system(rabix_cmd) } # docker build if(!is.na(file.info(input)$isdir) && file.info(input)$isdir){ dockerfile_path = file.path(dirname(input), '/Dockerfile') }else{ dockerfile_path = paste0(file_dir(input), '/Dockerfile') } if (!file.exists(dockerfile_path)) stop('Cannot find Dockerfile in the same directory of input file') 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), tolower(file_name_sans(input)), tag) docker_build_cmd = paste0("docker build ", ifelse(cache, " ", " --no-cache=true "), ifelse(rm, " --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 ", ifelse(privileged, " -privileged=true"," "), ifelse(dind, " -v /var/run/docker.sock:/var/run/docker.sock", " "), ifelse(rm, " --rm ", " "), "--name \"", container_name, "\" -u `id -u $USER` -v \"", file_dir(dockerfile_path), ":", "/liftrroot/\" ", image_name, " /usr/bin/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) { if(get_type(input) == "shinydoc"){ docker_run_cmd = paste0(docker_run_cmd_base, render_engine(input), "(",quote_str(basename(input)),");", "file.copy(", quote_str(c(basename(input),"/srv/shiny-server/")), ")\"") }else{ docker_run_cmd = paste0(docker_run_cmd_base, render_engine(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.') } if(is_shinyapp(input)){ dots_arg$file = file_name(input) }else{ 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_engine(input), ", ",render_args, ")") docker_run_cmd = paste0(docker_run_cmd_base, render_cmd, "\"") } if(!is.null(prebuild)){ message(prebuild) system2(prebuild) } message(docker_build_cmd) system(docker_build_cmd) if(is_shinyapp(input) || is_shinydoc(input)){ shiny_run = paste("docker run -dp ", paste0(port, ":3838 "), image_name) url = paste0("http://localhost:", port, "/", basename(input)) if(browseURL){ message("lauching shiny app in docker ...") system(shiny_run) message("Open browser for ", url) browseURL(url) }else{ message("run docker like this: ", shiny_run) message("You can view it at ", url) } }else{ message(docker_run_cmd) system(docker_run_cmd) } # cleanup docker containers and images # TODO: needs exception handling if (reset) { system(paste0("docker stop \"", container_name, "\"")) system(paste0("docker rm -f \"", container_name, "\"")) system(paste0("docker rmi -f \"", image_name, "\"")) } if(is_shinyapp(input)){ return(list('image_name' = image_name, 'container_name' = container_name, 'shiny_run' = shiny_run, 'url' = url)) }else{ return(list('image_name' = image_name, 'container_name' = container_name)) } }