drender.R 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246
  1. #' Render Dockerized R Markdown Documents
  2. #'
  3. #' Render dockerized R Markdown documents using Docker containers.
  4. #' Rabix tools/workflows will be ran if there is a \code{Rabixfile}
  5. #' generated by \link{lift} under the same directory.
  6. #'
  7. #' Before using \code{drender()}, run \link{lift} on the document
  8. #' first to generate \code{Dockerfile}.
  9. #' See \code{vignette('liftr-intro')} for details about the extended
  10. #' YAML front-matter metadata format and system requirements for
  11. #' rendering dockerized R Markdown documents.
  12. #'
  13. #' @param input Input file to render in Docker container.
  14. #' @param tag Docker image name to build, sent as docker argument \code{-t}.
  15. #' If not specified, it will use the same name as the input file.
  16. #' @param build_args A character string specifying additional
  17. #' \code{docker build} arguments. For example,
  18. #' \code{--pull=true -m="1024m" --memory-swap="-1"}.
  19. #' @param container_name Docker container name to run.
  20. #' If not specified, we will generate and use a random name.
  21. #' @param clean default FALSE, if TRUE, clean all containers before build.
  22. #' @param reset Should we cleanup the Docker container and
  23. #' Docker image after getting the rendered result?
  24. #' @param prebuild a command line string to call before docker build
  25. #' @param cache default TRUE, if FALSE, build with --no-cache=true
  26. #' @param rm default FALSE, if TRUE build with --rm
  27. #' @param port default 80 map to shiny's 3838 service
  28. #' @param browseURL FALSE, if TRUE open shiny app in the browser.
  29. #' @param privileged TRUE, with '--privileged=true'
  30. #' @param dind TRUE run with " -v /var/run/docker.sock:/var/run/docker.sock"
  31. #' @param ... Additional arguments passed to
  32. #' \code{\link[rmarkdown]{render}}.
  33. #'
  34. #' @return Rendered file is written to the same directory of the input file.
  35. #' A character vector with the image name and container name will be
  36. #' returned. You will be able to manage them with \code{docker}
  37. #' commands later if \code{reset = FALSE}.
  38. #'
  39. #' @export drender
  40. #'
  41. #' @importFrom rmarkdown render
  42. #'
  43. #' @examples
  44. ## Included in \dontrun{} since users need
  45. ## Docker and Rabix installed to run them.
  46. #' # 1. Dockerized R Markdown document
  47. #' # Docker is required to run the example,
  48. #' # so make sure we can use `docker` in shell.
  49. #' dir_docker = paste0(tempdir(), '/drender_docker/')
  50. #' dir.create(dir_docker)
  51. #' file.copy(system.file("examples/docker.Rmd", package = "liftr"), dir_docker)
  52. #' docker_input = paste0(dir_docker, "docker.Rmd")
  53. #' lift(docker_input)
  54. #' \dontrun{
  55. #' drender(docker_input)
  56. #' # view rendered document
  57. #' browseURL(paste0(dir_docker, "docker.html"))}
  58. #'
  59. #' # 2. Dockerized R Markdown document with Rabix options
  60. #' # Docker and Rabix are required to run the example,
  61. #' # so make sure we can use `docker` and `rabix` in shell.
  62. #' dir_rabix = paste0(tempdir(), '/drender_rabix/')
  63. #' dir.create(dir_rabix)
  64. #' file.copy(system.file("template/rabix.Rmd", package = "liftr"), dir_rabix)
  65. #' rabix_input = paste0(dir_rabix, "rabix.Rmd")
  66. #' lift(rabix_input)
  67. #' \dontrun{
  68. #' drender(rabix_input)
  69. #' # view rendered document
  70. #' browseURL(paste0(dir_rabix, "rabix.html"))}
  71. drender = function (input = NULL,
  72. tag = NULL, build_args = NULL, container_name = NULL,
  73. reset = FALSE,
  74. clean = FALSE,
  75. cache = TRUE,
  76. rm = FALSE,
  77. prebuild = NULL,
  78. port = 80,
  79. browseURL = FALSE,
  80. privileged = TRUE,
  81. dind = TRUE,
  82. ...) {
  83. if (is.null(input))
  84. stop('missing input file')
  85. if (!file.exists(normalizePath(input)))
  86. stop('input file does not exist')
  87. if(clean){
  88. message("cleaning containers...")
  89. system("docker stop $(docker ps -a -q)")
  90. system("docker rm $(docker ps -a -q)")
  91. }
  92. # run rabix first if Rabixfile is found
  93. rabixfile_path = paste0(file_dir(input), '/Rabixfile')
  94. if (file.exists(rabixfile_path)) {
  95. if (Sys.which('rabix') == '')
  96. stop('Cannot find `rabix` on system search path,
  97. please ensure we can use `rabix` from shell')
  98. rabix_cmd = paste0(readLines(rabixfile_path), collapse = '\n')
  99. system(rabix_cmd)
  100. }
  101. # docker build
  102. if(!is.na(file.info(input)$isdir) && file.info(input)$isdir){
  103. dockerfile_path = file.path(dirname(input), '/Dockerfile')
  104. }else{
  105. dockerfile_path = paste0(file_dir(input), '/Dockerfile')
  106. }
  107. if (!file.exists(dockerfile_path))
  108. stop('Cannot find Dockerfile in the same directory of input file')
  109. if (Sys.which('docker') == '')
  110. stop('Cannot find `docker` on system search path,
  111. please ensure we can use `docker` from shell')
  112. image_name = ifelse(is.null(tag), tolower(file_name_sans(input)), tag)
  113. docker_build_cmd = paste0("docker build ",
  114. ifelse(cache, " ", " --no-cache=true "),
  115. ifelse(rm, " --rm=true ", " "),
  116. build_args, " -t=\"", image_name, "\" ",
  117. file_dir(dockerfile_path))
  118. # docker run
  119. container_name = ifelse(is.null(container_name),
  120. paste0('liftr_container_', uuid()),
  121. container_name)
  122. docker_run_cmd_base =
  123. paste0("docker run ",
  124. ifelse(privileged, " -privileged=true"," "),
  125. ifelse(dind, " -v /var/run/docker.sock:/var/run/docker.sock", " "),
  126. ifelse(rm, " --rm ", " "),
  127. "--name \"", container_name,
  128. "\" -u `id -u $USER` -v \"",
  129. file_dir(dockerfile_path), ":", "/liftrroot/\" ",
  130. image_name,
  131. " /usr/bin/Rscript -e \"library('knitr');library('rmarkdown');",
  132. "library('shiny');setwd('/liftrroot/');")
  133. # process additional arguments passed to rmarkdown::render()
  134. dots_arg = list(...)
  135. if (length(dots_arg) == 0L) {
  136. if(get_type(input) == "shinydoc"){
  137. docker_run_cmd = paste0(docker_run_cmd_base,
  138. render_engine(input),
  139. "(",quote_str(basename(input)),");",
  140. "file.copy(", quote_str(c(basename(input),"/srv/shiny-server/")),
  141. ")\"")
  142. }else{
  143. docker_run_cmd = paste0(docker_run_cmd_base, render_engine(input),
  144. "('",
  145. file_name(input), "')\"")
  146. }
  147. } else {
  148. if (!is.null(dots_arg$input))
  149. stop('input can only be specified once')
  150. if (!is.null(dots_arg$output_file) |
  151. !is.null(dots_arg$output_dir) |
  152. !is.null(dots_arg$intermediates_dir)) {
  153. stop('`output_file`, `output_dir`, and `intermediates_dir`
  154. are not supported to be changed now, we will consider
  155. this in the next versions.')
  156. }
  157. if(is_shinyapp(input)){
  158. dots_arg$file = file_name(input)
  159. }else{
  160. dots_arg$input = file_name(input)
  161. }
  162. tmp = tempfile()
  163. dput(dots_arg, file = tmp)
  164. render_args = paste0(readLines(tmp), collapse = '\n')
  165. render_cmd = paste0("do.call(",render_engine(input), ", ",render_args, ")")
  166. docker_run_cmd = paste0(docker_run_cmd_base, render_cmd, "\"")
  167. }
  168. if(!is.null(prebuild)){
  169. message(prebuild)
  170. system2(prebuild)
  171. }
  172. message(docker_build_cmd)
  173. system(docker_build_cmd)
  174. if(is_shinyapp(input) || is_shinydoc(input)){
  175. shiny_run = paste("docker run -dp ", paste0(port, ":3838 "), image_name)
  176. url = paste0("http://localhost:", port, "/", basename(input))
  177. if(browseURL){
  178. message("lauching shiny app in docker ...")
  179. system(shiny_run)
  180. message("Open browser for ", url)
  181. browseURL(url)
  182. }else{
  183. message("run docker like this: ", shiny_run)
  184. message("You can view it at ", url)
  185. }
  186. }else{
  187. message(docker_run_cmd)
  188. system(docker_run_cmd)
  189. }
  190. # cleanup docker containers and images
  191. # TODO: needs exception handling
  192. if (reset) {
  193. system(paste0("docker stop \"", container_name, "\""))
  194. system(paste0("docker rm -f \"", container_name, "\""))
  195. system(paste0("docker rmi -f \"", image_name, "\""))
  196. }
  197. if(is_shinyapp(input)){
  198. return(list('image_name' = image_name,
  199. 'container_name' = container_name,
  200. 'shiny_run' = shiny_run,
  201. 'url' = url))
  202. }else{
  203. return(list('image_name' = image_name, 'container_name' = container_name))
  204. }
  205. }