tests/testthat/test-docker-client-containers.R

context("docker client: containers")


test_that("create", {
  d <- test_docker_client()
  nm <- rand_str(10, "stevedore_")
  x <- d$container$create("hello-world", name = nm)
  expect_is(x, "docker_container")
  expect_is(x, "stevedore_object")

  expect_equal(x$name(), nm)
  expect_equal(x$id(), x$inspect()$id)
  expect_equal(x$inspect()$name, paste0("/", nm))
  expect_identical(x$reload(), x)

  expect_null(x$remove())

  e <- get_error(x$inspect())
  expect_is(e, "docker_error")
  expect_equal(e$code, 404L)
  expect_equal(e$endpoint, "container_inspect")
  expect_equal(e$reason, "no such container")
})

test_that("create, using image", {
  d <- test_docker_client()
  nm <- rand_str(10, "stevedore_")
  img <- d$image$get("hello-world")
  x <- d$container$create(img, name = nm)
  expect_equal(x$image()$id(), img$id())
  x$remove()
})


test_that("get", {
  d <- test_docker_client()
  nm <- rand_str(10, "stevedore_")
  x1 <- d$container$create("hello-world", name = nm)
  x2 <- d$container$get(nm)
  expect_identical(x1$inspect(FALSE), x2$inspect(FALSE))

  d$container$remove(nm)

  e <- get_error(d$container$get(nm))
  expect_is(e, "docker_error")
  expect_equal(e$code, 404L)
})


test_that("list", {
  d <- test_docker_client()
  nm <- rand_str(10, "stevedore_")
  x <- d$container$create("hello-world", name = nm)

  cl <- d$container$list(all = TRUE)
  expect_is(cl, "data.frame")
  expect_true("names" %in% names(cl))
  expect_true(list(nm) %in% cl$names)
  expect_true(nm %in% cl$name)

  x$remove()
})


test_that("list with arguments", {
  d <- test_docker_client()
  cl <- d$container$list()

  nm <- rand_str(10, "stevedore_")
  x <- d$container$create("hello-world", name = nm)

  ## First, the easy arguments:
  cl <- d$container$list(all = TRUE, limit = 10L, size = TRUE)
  i <- match(nm, cl$name)
  expect_false(is.na(i))
  expect_false(is.na(cl$size_root_fs[i]))

  ## Then the filters
  f1 <- c(status = "created")
  f2 <- list(status = "exited")

  cl1 <- d$container$list(all = TRUE, limit = 10L, size = TRUE, filters = f1)
  cl2 <- d$container$list(all = TRUE, limit = 10L, size = TRUE, filters = f2)
  expect_true(nm %in% cl1$name)
  expect_false(nm %in% cl2$name)

  expect_identical(x$start(), x)
  x$wait()

  cl1 <- d$container$list(all = TRUE, limit = 10L, size = TRUE, filters = f1)
  cl2 <- d$container$list(all = TRUE, limit = 10L, size = TRUE, filters = f2)
  expect_false(nm %in% cl1$name)
  expect_true(nm %in% cl2$name)

  x$remove()
})


test_that("prune", {
  d <- test_docker_client()
  nm <- rand_str(10, "stevedore_")
  x <- d$container$create("hello-world", name = nm)

  ans <- d$container$prune()
  expect_true(x$id() %in% ans$containers_deleted)
})


test_that("diff", {
  ## TODO: need to get a better test in here - one that actually shows
  ## we can report this.  For now it should be ok to show that the
  ## endpoint works, I guess.
  d <- test_docker_client()
  nm <- rand_str(10, "stevedore_")
  x <- d$container$create("hello-world", name = nm)
  xd <- x$diff()

  expect_is(xd, "data.frame")
  x$remove()
})


test_that("export", {
  d <- test_docker_client()
  nm <- rand_str(10, "stevedore_")
  x <- d$container$create("hello-world", name = nm)

  ## TODO: here, and other export type ones - we need some optional
  ## 'filename' argument for dumping the export into.  It might be
  ## worth a small wrapper around it (as a 'tar' class) so that we can
  ## interact more sensibly.  We might also need to stream to disk -
  ## not memory via modifying the request.  This would be done the
  ## same way as 'hijack' is done - but we'd pass through the required
  ## filename and go via curl::curl_fetch_disk rather than
  ## curl_fetch_memory.

  ## TODO: what does one _do_ with an exported container?
  tar <- x$export()
  expect_is(tar, "raw")
  path <- untar_bin(tar)
  on.exit(unlink(path, recursive = TRUE), add = TRUE)
  expect_true("hello" %in% dir(path))
  x$remove()
})


test_that("path_stat", {
  d <- test_docker_client()
  nm <- rand_str(10, "stevedore_")
  x <- d$container$create("hello-world", name = nm)
  on.exit(x$remove())

  ## There's no spec here so not much more we can do
  dat <- x$path_stat("/")
  expect_is(dat, "list")
  expect_equal(dat$name, "/")

  e <- get_error(x$path_stat("foo"))
  expect_is(e, "docker_error")
  expect_equal(e$code, 404L)
})


