From b9cbbdd2629977aeb5610d41832228198e0a2357 Mon Sep 17 00:00:00 2001 From: Hong Ooi Date: Wed, 6 May 2020 03:24:27 +1000 Subject: [PATCH] Dev (#14) * vignette tweak * bump ver no * tool tests * pass cmd args as processx::run expects * update news --- DESCRIPTION | 2 +- NEWS.md | 3 +- R/ext_tools.R | 71 +++++++++++++++--------- man/call_docker.Rd | 20 +------ man/call_docker_compose.Rd | 4 +- man/call_helm.Rd | 4 +- man/call_kubectl.Rd | 4 +- tests/testthat/test00_tools.R | 39 +++++++++++++ vignettes/vig03_securing_aks_traefik.Rmd | 4 +- 9 files changed, 99 insertions(+), 52 deletions(-) create mode 100644 tests/testthat/test00_tools.R diff --git a/DESCRIPTION b/DESCRIPTION index 2518323..a87ebe9 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,6 +1,6 @@ Package: AzureContainers Title: Interface to 'Container Instances', 'Docker Registry' and 'Kubernetes' in 'Azure' -Version: 1.2.1.9000 +Version: 1.3.0 Authors@R: c( person("Hong", "Ooi", , "hongooi@microsoft.com", role = c("aut", "cre")), person("Bill", "Liang", role = "ctb", comment = "Assistance debugging MMLS on Kubernetes"), diff --git a/NEWS.md b/NEWS.md index a79e73a..089f4fd 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,4 +1,4 @@ -# AzureContainers 1.2.1.9000 +# AzureContainers 1.3.0 - Significant enhancements for AKS: - Fully support creating clusters with managed identities. This is recommended and the new default, compared to the older method of using service principals to control cluster resources. @@ -11,6 +11,7 @@ - The functions to call external tools (`call_docker`, `call_docker_compose`, `call_kubernetes` and `call_helm`) now use the value of the system option `azure_containers_tool_echo` to determine whether to echo output to the screen. If this is unset, the fallback is `TRUE` (as in previous versions). - Remove MMLS vignette; version 9.3.0 is now very old. - New vignettes on securing an ACI deployment with RestRserve, and deploying a secured service on AKS with Traefik/Let's Encrypt. +- The utility functions `call_docker`, `call_docker_compose`, `call_kubectl` and `call_helm` now accept a vector of individual commandline options as the first argument, which is the format expected by `processx::run`. You can still provide the full commandline as a single string but this is discouraged, as it will likely fail for things like paths containing spaces. # AzureContainers 1.2.1 diff --git a/R/ext_tools.R b/R/ext_tools.R index 2134a3f..3627fdb 100644 --- a/R/ext_tools.R +++ b/R/ext_tools.R @@ -1,12 +1,14 @@ #' Call the docker commandline tool #' -#' @param cmd The docker command line to execute. +#' @param cmd The docker command. This should be a _vector_ of individual docker arguments, but can also be a single commandline string. See below. #' @param echo Whether to echo the output of the command to the console. #' @param ... Other arguments to pass to [processx::run]. #' #' @details #' This function calls the `docker` binary, which must be located in your search path. AzureContainers will search for the binary at package startup, and print a warning if it is not found. - +#' +#' The docker command should be specified as a vector of the individual arguments, which is what `processx::run` expects. If a single string is passed, for convenience and back-compatibility reasons `call_docker` will split it into arguments for you. This is prone to error, for example if you are working with pathnames that contain spaces, so it's strongly recommended to pass a vector of arguments as a general practice. +#' #' @return #' A list with the following components: #' - `status`: The exit status of the docker tool. If this is `NA`, then the process was killed and had no exit status. @@ -30,15 +32,18 @@ #' # without any args, prints the docker help screen #' call_docker() #' -#' # build an image +#' # build an image: recommended usage +#' call_docker(c("build", "-t", "myimage", "".")) +#' +#' # alternative usage, will be split into individual arguments #' call_docker("build -t myimage .") #' #' # list running containers -#' call_docker("container ls") +#' call_docker(c("container", "ls")) #' #' # prune unused containers and images -#' call_docker("container prune -f") -#' call_docker("image prune -f") +#' call_docker(c("container", "prune", "-f")) +#' call_docker(c("image", "prune", "-f")) #' #' } #' @export @@ -46,13 +51,15 @@ call_docker <- function(cmd="", ..., echo=getOption("azure_containers_tool_echo" { if(.AzureContainers$docker == "") stop("docker binary not found", call.=FALSE) - message("Docker operation: ", cmd) + + if(length(cmd) == 1 && grepl(" ", cmd, fixed=TRUE)) + cmd <- strsplit(cmd, "\\s+")[[1]] win <- .Platform$OS.type == "windows" if(!win) { dockercmd <- "sudo" - realcmd <- paste(.AzureContainers$docker, cmd) + realcmd <- c(.AzureContainers$docker, cmd) } else { @@ -61,21 +68,23 @@ call_docker <- function(cmd="", ..., echo=getOption("azure_containers_tool_echo" } echo <- as.logical(echo) - val <- processx::run(dockercmd, strsplit(realcmd, " ", fixed=TRUE)[[1]], ..., echo=echo) - val$cmdline <- paste("docker", cmd) + val <- processx::run(dockercmd, realcmd, ..., echo=echo) + val$cmdline <- paste("docker", paste(realcmd, collapse=" ")) invisible(val) } #' Call the docker-compose commandline tool #' -#' @param cmd The docker-compose command line to execute. +#' @param cmd The docker-compose command line to execute. This should be a _vector_ of individual docker-compose arguments, but can also be a single commandline string. See below. #' @param echo Whether to echo the output of the command to the console. #' @param ... Other arguments to pass to [processx::run]. #' #' @details #' This function calls the `docker-compose` binary, which must be located in your search path. AzureContainers will search for the binary at package startup, and print a warning if it is not found. - +#' +#' The docker-compose command should be specified as a vector of the individual arguments, which is what `processx::run` expects. If a single string is passed, for convenience and back-compatibility reasons `call_docker_compose` will split it into arguments for you. This is prone to error, for example if you are working with pathnames that contain spaces, so it's strongly recommended to pass a vector of arguments as a general practice. +#' #' @return #' A list with the following components: #' - `status`: The exit status of the docker-compose tool. If this is `NA`, then the process was killed and had no exit status. @@ -97,13 +106,15 @@ call_docker_compose <- function(cmd="", ..., echo=getOption("azure_containers_to { if(.AzureContainers$dockercompose == "") stop("docker-compose binary not found", call.=FALSE) - message("Docker-compose operation: ", cmd) + + if(length(cmd) == 1 && grepl(" ", cmd, fixed=TRUE)) + cmd <- strsplit(cmd, "\\s+")[[1]] win <- .Platform$OS.type == "windows" if(!win) { dcmpcmd <- "sudo" - realcmd <- paste(.AzureContainers$dockercompose, cmd) + realcmd <- c(.AzureContainers$dockercompose, cmd) } else { @@ -112,22 +123,24 @@ call_docker_compose <- function(cmd="", ..., echo=getOption("azure_containers_to } echo <- as.logical(echo) - val <- processx::run(dcmpcmd, strsplit(realcmd, " ", fixed=TRUE)[[1]], ..., echo=echo) - val$cmdline <- paste("docker-compose", cmd) + val <- processx::run(dcmpcmd, realcmd, ..., echo=echo) + val$cmdline <- paste("docker-compose", paste(realcmd, collapse=" ")) invisible(val) } #' Call the Kubernetes commandline tool, kubectl #' -#' @param cmd The kubectl command line to execute. +#' @param cmd The kubectl command line to execute. This should be a _vector_ of individual kubectl arguments, but can also be a single commandline string. See below. #' @param echo Whether to echo the output of the command to the console. #' @param config The pathname of the cluster config file, if required. #' @param ... Other arguments to pass to [processx::run]. #' #' @details #' This function calls the `kubectl` binary, which must be located in your search path. AzureContainers will search for the binary at package startup, and print a warning if it is not found. - +#' +#' The kubectl command should be specified as a vector of the individual arguments, which is what `processx::run` expects. If a single string is passed, for convenience and back-compatibility reasons `call_docker_compose` will split it into arguments for you. This is prone to error, for example if you are working with pathnames that contain spaces, so it's strongly recommended to pass a vector of arguments as a general practice. +#' #' @return #' A list with the following components: #' - `status`: The exit status of the kubectl tool. If this is `NA`, then the process was killed and had no exit status. @@ -170,25 +183,29 @@ call_kubectl <- function(cmd="", config=NULL, ..., echo=getOption("azure_contain if(!is.null(config)) config <- paste0("--kubeconfig=", config) - message("Kubernetes operation: ", cmd, " ", config) + + if(length(cmd) == 1 && grepl(" ", cmd, fixed=TRUE)) + cmd <- strsplit(cmd, "\\s+")[[1]] echo <- as.logical(echo) - val <- processx::run(.AzureContainers$kubectl, c(strsplit(cmd, " ", fixed=TRUE)[[1]], config), ..., echo=echo) - val$cmdline <- paste("kubectl", cmd, config) + val <- processx::run(.AzureContainers$kubectl, c(cmd, config), ..., echo=echo) + val$cmdline <- paste("kubectl", paste(cmd, collapse=" "), config) invisible(val) } #' Call the Helm commandline tool #' -#' @param cmd The Helm command line to execute. +#' @param cmd The Helm command line to execute. This should be a _vector_ of individual helm arguments, but can also be a single commandline string. See below. #' @param echo Whether to echo the output of the command to the console. #' @param config The pathname of the cluster config file, if required. #' @param ... Other arguments to pass to [processx::run]. #' #' @details #' This function calls the `helm` binary, which must be located in your search path. AzureContainers will search for the binary at package startup, and print a warning if it is not found. - +#' +#' The helm command should be specified as a vector of the individual arguments, which is what `processx::run` expects. If a single string is passed, for convenience and back-compatibility reasons `call_docker_compose` will split it into arguments for you. This is prone to error, for example if you are working with pathnames that contain spaces, so it's strongly recommended to pass a vector of arguments as a general practice. +#' #' @return #' A list with the following components: #' - `status`: The exit status of the helm tool. If this is `NA`, then the process was killed and had no exit status. @@ -214,11 +231,13 @@ call_helm <- function(cmd="", config=NULL, ..., echo=getOption("azure_containers if(!is.null(config)) config <- paste0("--kubeconfig=", config) - message("Helm operation: ", cmd, " ", config) + + if(length(cmd) == 1 && grepl(" ", cmd, fixed=TRUE)) + cmd <- strsplit(cmd, "\\s+")[[1]] echo <- as.logical(echo) - val <- processx::run(.AzureContainers$helm, c(strsplit(cmd, " ", fixed=TRUE)[[1]], config), ..., echo=echo) - val$cmdline <- paste("helm", cmd, config) + val <- processx::run(.AzureContainers$helm, c(cmd, config), ..., echo=echo) + val$cmdline <- paste("helm", paste(cmd, collapse=" "), config) invisible(val) } diff --git a/man/call_docker.Rd b/man/call_docker.Rd index 2480c2e..c9b9f3d 100644 --- a/man/call_docker.Rd +++ b/man/call_docker.Rd @@ -8,7 +8,7 @@ call_docker(cmd = "", ..., echo = getOption("azure_containers_tool_echo", TRUE)) } \arguments{ -\item{cmd}{The docker command line to execute.} +\item{cmd}{The docker command. This should be a \emph{vector} of individual docker arguments, but can also be a single commandline string. See below.} \item{...}{Other arguments to pass to \link[processx:run]{processx::run}.} @@ -31,24 +31,8 @@ Call the docker commandline tool } \details{ This function calls the \code{docker} binary, which must be located in your search path. AzureContainers will search for the binary at package startup, and print a warning if it is not found. -} -\examples{ -\dontrun{ -# without any args, prints the docker help screen -call_docker() - -# build an image -call_docker("build -t myimage .") - -# list running containers -call_docker("container ls") - -# prune unused containers and images -call_docker("container prune -f") -call_docker("image prune -f") - -} +The docker command should be specified as a vector of the individual arguments, which is what \code{processx::run} expects. If a single string is passed, for convenience and back-compatibility reasons \code{call_docker} will split it into arguments for you. This is prone to error, for example if you are working with pathnames that contain spaces, so it's strongly recommended to pass a vector of arguments as a general practice. } \seealso{ \link[processx:run]{processx::run}, \link{call_docker_compose}, \link{call_kubectl} for the equivalent interface to the \code{kubectl} Kubernetes tool diff --git a/man/call_docker_compose.Rd b/man/call_docker_compose.Rd index 7ec360e..ed9c434 100644 --- a/man/call_docker_compose.Rd +++ b/man/call_docker_compose.Rd @@ -8,7 +8,7 @@ call_docker_compose(cmd = "", ..., echo = getOption("azure_containers_tool_echo", TRUE)) } \arguments{ -\item{cmd}{The docker-compose command line to execute.} +\item{cmd}{The docker-compose command line to execute. This should be a \emph{vector} of individual docker-compose arguments, but can also be a single commandline string. See below.} \item{...}{Other arguments to pass to \link[processx:run]{processx::run}.} @@ -31,6 +31,8 @@ Call the docker-compose commandline tool } \details{ This function calls the \code{docker-compose} binary, which must be located in your search path. AzureContainers will search for the binary at package startup, and print a warning if it is not found. + +The docker-compose command should be specified as a vector of the individual arguments, which is what \code{processx::run} expects. If a single string is passed, for convenience and back-compatibility reasons \code{call_docker_compose} will split it into arguments for you. This is prone to error, for example if you are working with pathnames that contain spaces, so it's strongly recommended to pass a vector of arguments as a general practice. } \seealso{ \link[processx:run]{processx::run}, \link{call_docker}, \link{call_kubectl} for the equivalent interface to the \code{kubectl} Kubernetes tool diff --git a/man/call_helm.Rd b/man/call_helm.Rd index 073bb05..f229e65 100644 --- a/man/call_helm.Rd +++ b/man/call_helm.Rd @@ -8,7 +8,7 @@ call_helm(cmd = "", config = NULL, ..., echo = getOption("azure_containers_tool_echo", TRUE)) } \arguments{ -\item{cmd}{The Helm command line to execute.} +\item{cmd}{The Helm command line to execute. This should be a \emph{vector} of individual helm arguments, but can also be a single commandline string. See below.} \item{config}{The pathname of the cluster config file, if required.} @@ -33,6 +33,8 @@ Call the Helm commandline tool } \details{ This function calls the \code{helm} binary, which must be located in your search path. AzureContainers will search for the binary at package startup, and print a warning if it is not found. + +The helm command should be specified as a vector of the individual arguments, which is what \code{processx::run} expects. If a single string is passed, for convenience and back-compatibility reasons \code{call_docker_compose} will split it into arguments for you. This is prone to error, for example if you are working with pathnames that contain spaces, so it's strongly recommended to pass a vector of arguments as a general practice. } \seealso{ \link[processx:run]{processx::run}, \link{call_docker}, \link{call_kubectl} diff --git a/man/call_kubectl.Rd b/man/call_kubectl.Rd index 96a434e..6efe1f4 100644 --- a/man/call_kubectl.Rd +++ b/man/call_kubectl.Rd @@ -8,7 +8,7 @@ call_kubectl(cmd = "", config = NULL, ..., echo = getOption("azure_containers_tool_echo", TRUE)) } \arguments{ -\item{cmd}{The kubectl command line to execute.} +\item{cmd}{The kubectl command line to execute. This should be a \emph{vector} of individual kubectl arguments, but can also be a single commandline string. See below.} \item{config}{The pathname of the cluster config file, if required.} @@ -33,6 +33,8 @@ Call the Kubernetes commandline tool, kubectl } \details{ This function calls the \code{kubectl} binary, which must be located in your search path. AzureContainers will search for the binary at package startup, and print a warning if it is not found. + +The kubectl command should be specified as a vector of the individual arguments, which is what \code{processx::run} expects. If a single string is passed, for convenience and back-compatibility reasons \code{call_docker_compose} will split it into arguments for you. This is prone to error, for example if you are working with pathnames that contain spaces, so it's strongly recommended to pass a vector of arguments as a general practice. } \examples{ \dontrun{ diff --git a/tests/testthat/test00_tools.R b/tests/testthat/test00_tools.R new file mode 100644 index 0000000..a77f5d9 --- /dev/null +++ b/tests/testthat/test00_tools.R @@ -0,0 +1,39 @@ +context("Tools interface") + +if(.AzureContainers$docker == "" || + .AzureContainers$dockercompose == "" || + .AzureContainers$kubectl == "" || + .AzureContainers$helm == "") + skip("Tests skipped: external tools not found") + +test_that("Docker works", +{ + cmd <- "--help" + obj <- call_docker(cmd) + expect_is(obj, "list") + expect_identical(obj$cmdline, "docker --help") +}) + +test_that("Docker compose works", +{ + cmd <- "--help" + obj <- call_docker_compose(cmd) + expect_is(obj, "list") + expect_identical(obj$cmdline, "docker-compose --help") +}) + +test_that("Kubectl works", +{ + cmd <- "--help" + obj <- call_kubectl(cmd) + expect_is(obj, "list") + expect_identical(trimws(obj$cmdline), "kubectl --help") +}) + +test_that("Helm works", +{ + cmd <- "--help" + obj <- call_helm(cmd) + expect_is(obj, "list") + expect_identical(trimws(obj$cmdline), "helm --help") +}) diff --git a/vignettes/vig03_securing_aks_traefik.Rmd b/vignettes/vig03_securing_aks_traefik.Rmd index f8a71fd..37baca4 100644 --- a/vignettes/vig03_securing_aks_traefik.Rmd +++ b/vignettes/vig03_securing_aks_traefik.Rmd @@ -321,7 +321,5 @@ httr::content(response, simplifyVector=TRUE) ## Further comments -In this vignette, we've used Let's Encrypt to obtain a TLS certificate, as it is a convenient and free source of certificates. In a production setting you would generally use a certificate issued to your organisation. - -Similarly, we've secured the predictive service with a single username and password. While we could add more users to the authentication file, a more flexible and scalable solution is to use a frontend service, such as [Azure API Management](https://azure.microsoft.com/services/api-management/), or a directory service like [Azure Active Directory](https://azure.microsoft.com/services/active-directory/). The specifics will typically be determined by your organisation's IT infrastructure, and are beyond the scope of this vignette. +In this vignette, we've secured the predictive service with a single username and password. While we could add more users to the authentication file, a more flexible and scalable solution is to use a frontend service, such as [Azure API Management](https://azure.microsoft.com/services/api-management/), or a directory service like [Azure Active Directory](https://azure.microsoft.com/services/active-directory/). The specifics will typically be determined by your organisation's IT infrastructure, and are beyond the scope of this vignette.