render_docker.R 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184
  1. #' Render Containerized R Markdown Documents
  2. #'
  3. #' @description
  4. #' Render R Markdown documents using Docker.
  5. #'
  6. #' @details
  7. #' Before using this function, please run \code{\link{lift}} on the
  8. #' RMD document first to generate the \code{Dockerfile}.
  9. #'
  10. #' After a successful rendering, you will be able to clean up the
  11. #' Docker image with \code{\link{prune_image}}.
  12. #'
  13. #' Please see \code{vignette('liftr-intro')} for details of the extended
  14. #' YAML metadata format and system requirements for writing and rendering
  15. #' containerized R Markdown documents.
  16. #'
  17. #' @param input Input file to render in Docker container.
  18. #' @param tag Docker image name to build, sent as docker argument \code{-t}.
  19. #' If not specified, it will use the same name as the input file.
  20. #' @param container_name Docker container name to run.
  21. #' If not specified, will use a randomly generated name.
  22. #' @param cache Logical. Controls the \code{--no-cache} argument
  23. #' in \code{docker run}. Setting this to be \code{TRUE} can accelerate
  24. #' the rendering speed substantially for repeated/interactive rendering
  25. #' since the Docker image layers will be cached, with only the changed
  26. #' (knitr related) image layer being updated. Default is \code{TRUE}.
  27. #' @param build_args A character string specifying additional
  28. #' \code{docker build} arguments. For example,
  29. #' \code{--pull=true -m="1024m" --memory-swap="-1"}.
  30. #' @param run_args A character string specifying additional
  31. #' \code{docker run} arguments. For example, \code{--privileged=true}.
  32. #' @param prune Logical. Should we clean up all dangling containers,
  33. #' volumes, networks, and images in case the rendering was not successful?
  34. #' Default is \code{TRUE}.
  35. #' @param prune_info Logical. Should we save the Docker container and
  36. #' image information to a YAML file (name ended with \code{.docker.yml})
  37. #' for manual pruning or inspections later? Default is \code{TRUE}.
  38. #' @param ... Additional arguments passed to
  39. #' \code{\link[rmarkdown]{render}}.
  40. #'
  41. #' @return
  42. #' \itemize{
  43. #' \item A list containing the image name, container name,
  44. #' and Docker commands will be returned.
  45. #' \item An YAML file ending with \code{.docker.yml} storing the
  46. #' image name, container name, and Docker commands for rendering
  47. #' this document will be written to the directory of the input file.
  48. #' \item The rendered output will be written to the directory of the
  49. #' input file.
  50. #' }
  51. #'
  52. #' @export render_docker
  53. #'
  54. #' @importFrom rmarkdown render
  55. #' @importFrom yaml as.yaml
  56. #'
  57. #' @examples
  58. ## Included in \dontrun{} since users need Docker installed to run them.
  59. #' # copy example file
  60. #' dir_example = paste0(tempdir(), "/liftr-tidyverse/")
  61. #' dir.create(dir_example)
  62. #' file.copy(system.file("examples/liftr-tidyverse.Rmd", package = "liftr"), dir_example)
  63. #'
  64. #' # containerization
  65. #' input = paste0(dir_example, "liftr-tidyverse.Rmd")
  66. #' lift(input)
  67. #'
  68. #' \dontrun{
  69. #' # render the document with Docker
  70. #' render_docker(input)
  71. #'
  72. #' # view rendered document
  73. #' browseURL(paste0(dir_example, "liftr-tidyverse.pdf"))
  74. #'
  75. #' # remove the generated Docker image
  76. #' prune_image(paste0(dir_example, "liftr-tidyverse.docker.yml"))}
  77. render_docker = function(
  78. input = NULL, tag = NULL, container_name = NULL,
  79. cache = TRUE, build_args = NULL, run_args = NULL,
  80. prune = TRUE, prune_info = TRUE, ...) {
  81. if (is.null(input))
  82. stop('missing input file')
  83. if (!file.exists(normalizePath(input)))
  84. stop('input file does not exist')
  85. # docker build
  86. dockerfile_path = paste0(file_dir(input), '/Dockerfile')
  87. if (!file.exists(dockerfile_path))
  88. stop('Cannot find Dockerfile in the same directory of input file,
  89. please containerize the R Markdown document via lift() first.')
  90. if (Sys.which('docker') == '')
  91. stop('Cannot find `docker` on system search path,
  92. please ensure we can use `docker` from shell')
  93. image_name = ifelse(is.null(tag), file_name_sans(input), tag)
  94. cache = paste0("--no-cache=", ifelse(cache, "false", "true"))
  95. docker_build_cmd = paste0(
  96. "docker build ", cache, " --rm=true ",
  97. build_args, " -t=\"", image_name, "\" ",
  98. file_dir(dockerfile_path))
  99. # docker run
  100. container_name = ifelse(
  101. is.null(container_name),
  102. paste0('liftr_container_', uuid()),
  103. container_name)
  104. docker_run_cmd_base = paste0(
  105. "docker run --rm ", run_args,
  106. " --name \"", container_name,
  107. "\" -u `id -u $USER` -v \"",
  108. file_dir(dockerfile_path), ":", "/liftrroot/\" ",
  109. image_name,
  110. " Rscript -e \"library('knitr');library('rmarkdown');",
  111. "library('shiny');setwd('/liftrroot/');")
  112. # process additional arguments passed to rmarkdown::render()
  113. dots_arg = list(...)
  114. if (length(dots_arg) == 0L) {
  115. docker_run_cmd = paste0(
  116. docker_run_cmd_base, "render(input = '",
  117. file_name(input), "')\"")
  118. } else {
  119. if (!is.null(dots_arg$input))
  120. stop('input can only be specified once')
  121. if (!is.null(dots_arg$output_file) |
  122. !is.null(dots_arg$output_dir) |
  123. !is.null(dots_arg$intermediates_dir)) {
  124. stop('`output_file`, `output_dir`, and `intermediates_dir`
  125. are not supported to be changed now, we will consider
  126. this in the next versions.')
  127. }
  128. dots_arg$input = file_name(input)
  129. tmp = tempfile()
  130. dput(dots_arg, file = tmp)
  131. render_args = paste0(readLines(tmp), collapse = '\n')
  132. render_cmd = paste0("do.call(render, ", render_args, ')')
  133. docker_run_cmd = paste0(docker_run_cmd_base, render_cmd, "\"")
  134. }
  135. # output container and image info before rendering
  136. res = list(
  137. 'container_name' = container_name,
  138. 'image_name' = image_name,
  139. 'docker_build_cmd' = docker_build_cmd,
  140. 'docker_run_cmd' = docker_run_cmd)
  141. if (prune_info) {
  142. writeLines(as.yaml(res), con = paste0(
  143. file_dir(input), '/', file_name_sans(input), '.docker.yml'))
  144. }
  145. # render
  146. system(docker_build_cmd)
  147. system(docker_run_cmd)
  148. # cleanup dangling containers, images, volumes, and networks
  149. if (prune) {
  150. cat('Cleaning up...\n')
  151. on.exit(system('docker system prune --force'))
  152. }
  153. res
  154. }
  155. #' @rdname render_docker
  156. #' @export drender
  157. drender = function(...) {
  158. .Deprecated('render_docker')
  159. }