test_that("archive export", {
  ## TODO: another tar endpoint
  d <- test_docker_client()
  nm <- rand_str(10, "stevedore_")
  x <- d$container$create("hello-world", name = nm)
  on.exit(x$remove())

  bin <- x$get_archive("hello", NULL)
  p <- untar_bin(bin)
  on.exit(unlink(p, recursive = TRUE), add = TRUE)
  expect_true(file.exists(file.path(p, "hello")))

  path <- x$get_archive("hello", tempfile_test())
  expect_true(file.exists(path))
  expect_identical(readBin(path, raw(), file.size(path)), bin)

  expect_error(x$get_archive("hello", FALSE),
               "'dest' must be a scalar character (non-NA), or NULL",
               fixed = TRUE)
})


test_that("archive import", {
  d <- test_docker_client()
  nm <- rand_str(10, "stevedore_")
  x <- d$container$create("hello-world", name = nm)
  on.exit(x$remove())

  e <- get_error(x$path_stat("foo"))
  expect_equal(e$code, 404L)

  path <- tempfile_test()
  dir.create(path)
  dat <- rand_str(100)
  writeLines(dat, file.path(path, "foo"))

  bin <- tar_directory(path)

  ## Down here, we need to get the types correct for handling.
  x$put_archive(bin, "/")
  bin2 <- x$get_archive("foo", NULL)
  p <- untar_bin(bin2)
  on.exit(unlink(p, recursive = TRUE), add = TRUE)
  expect_true(file.exists(file.path(p, "foo")))
  expect_identical(readLines(file.path(p, "foo")), dat)
})


test_that("archive import (file)", {
  d <- test_docker_client()
  nm <- rand_str(10, "stevedore_")
  ## x <- d$container$create("hello-world", name = nm)
  x <- d$container$create("bfirsh/reticulate-splines", name = nm)
  on.exit(x$remove(force = TRUE))
  x$start()

  path <- tempfile_test()
  dir.create(path)
  dat <- rand_str(100)
  p <- file.path(path, "foo")
  writeLines(dat, p)

  ## Down here, we need to get the types correct for handling.
  x$put_archive(p, "/")

  tmp <- x$get_archive("foo", tempfile_test())
  p2 <- untar_bin(read_binary(tmp))
  on.exit(unlink(p2, recursive = TRUE), add = TRUE)
  expect_true(file.exists(file.path(p2, "foo")))
  expect_identical(readLines(file.path(p2, "foo")), dat)
})

test_that("kill", {
  d <- test_docker_client()
  nm <- rand_str(10, "stevedore_")
  x <- d$container$create("alpine", cmd = c("sleep", "10000"), name = nm)
  expect_identical(withVisible(x$start()),
                   list(value = x, visible = FALSE))
  expect_equal(x$status(), "running")
  expect_null(x$kill())
  expect_equal(x$status(), "exited")
  x$remove()
})


test_that("logs", {
  d <- test_docker_client()
  nm <- rand_str(10, "stevedore_")
  ## TODO: this only prints out one per second - I'd like to up this
  ## to every 0.1s so that we can run this for less time.
  x <- d$container$create("bfirsh/reticulate-splines", name = nm)
  expect_identical(withVisible(x$start()),
                   list(value = x, visible = FALSE))
  logs <- x$logs()
  expect_is(logs, "docker_stream")
  expect_equal(logs[[1]], "Reticulating spline 1...\n")
  x$kill()
  x$remove()

  ## TODO: this can be tested separately from the docker bits too (and
  ## should be)
  expect_match(format(logs, style = "plain"),
               "^Reticulating spline \\d+...\n$")
  expect_match(format(logs, style = "prefix"),
               "^O> Reticulating spline \\d+...\n")
  expect_equal(all(crayon::has_style(format(logs, style = "colour"))),
               crayon::has_color())
  expect_match(capture.output(print(logs, style = "plain")),
               "^Reticulating spline \\d+...$")
  expect_match(capture.output(print(logs, style = "prefix")),
               "^O> Reticulating spline \\d+...$")
})


test_that("pause/unpause", {
  d <- test_docker_client()
  nm <- rand_str(10, "stevedore_")
  ## TODO: this only prints out one per second - I'd like to up this
  ## to every 0.1s so that we can run this for less time.
  x <- d$container$create("bfirsh/reticulate-splines", name = nm)
  x$start()
  expect_null(x$pause())
  expect_equal(x$status(), "paused")
  e <- get_error(x$pause())
  expect_is(e, "docker_error")

  ## This is not actually in the spec, but it returns 409 (Conflict)
  ## on pause of paused container - unpausing an unpaused container
  ## returns 500 though so this does not seem reliable.
  ## expect_equal(e$code, 409)
  expect_null(x$unpause())
  expect_equal(x$status(), "running")

  e <- get_error(x$unpause())
  expect_is(e, "docker_error")

  x$kill()
  x$remove()
})


test_that("rename", {
  d <- test_docker_client()
  nm1 <- rand_str(10, "stevedore_")
  nm2 <- rand_str(10, "stevedore_")
  ## TODO: this only prints out one per second - I'd like to up this
  ## to every 0.1s so that we can run this for less time.
  x <- d$container$create("bfirsh/reticulate-splines", name = nm1)
  expect_null(x$rename(nm2))
  expect_equal(x$reload()$name(), nm2)
  x$remove()
})


test_that("restart", {
  d <- test_docker_client()
  nm <- rand_str(10, "stevedore_")
  x <- d$container$create("bfirsh/reticulate-splines", name = nm)
  x$start()
  Sys.sleep(0.5)
  logs1 <- x$logs()
  expect_null(x$restart(0))
  Sys.sleep(0.5)
  logs2 <- x$logs()

  expect_equal(sum(logs2 == "Reticulating spline 1...\n"), 2)
  x$kill()
  x$remove()
})


test_that("stats", {
  d <- test_docker_client()
  nm <- rand_str(10, "stevedore_")
  x <- d$container$create("bfirsh/reticulate-splines", name = nm)
  s <- x$stats()
  ## There is no spec here so I can't really test much here except
  ## that this does exist.  Note that this is quite slow if using a
  ## running container!
  expect_is(s, "list")
  x$remove()
})


test_that("stop", {
  d <- test_docker_client()
  nm <- rand_str(10, "stevedore_")
  x <- d$container$create("bfirsh/reticulate-splines", name = nm)
  x$start()
  expect_null(x$stop(0))
  expect_equal(x$status(), "exited")
  x$remove()
})


test_that("top", {
  d <- test_docker_client()
  nm <- rand_str(10, "stevedore_")
  x <- d$container$create("bfirsh/reticulate-splines", name = nm)
  x$start()
  t <- x$top()
  expect_is(t, "data.frame")
  expect_true("PID" %in% names(t))
  x$kill()
  x$remove()
})


test_that("top works with custom data.frame handler", {
  d <- test_docker_client(data_frame = dummy_data_frame_wrapper)
  nm <- rand_str(10, "stevedore_")
  x <- d$container$create("bfirsh/reticulate-splines", name = nm)
  x$start()
  t <- x$top()
  x$kill()
  x$remove()
  expect_is(t, "data.frame")
  expect_is(t, "extra")
})


test_that("update", {
  d <- test_docker_client()
  nm <- rand_str(10, "stevedore_")
  x <- d$container$create("bfirsh/reticulate-splines", name = nm)
  on.exit(x$remove())
  info <- x$inspect(FALSE)
  n <- as.integer(10 * 1001 * 1001)
  y <- x$update(memory = n, memory_swap = -1L)

  expect_identical(x, y)
  info2 <- y$inspect(TRUE)
  expect_identical(info2$host_config$memory, n)
  expect_identical(info2$host_config$memory_swap, -1L)
})


test_that("wait", {
  d <- test_docker_client()
  nm <- rand_str(10, "stevedore_")
  x <- d$container$create("hello-world", name = nm)
  x$start()
  expect_equal(x$wait(), list(exit_code = 0L))
  expect_equal(x$status(), "exited")
  x$remove()
})


test_that("prune", {
  d <- test_docker_client()
  nm <- rand_str(10, "stevedore_")
  x <- d$container$create("hello-world", name = nm)
  ans <- d$container$prune()
  expect_true(x$id() %in% ans$containers_deleted)
  e <- get_error(x$status())
  expect_is(e, "docker_error")
  expect_equal(e$code, 404L)
})


test_that("image", {
  d <- test_docker_client()
  nm <- rand_str(10, "stevedore_")
  x <- d$container$create("hello-world", name = nm)
  img <- x$image()
  cmp <- d$image$get("hello-world")
  expect_equal(cmp$inspect(), img$inspect())
  x$remove()
})


test_that("exec_create", {
  d <- test_docker_client()
  nm <- rand_str(10, "stevedore_")
  ## this sets up a container that will run forever
  x <- d$container$create("richfitz/iterate",
                          cmd = c("100", "100"),
                          name = nm)
  x$start()
  ans <- x$exec_create("ls")

  expect_is(ans, "docker_exec")
  expect_is(ans, "stevedore_object")

  info <- ans$inspect()
  expect_false(info$running)
  expect_true(info$open_stdout)
  expect_true(info$open_stderr)

  txt <- capture.output(res <- ans$start(detach = FALSE))
  expect_is(res, "docker_stream")
  cmp <- unlist(strsplit(format(res, style = "prefix"), "\n"))
  expect_equal(txt, cmp)

  x$kill()
  x$remove()
})


test_that("exec: in one", {
  d <- test_docker_client()
  nm <- rand_str(10, "stevedore_")
  ## this sets up a container that will run forever
  x <- d$container$create("richfitz/iterate",
                          cmd = c("100", "100"),
                          name = nm)

  ## This is almost there but detach needs setting properly to become
  ## FALSE by default or something.
  x$start()

  txt <- capture.output(res <- x$exec("ls"))

  expect_is(res$output, "docker_stream")
  cmp <- unlist(strsplit(format(res$output, style = "prefix"), "\n"))
  expect_equal(txt, cmp)
  expect_equal(res$exit_code, 0L)
  expect_match(res$id, "^[[:xdigit:]]+$")

  x$kill()
  x$remove()
})


test_that("exec, twice", {
  skip("wip")
  d <- test_docker_client()
  nm <- rand_str(10, "stevedore_")
  ## this sets up a container that will run forever
  x <- d$container$create("richfitz/iterate",
                          cmd = c("100", "100"),
                          name = nm)
  x$start()
  ans <- x$exec_create("ls")

  ans$start()
  ## This does not throw but it should!  The issue here is that docker
  ## is not actuall creating error codes here.  So some more care
  ## might be needed.  Probably best not to return prepared exec
  ## instances by default!
  expect_error(ans$start())
})


test_that("run", {
  d <- test_docker_client()
  txt <- capture.output(ans <- d$container$run("hello-world"))
  on.exit(ans$container$remove())
  expect_is(ans, "docker_run_output")

  expect_equal(names(ans), c("container", "logs"))
  expect_is(ans$container, "docker_container")
  expect_is(ans$logs, "docker_stream")

  expect_equal(txt, format(ans$logs, strip_newline = TRUE, style = "prefix"))

  txt <- capture.output(print(ans, style = "prefix"))
  expect_match(txt, "<docker_run_output>", fixed = TRUE, all = FALSE)
  expect_match(txt, "  $container", fixed = TRUE, all = FALSE)
  expect_match(txt, "  $logs", fixed = TRUE, all = FALSE)
})


test_that("run: detach", {
  d <- test_docker_client()
  ans <- d$container$run("richfitz/iterate", c("10", "0.1"), detach = TRUE)
  expect_is(ans, "docker_container")
  expect_equal(ans$wait(), list("exit_code" = 0L))
  ans$remove()
})


test_that("run: no such container", {
  d <- test_docker_client()
  msg <- capture_messages(
    e <- get_error(d$container$run("richfitz/nosuchcontainer")))
  expect_match(
    msg, "Unable to find image 'richfitz/nosuchcontainer:latest' locally",
    fixed = TRUE, all = FALSE) # allow additional here
  expect_is(e, "docker_error")
  ## TODO: the endpoint registering as failed here is image_create,
  ## not image_pull which is the one that does fail.  This is because
  ## docker_get_image uses tryCatch and *rethrows* the error but what
  ## we should do is withCallingHandlers and then avoid throwing at
  ## all if we can recover.
  if (has_internet()) {
    expect_equal(e$code, 404L)
  } else {
    expect_equal(e$code, 500L)
  }
  expect_equal(e$endpoint, "image_create")
})


test_that("run with get/pull error handling", {
  d <- test_docker_client()
  skip("currently broken on ci")
  err <- get_error(d$container$run("foo bar"))
  expect_is(err, "docker_error")
  expect_equal(err$code, 400L) # bad request
})


test_that("run: remove", {
  d <- test_docker_client()
  ans <- d$container$run("hello-world", rm = TRUE, stream = FALSE)
  expect_equal(names(ans), c("container", "logs"))
  expect_is(ans$container, "docker_container")
  expect_is(ans$logs, "docker_stream")
  e <- get_error(d$container$get(ans$container$id()))
  expect_true(is_docker_error_not_found(e))
})


test_that("run: error", {
  d <- test_docker_client()
  e <- get_error(
    d$container$run("richfitz/error", "4", rm = TRUE, stream = FALSE))

  expect_is(e, "container_error")

  expect_equal(
    e$message,
    "Command '4' in image 'richfitz/error:latest' returned non-zero exit status 4", fixed = TRUE)

  expect_is(e$container, "docker_container")
  expect_equal(e$exit_status, 4L)
  expect_equal(e$cmd, "4")
  expect_equal(e$image$id(), d$image$get("richfitz/error")$id())
  expect_is(e$out, "docker_stream")
  expect_equal(e$out, docker_stream("throwing error 4\n", 1L))
})


test_that("run: error to stderr", {
  d <- test_docker_client()
  e <- get_error(
    d$container$run("richfitz/error", "14", rm = TRUE, stream = FALSE))

  expect_is(e, "container_error")

  expect_equal(
    e$message,
    "Command '14' in image 'richfitz/error:latest' returned non-zero exit status 14\nthrowing error 14 to stderr\n", fixed = TRUE)
})


test_that("run with image", {
  d <- test_docker_client()
  img <- d$image$get("hello-world")
  ans <- d$container$run(img, rm = TRUE, stream = FALSE)
  expect_is(ans$logs, "docker_stream")
})


test_that("scalar cmd", {
  d <- test_docker_client()
  c1 <- d$container$create("alpine:3.1", I("echo hello world"))

  dat <- c1$inspect(FALSE)
  expect_identical(dat$path, "echo")
  expect_identical(dat$args, c("hello", "world"))

  c1$start()
  c1$wait()
  log <- c1$logs()
  expect_equal(format(log, style = "plain", filter = "stdout"),
               "hello world\n")
  c1$remove()
})


test_that("scalar exec", {
  d <- test_docker_client()
  nm <- rand_str(10, "stevedore_")
  ## this sets up a container that will run forever
  x <- d$container$create("richfitz/iterate",
                          cmd = c("100", "100"),
                          name = nm)
  x$start()
  ans <- x$exec_create(I("echo hello world"))
  dat <- ans$inspect(FALSE)
  expect_equal(dat$process_config$entrypoint, "echo")
  expect_equal(dat$process_config$arguments, c("hello", "world"))
  capture.output(res <- ans$start(detach = FALSE))

  expect_equal(format(res, style = "plain", filter = "stdout"),
               "hello world\n")
  x$kill()
  x$remove()
})


test_that("stream logs", {
  d <- test_docker_client()
  nm <- rand_str(10, "stevedore_")
  p <- tempfile_test()
  x <- d$container$create("richfitz/iterate",
                          cmd = c("10", "0.1"),
                          name = nm)
  x$start()
  res <- x$logs(follow = TRUE, stream = p)
  expect_is(res, "docker_stream")
  cmp <- readLines(p)
  expect_equal(format(res, style = "prefix"), paste0(cmp, "\n"))

  x$remove()
})


test_that("fetch, but don't print, logs", {
  ## Getting some stray beginning of lines using follow on a stopped
  ## container - I *bet* that there's a cat
  d <- test_docker_client()
  nm <- rand_str(10, "stevedore_")
  x <- d$container$create("richfitz/iterate",
                          cmd = c("10", "0.1"),
                          name = nm)
  x$start()
  x$wait()
  p <- tempfile_test()
  expect_silent(v1 <- x$logs(stream = NULL, follow = TRUE))
  expect_silent(v2 <- x$logs(stream = p, follow = TRUE))
  txt <- capture.output(v3 <- x$logs(stream = stdout(), follow = TRUE))

  expect_match(txt, "O> Done!", all = FALSE, fixed = TRUE)
  expect_is(v1, "docker_stream")
  expect_is(v2, "docker_stream")
  expect_is(v3, "docker_stream")

  expect_equal(format(v1, style = "prefix"), paste0(txt, "\n"))
  expect_equal(format(v2, style = "prefix"), paste0(txt, "\n"))
  expect_equal(format(v3, style = "prefix"), paste0(txt, "\n"))

  x$remove()
})


test_that("auto-remove: create", {
  d <- test_docker_client()
  nm <- rand_str(10, "stevedore_")
  x <- d$container$create(
    "alpine:3.1", cmd = c("echo", "hello world"), name = nm,
    host_config = list(AutoRemove = jsonlite::unbox(TRUE)))
  expect_true(x$inspect(FALSE)$host_config$auto_remove)
  x$start()
  x$wait()
  e <- repeat_until_error(x$inspect)
  expect_equal(e$code, 404L)
})


test_that("auto-remove: run", {
  d <- test_docker_client()
  nm <- rand_str(10, "stevedore_")
  x <- d$container$run(
    "alpine:3.1", cmd = c("echo", "hello world"), name = nm,
    rm = TRUE, detach = TRUE)

  expect_true(x$inspect(FALSE)$host_config$auto_remove)
  x$wait()
  e <- repeat_until_error(x$inspect)
  expect_equal(e$code, 404L)
})


## TODO: there is not a test in here (or anywhere really) that this
## shows that this actually streams logs from the beginning (rather
## than just going all at the end).
test_that("stream", {
  d <- test_docker_client()
  nm <- rand_str(10, "stevedore_")
  p <- tempfile_test()
  x <- d$container$run("richfitz/iterate",
                       cmd = c("10", "0.1"),
                       name = nm, detach = FALSE,
                       stream = p, rm = TRUE)
  expect_equal(paste0(readLines(p), "\n"),
               format(x$logs, style = "prefix"))
})


test_that("volume map", {
  d <- test_docker_client()
  nm <- rand_str(10, "stevedore_")

  p <- tempfile_test()
  dir.create(p)
  on.exit(unlink(p, recursive = TRUE), add = TRUE)
  v <- sprintf("%s:%s", getwd(), "/host")

  x <- d$container$create("alpine:latest", cmd = c("ls", "/host"),
                          name = nm, volumes = v)
  x$start()
  x$wait()
  expect_equal(sort(trimws(format(x$logs(), style = "plain"))),
               sort(dir()))
  x$remove()
})


test_that("volume map: docker volume", {
  skip_on_travis() # FIXME
  d <- test_docker_client()
  volume <- d$volume$create("myvolume")

  nm <- rand_str(10, "stevedore_")
  v <- sprintf("%s:%s", volume$name(), "/host")

  x <- d$container$create("richfitz/iterate",
                          cmd = c("100", "100"),
                          name = nm, volumes = v)

  on.exit({
    x$remove(force = TRUE)
    volume$remove()
  })

  x$start()
  x$exec(c("touch", "/host/foo"), stderr = FALSE, stdout = FALSE)

  y <- d$container$create("alpine",
                          cmd = c("ls", "/host"),
                          name = rand_str(10, "stevedore_"), volumes = v)
  y$start()
  y$wait()
  expect_equal(trimws(format(y$logs(), style = "plain")), "foo")
  y$remove()
})


test_that("volume map: readonly", {
  skip_on_travis() # FIXME
  d <- test_docker_client()
  volume <- d$volume$create("avolume")
  nm <- rand_str(10, "stevedore_")
  dest <- "/host"
  x <- d$container$create("richfitz/iterate",
                          cmd = c("100", "100"),
                          name = nm,
                          volumes = volume$map(dest, TRUE))

  on.exit({
    x$remove(force = TRUE)
    volume$remove()
  })

  x$start()

  x$exec(c("touch", "/host/foo"), stdout = FALSE, stderr = FALSE)

  ans <- x$exec(c("ls", "/host"), stream = FALSE)
  expect_equal(ans$output, character(0))
})


test_that("port map", {
  ## Roughly equivalent to:
  ## docker run --rm -p 10080:80 nginx
  port <- "10080" # port to use - hopefully clear
  ports <- sprintf("%s:80", port)
  d <- test_docker_client()
  nm <- rand_str(10, "stevedore_")
  x <- d$container$create("nginx", name = nm, ports = ports)
  x$start()
  on.exit(x$remove(force = TRUE))

  url <- sprintf("http://127.0.0.1:%s/", port)
  ## This test is a little flakey to start - so retry up to 5 times
  ## (with a 1/2s delay between each)
  for (i in 1:5) {
    dat <- tryCatch(curl::curl_fetch_memory(url),
                    error = function(e) NULL)
    if (!is.null(dat)) {
      break
    }
    Sys.sleep(0.5)
  }
  expect_equal(dat$status_code, 200L)
  expect_true(grepl("nginx", rawToChar(dat$content)))
})


test_that("port map - random free port", {
  d <- test_docker_client()
  nm <- rand_str(10, "stevedore_")
  x <- d$container$create("nginx", name = nm, ports = 80L)
  x$start()
  on.exit(x$remove(force = TRUE))

  ports <- x$ports()
  expect_is(ports, "data.frame")
  expect_equal(names(ports),
               c("container_port", "protocol", "host_ip", "host_port"))
  expect_true(all(vlapply(ports, is.character)))
  expect_equal(ports$protocol[[1]], "tcp")
  expect_equal(ports$host_ip[[1]], "0.0.0.0")
  expect_equal(ports$container_port[[1]], "80")
})


test_that("port map - expose all ports", {
  d <- test_docker_client()
  nm <- rand_str(10, "stevedore_")
  x <- d$container$create("nginx", name = nm, ports = TRUE)
  on.exit(x$remove(force = TRUE))
  x$start()
  ports <- x$ports()
  expect_identical(ports$container_port[[1]], "80")
})


test_that("query ports of container with none", {
  d <- test_docker_client()
  nm <- rand_str(10, "stevedore_")
  x <- d$container$create("richfitz/iterate",
                          cmd = c("100", "100"),
                          name = nm)
  ports <- x$ports()
  expect_equal(ports, data_frame(container_port = character(0),
                                 protocol = character(0),
                                 host_ip = character(0),
                                 host_port = character(0)))

  x$remove()
})


test_that("port map - data.frame map", {
  d <- test_docker_client(data_frame = dummy_data_frame_wrapper)
  nm <- rand_str(10, "stevedore_")
  x <- d$container$create("nginx", name = nm, ports = TRUE)
  on.exit(x$remove(force = TRUE))
  x$start()
  ports <- x$ports()
  expect_identical(ports$container_port[[1]], "80")
  expect_is(ports, "extra")
  expect_is(ports, "data.frame")
})



test_that("network: host", {
  d <- test_docker_client()
  nm <- rand_str(10, "stevedore_")
  x <- d$container$create("nginx", name = nm, ports = "80")
  x$start()
  on.exit(x$remove(force = TRUE), add = TRUE)
  port <- x$ports()$host_port

  ## docker run --rm --network=host richfitz/curl -s http://localhost:32776
  ## docker run --rm richfitz/curl -s http://localhost:32776

  image <- "richfitz/curl"
  args <- c("-s", sprintf("http://localhost:%s", port))

  ## This will fail because we don't have host networking
  y <- d$container$create(image, args)
  on.exit(y$remove(force = TRUE), add = TRUE)
  y$start()
  code <- y$wait()
  expect_false(code$exit_code == 0)
  expect_equal(format(y$logs(), style = "plain"), character(0))

  ## Then with host networking
  z <- d$container$create(image, args, network = "host")
  on.exit(z$remove(force = TRUE), add = TRUE)
  z$start()
  code <- z$wait()
  expect_true(code$exit_code == 0)
  expect_match(format(z$logs(), style = "plain"), "nginx", all = FALSE)
})


test_that("network: custom", {
  ## Here's the scenario:
  ##
  ## docker network create testing
  ## docker run --network=testing --rm -d --name server nginx
  ## docker run --network=testing --rm richfitz/curl -s http://server
  ## docker stop server
  ## docker network rm testing

  server <- rand_str(10, "stevedore_")
  client <- rand_str(10, "stevedore_")
  network <- rand_str(3, "stevedore_")
  url <- paste0("http://", server)

  d <- test_docker_client()
  nw <- d$network$create(network)
  on.exit({
    if (exists("x")) {
      x$remove(force = TRUE)
    }
    if (exists("y")) {
      y$remove(force = TRUE)
    }
    nw$remove()
  })

  x <- d$container$create("nginx", name = server, network = nw)
  x$start()

  y <- d$container$create("richfitz/curl", c("-s", server),
                          network = network, name = client)
  y$start()
  code <- y$wait()
  expect_true(code$exit_code == 0)
  expect_match(format(y$logs(), style = "plain"), "nginx", all = FALSE)
})


test_that("logs with tty", {
  ## > When the TTY setting is enabled in POST /containers/create, the
  ## > stream is not multiplexed. The data exchanged over the hijacked
  ## > connection is simply the raw data from the process PTY and
  ## > client's stdin.
  d <- test_docker_client()
  nm1 <- rand_str(10, "stevedore_")
  nm2 <- rand_str(10, "stevedore_")
  p <- tempfile_test()
  x <- d$container$create("richfitz/iterate",
                          cmd = c("10", "0.1"),
                          name = nm1, tty = TRUE)
  y <- d$container$create("richfitz/iterate",
                          cmd = c("10", "0.1"),
                          name = nm2, tty = FALSE)
  x$start()
  y$start()
  x$wait()
  y$wait()

  res1 <- x$logs()
  res2 <- y$logs()
  expect_is(res1, "character")
  expect_equal(res1[[length(res1)]], "Done!")

  expect_equal(res1, trimws(format(res2, style = "plain")))

  x$remove()
  y$remove()
})


test_that("stream logs with tty", {
  d <- test_docker_client()
  nm <- rand_str(10, "stevedore_")
  p <- tempfile_test()
  x <- d$container$create("richfitz/iterate",
                          cmd = c("10", "0.1"),
                          name = nm, tty = TRUE)
  x$start()
  res <- x$logs(follow = TRUE, stream = p)
  expect_is(res, "character")

  cmp <- readLines(p)
  expect_equal(res, cmp)

  x$remove()
})


test_that("run, passing through host_config", {
  d <- test_docker_client()
  nm <- rand_str(10, "stevedore_")

  p <- tempfile_test()
  dir.create(p)
  on.exit(unlink(p, recursive = TRUE), add = TRUE)
  v <- sprintf("%s:%s", getwd(), "/host")

  res <- d$container$run("alpine:latest", cmd = c("ls", "/host"),
                         stream = FALSE, name = nm, volumes = v, rm = TRUE)

  expect_equal(sort(trimws(format(res$logs, style = "plain"))),
               sort(dir()))
  e <- get_error(res$container$status())
  expect_true(is_docker_error_not_found(e))
})


test_that("commit", {
  d <- test_docker_client()
  x <- d$container$run("alpine:3.1", c("tar", "-zcvf", "/etc.tar.gz", "/etc"),
                       stream = FALSE)
  on.exit(x$container$remove())
  expect_equal(x$container$diff(),
               data_frame(path = "/etc.tar.gz", kind = 1L))
  img <- x$container$commit("alpine-with-etc", "latest")
  h <- img$history()
  expect_match(h[1, "created_by"], "^tar ")

  files <- d$container$run(img, c("ls", "/"), rm = TRUE, stream = FALSE)$logs
  expect_true("etc.tar.gz" %in% trimws(files))

  img$remove()
})


test_that("versioned responses", {
  skip("work in progress")
  ## TODO: this is broken for 25..28 inclusive.  The fix is to get the
  ## newer port definition here, but I don't know if that is because
  ## the spec (and responses) are wrong or if it's because new clients
  ## do not return the correct information.  This is a bit of a faff
  ## really.
  d1 <- test_docker_client(api_version = test_docker_versions[[1]])
  nm <- rand_str(10, "stevedore_")
  ## options(error = recover)
  x <- d1$container$create("nginx", ports = TRUE, name = nm)

  ## Error is with data: named empty list (json: {})
  ## From 'Ports'

  d2 <- test_docker_client(api_version = tail(test_docker_versions, 1))
  x <- d2$container$create("nginx", ports = TRUE)
  ## on.exit(x$remove(force = TRUE))
  x$start()
  x$ports()
  y <- d1$container$get(x$id())
  x$remove(force = TRUE)
})


test_that("environment variables on create", {
  d <- test_docker_client()
  res <- d$container$run("alpine", "env", detach = FALSE, stream = NULL,
                         env = c("X_FOO" = "1"), rm = TRUE)
  txt <- format(res$logs, style = "plain", strip_newline = TRUE)
  expect_true("X_FOO=1" %in% txt)
})


test_that("set user", {
  skip_on_windows()
  d <- test_docker_client()
  uid <- user(group = TRUE)
  res1 <- d$container$run("alpine:3.1", "id",
                          detach = FALSE, stream = NULL, rm = TRUE)
  res2 <- d$container$run("alpine:3.1", "id", user = uid,
                          detach = FALSE, stream = NULL, rm = TRUE)

  parse_id <- function(x) {
    x <- strsplit(x, "\\s+")[[1]]
    xx <- strsplit(x, "=", fixed = TRUE)
    label <- vcapply(xx, "[[", 1L)
    value <- vcapply(xx, "[[", 2L)
    paste(sub("[^0-9].*", "", value[label == "uid"]),
          sub("[^0-9].*", "", value[label == "gid"]),
          sep = ":")
  }

  expect_equal(parse_id(res1$logs), "0:0")
  expect_equal(parse_id(res2$logs), uid)
})


test_that("get (offline)", {
  cl <- null_docker_client()
  x <- cl$container$get(dummy_id())
  expect_is(x, "docker_container")
  expect_equal(x$id(), dummy_id())
})


test_that("process ports", {
  opts <- list(data_frame = identity)
  expect_equal(docker_container_ports(NULL, opts),
               data_frame(container_port = character(0),
                          protocol = character(0),
                          host_ip = character(0),
                          host_port = character(0)))

  d <- list("80/tcp" = data_frame(host_ip = "0.0.0.0", host_port = "32789"))
  attrs <- list(network_settings = list(ports = d))

  expect_equal(docker_container_ports(attrs, opts),
               data_frame(container_port = "80",
                          protocol = "tcp",
                          host_ip = "0.0.0.0",
                          host_port = "32789"))

  cl <- null_docker_client()
  x <- cl$container$get(dummy_id())
  expect_equal(x$ports(),
               data_frame(container_port = character(0),
                          protocol = character(0),
                          host_ip = character(0),
                          host_port = character(0)))
})


test_that("cp_in single file", {
  cl <- test_docker_client()
  tmp1 <- tempfile()
  tmp2 <- tempfile()
  writeLines("hello world", tmp1)
  writeLines("goodbye world", tmp2)

  nm <- rand_str(10, "stevedore_")
  x <- cl$container$run("richfitz/iterate",
                        cmd = c("100", "100"),
                        name = nm,
                        rm = TRUE,
                        detach = TRUE)
  on.exit({
    x$kill()
    unlink(c(tmp1, tmp2))
  })

  remote_read <- function(path, x) {
    as.character(x$exec(c("cat", path), stream = FALSE)$output)
  }

  ## file => nonexistant (and create)
  x$cp_in(tmp1, "tmp/myfile")
  expect_equal(remote_read("tmp/myfile", x), "hello world\n")

  ## file => file (and overwrite)
  x$cp_in(tmp2, "tmp/myfile")
  expect_equal(remote_read("tmp/myfile", x), "goodbye world\n")

  ## file => directory
  x$cp_in(tmp1, "tmp")
  expect_equal(remote_read(file.path("tmp", basename(tmp1)), x),
               "hello world\n")

  ## file => missing dir
  expect_error(x$cp_in(tmp1, "missingdir/"),
               "'dest' is a directory but does not exist on container")
})


test_that("cp in a directory", {
  cl <- test_docker_client()
  tmp1 <- tempfile()
  tmp2 <- tempfile()
  dir.create(tmp1)
  dir.create(tmp2)
  writeLines("hello world", file.path(tmp1, "a"))
  writeLines("goodbye world", file.path(tmp2, "a"))

  nm <- rand_str(10, "stevedore_")
  x <- cl$container$run("richfitz/iterate",
                        cmd = c("100", "100"),
                        name = nm,
                        rm = TRUE,
                        detach = TRUE)
  on.exit({
    x$kill()
    unlink(c(tmp1, tmp2), recursive = TRUE)
  })

  remote_read <- function(path, x) {
    as.character(x$exec(c("cat", path), stream = FALSE)$output)
  }

  ## directory => nonexistant (and create)
  x$cp_in(tmp1, "tmp/mydir")
  expect_equal(remote_read("tmp/mydir/a", x), "hello world\n")

  ## file => existing directory
  x$cp_in(tmp2, "tmp")
  filename <- sprintf("tmp/%s/a", basename(tmp2))
  expect_equal(remote_read(filename, x), "goodbye world\n")

  ## directory => file
  expect_error(x$cp_in(tmp2, "/usr/local/bin/iterate"),
               "as dest '/usr/local/bin/iterate' is a file")
})


test_that("cp_out single file", {
  cl <- test_docker_client()

  nm <- rand_str(10, "stevedore_")
  x <- cl$container$run("richfitz/iterate",
                        cmd = c("100", "100"),
                        name = nm,
                        rm = TRUE,
                        detach = TRUE)
  on.exit({
    x$kill()
    unlink(c(tmp1, tmp2))
  })

  txt <- readLines("images/iterate/iterate")

  ## file => dir
  tmp1 <- tempfile()
  dir.create(tmp1)
  x$cp_out("/usr/local/bin/iterate", tmp1)
  expect_equal(readLines(file.path(tmp1, "iterate")), txt)

  ## file => nonexistant
  tmp2 <- tempfile()
  x$cp_out("/usr/local/bin/iterate", tmp2)
  expect_equal(readLines(tmp2), txt)

  ## file => existing file
  tmp3 <- tempfile()
  writeLines("hello", tmp3)
  x$cp_out("/usr/local/bin/iterate", tmp3)
  expect_equal(readLines(tmp3), txt)

  ## file => missing directory
  tmp4 <- file.path(tempfile(), "subdir")
  expect_error(x$cp_out("/usr/local/bin/iterate", tmp4),
               "Destination directory does not exist")
})


test_that("cp_out directory", {
  cl <- test_docker_client()

  nm <- rand_str(10, "stevedore_")
  x <- cl$container$run("richfitz/iterate",
                        cmd = c("100", "100"),
                        name = nm,
                        rm = TRUE,
                        detach = TRUE)
  on.exit({
    x$kill()
    unlink(c(tmp1, tmp2))
  })

  txt <- readLines("images/iterate/iterate")

  ## dir => dir
  tmp1 <- tempfile()
  dir.create(tmp1)
  x$cp_out("/usr/local/bin", tmp1)
  expect_equal(readLines(file.path(tmp1, "bin", "iterate")), txt)

  ## file => nonexistant
  tmp2 <- tempfile()
  x$cp_out("/usr/local/bin", tmp2)
  expect_equal(readLines(file.path(tmp2, "iterate")), txt)

  ## file => existing file
  tmp3 <- tempfile()
  writeLines("hello", tmp3)
  expect_error(x$cp_out("/usr/local/bin", tmp3),
               "Can't overwrite file '.+' with directory '.+'")
})

Try the stevedore package in your browser

Any scripts or data that you put into this service are public.

stevedore documentation built on July 26, 2023, 5:35 p.m.