Skip to content

Commit

Permalink
template build helpers
Browse files Browse the repository at this point in the history
  • Loading branch information
hongooi73 committed Jun 1, 2019
1 parent eff6858 commit 417db35
Show file tree
Hide file tree
Showing 13 changed files with 490 additions and 43 deletions.
6 changes: 3 additions & 3 deletions DESCRIPTION
@@ -1,12 +1,12 @@
Package: AzureRMR
Title: Interface to 'Azure Resource Manager'
Version: 2.1.1.9000
Version: 2.1.2
Authors@R: c(
person("Hong", "Ooi", , "hongooi@microsoft.com", role = c("aut", "cre")),
person("Microsoft", role="cph")
)
Description: A lightweight but powerful R interface to the 'Azure Resource Manager' REST API. The package exposes classes and methods for 'OAuth' authentication and working with subscriptions and resource groups. It also provides functionality for creating and deleting 'Azure' resources and deploying templates. While 'AzureRMR' can be used to manage any 'Azure' service, it can also be extended by other packages to provide extra functionality for specific services.
URL: https://github.com/Azure/AzureRMR
Description: A lightweight but powerful R interface to the 'Azure Resource Manager' REST API. The package exposes classes and methods for 'OAuth' authentication and working with subscriptions and resource groups. It also provides functionality for creating and deleting 'Azure' resources and deploying templates. While 'AzureRMR' can be used to manage any 'Azure' service, it can also be extended by other packages to provide extra functionality for specific services. Part of the 'AzureR' family of packages.
URL: https://github.com/Azure/AzureRMR https://github.com/Azure/AzureR
BugReports: https://github.com/Azure/AzureRMR/issues
License: MIT + file LICENSE
VignetteBuilder: knitr
Expand Down
4 changes: 4 additions & 0 deletions NAMESPACE
@@ -1,5 +1,7 @@
# Generated by roxygen2: do not edit by hand

S3method(build_template_definition,default)
S3method(build_template_parameters,default)
export(AzureR_dir)
export(az_resource)
export(az_resource_group)
Expand All @@ -8,6 +10,8 @@ export(az_role_assignment)
export(az_role_definition)
export(az_subscription)
export(az_template)
export(build_template_definition)
export(build_template_parameters)
export(call_azure_rm)
export(call_azure_url)
export(clean_token_directory)
Expand Down
3 changes: 2 additions & 1 deletion NEWS.md
@@ -1,6 +1,7 @@
# AzureRMR 2.1.1.9000
# AzureRMR 2.1.2

- Fix a bug in template deployment where null fields were not handled correctly.
- New `build_template_definition` and `build_parameters_parameters` generics to help in template deployment. These can take as inputs R lists, JSON text strings, or file connections, and can also be extended by other packages.

# AzureRMR 2.1.1

Expand Down
44 changes: 9 additions & 35 deletions R/az_template.R
Expand Up @@ -28,8 +28,10 @@
#' - `parameters`: The parameters for the template. This can be provided using any of the same methods as the `template` argument.
#' - `wait`: Optionally, whether to wait until the deployment is complete. Defaults to FALSE, in which case the method will return immediately.
#'
#' You can use the `build_template_definition` and `build_template_parameters` helper functions to construct the inputs for deploying a template. These can take as inputs R lists, JSON text strings, or file connections, and can also be extended by other packages.
#'
#' @seealso
#' [az_resource_group], [az_resource],
#' [az_resource_group], [az_resource], [build_template_definition], [build_template_parameters]
#' [Template overview](https://docs.microsoft.com/en-us/azure/templates/),
#' [Template API reference](https://docs.microsoft.com/en-us/rest/api/resources/deployments)
#'
Expand Down Expand Up @@ -200,20 +202,24 @@ private=list(
# fold template data into properties
properties <- if(is.list(template))
append_json(properties, template=generate_json(template))
else if(is_file_spec(template))
append_json(properties, template=readLines(template))
else if(is_url(template))
append_json(properties, templateLink=generate_json(list(uri=template)))
else append_json(properties, template=template)

# handle case of missing or empty parameters arg
# must be a _named_ list for jsonlite to turn into an object, not an array
if(missing(parameters) || is_empty(parameters))
parameters <- structure(list(), names=character(0))
parameters <- named_list()

# fold parameter data into properties
properties <- if(is_empty(parameters))
append_json(properties, parameters=generate_json(parameters))
else if(is.list(parameters))
append_json(properties, parameters=generate_json(private$make_param_list(parameters)))
append_json(properties, parameters=do.call(build_template_parameters, parameters))
else if(is_file_spec(parameters))
append_json(properties, parameters=readLines(parameters))
else if(is_url(parameters))
append_json(properties, parametersLink=generate_json(list(uri=parameters)))
else append_json(properties, parameters=parameters)
Expand Down Expand Up @@ -279,12 +285,6 @@ private=list(
}
},

# params for templates require lists of (value=x) rather than vectors as inputs
make_param_list=function(params)
{
lapply(params, function(x) if(is.list(x)) x else list(value=x))
},

tpl_op=function(op="", ...)
{
op <- construct_path("resourcegroups", self$resource_group,
Expand All @@ -293,29 +293,3 @@ private=list(
}
))


generate_json <- function(object)
{
jsonlite::toJSON(object, pretty=TRUE, auto_unbox=TRUE, null="null", digits=22)
}


append_json <- function(props, ...)
{
lst <- list(...)
lst_names <- names(lst)
if(is.null(lst_names) || any(lst_names == ""))
stop("Deployment properties must be named", call.=FALSE)

for(i in seq_along(lst))
{
lst_i <- lst[[i]]
if(inherits(lst_i, "connection") || (length(lst_i) == 1 && file.exists(lst_i)))
lst_i <- readLines(lst_i)

newprop <- sprintf(', "%s": %s}', lst_names[i], paste0(lst_i, collapse="\n"))
props <- sub("\\}$", newprop, props)
}

props
}
218 changes: 218 additions & 0 deletions R/build_tpl_json.R
@@ -0,0 +1,218 @@
#' Build the JSON for a template and its parameters
#'
#' @param ... For `build_template_parameters`, named arguments giving the values of each template parameter. For `build_template_definition`, further arguments passed to class methods.
#' @param parameters For `build_template_definition`, the parameter names and types for the template. See 'Details' below.
#' @param variables Internal variables used by the template.
#' @param resources List of resources that the template should deploy.
#' @param outputs The template outputs.
#'
#' @details
#' `build_template_definition` is used to generate a template from its components. The arguments can be specified in various ways:
#' - As character strings containing unparsed JSON text.
#' - As an R list of (nested) objects representing the parsed JSON.
#' - A connection pointing to a JSON file or object.
#' - For the `parameters` argument, this can also be a character vector containing the types of each parameter.
#'
#' `build_template_parameters` is for creating the list of parameters to be passed along with the template. Its arguments should all be named, and contain either the JSON text or an R list giving the parsed JSON.
#'
#' Both of these are generics and can be extended by other packages to handle specific deployment scenarios, eg virtual machines.
#'
#' @return
#' The JSON text for the template definition and its parameters.
#'
#' @examples
#' # dummy example
#' # note that 'resources' arg should be a _list_ of resources
#' build_template_definition(resources=list(list(name="resource here")))
#'
#' # specifying parameters as a list
#' build_template_definition(parameters=list(par1=list(type="string")),
#' resources=list(list(name="resource here")))
#'
#' # specifying parameters as a vector
#' build_template_definition(parameters=c(par1="string"),
#' resources=list(list(name="resource here")))
#'
#' # realistic example: storage account
#' build_template_definition(
#' parameters=c(
#' name="string",
#' location="string",
#' sku="string"
#' ),
#' variables=list(
#' id="[resourceId('Microsoft.Storage/storageAccounts', parameters('name'))]"
#' ),
#' resources=list(
#' list(
#' name="[parameters('name')]",
#' location="[parameters('location')]",
#' type="Microsoft.Storage/storageAccounts",
#' apiVersion="2018-07-01",
#' sku=list(
#' name="[parameters('sku')]"
#' ),
#' kind="Storage"
#' )
#' ),
#' outputs=list(
#' storageId="[variables('id')]"
#' )
#' )
#'
#' # providing JSON text as input
#' build_template_definition(
#' parameters=c(name="string", location="string", sku="string"),
#' resources='[
#' {
#' "name": "[parameters(\'name\')]",
#' "location": "[parameters(\'location\')]",
#' "type": "Microsoft.Storage/storageAccounts",
#' "apiVersion": "2018-07-01",
#' "sku": {
#' "name": "[parameters(\'sku\')]"
#' },
#' "kind": "Storage"
#' }
#' ]'
#' )
#'
#' # parameter values
#' build_template_parameters(name="mystorageacct", location="westus", sku="Standard_LRS")
#'
#' build_template_parameters(
#' param='{
#' "name": "myname",
#' "properties": { "prop1": 42, "prop2": "hello" }
#' }'
#' )
#'
#' param_json <- '{
#' "name": "myname",
#' "properties": { "prop1": 42, "prop2": "hello" }
#' }'
#' build_template_parameters(param=textConnection(param_json))
#'
#' \dontrun{
#' # reading JSON definitions from a file
#' build_template_definition(
#' parameters=file("parameter_def.json"),
#' resources=file("resource_def.json")
#'
#' build_template_parameters(name="myres_name", complex_type=file("myres_params.json"))
#' )
#' }
#'
#' @rdname build_template
#' @aliases build_template
#' @export
build_template_definition <- function(...)
{
UseMethod("build_template_definition")
}


#' @rdname build_template
#' @export
build_template_definition.default <- function(parameters=NULL, variables=NULL, resources=NULL, outputs=NULL, ...)
{
# special treatment for parameters arg: convert 'c(name="type")' to 'list(name=list(type="type"))'
if(is.character(parameters))
parameters <- sapply(parameters, function(type) list(type=type), simplify=FALSE)

parts <- lapply(
list(parameters=parameters, variables=variables, resources=resources, outputs=outputs, ...),
function(x)
{
if(inherits(x, "connection"))
{
on.exit(close(x))
readLines(x)
}
else generate_json(if(is.null(x)) named_list() else x)
}
)
json <- generate_json(list(
`$schema`="http://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
contentVersion="1.0.0.0"
))
for(i in seq_along(parts))
json <- do.call(append_json, c(json, parts[i]))

jsonlite::prettify(json)
}


#' @rdname build_template
#' @export
build_template_parameters <- function(...)
{
UseMethod("build_template_parameters")
}


#' @rdname build_template
#' @export
build_template_parameters.default <- function(...)
{
dots <- list(...)

# handle no-parameter case
if(is_empty(dots))
return("{}")

parms <- lapply(dots, function(value)
{
# need to duplicate functionality of generate_json, one level down
if(inherits(value, "connection"))
{
on.exit(close(value))
generate_json(list(value=jsonlite::fromJSON(readLines(value), simplifyVector=FALSE)))
}
else if(is.character(value) && jsonlite::validate(value))
generate_json(list(value=jsonlite::fromJSON(value, simplifyVector=FALSE)))
else generate_json(list(value=value))
})

jsonlite::prettify(do.call(append_json, c(list("{}"), parms)))
}


generate_json <- function(object)
{
if(is.character(object) && jsonlite::validate(object))
object
else jsonlite::toJSON(object, auto_unbox=TRUE, null="null", digits=22)
}


append_json <- function(props, ...)
{
lst <- list(...)
lst_names <- names(lst)
if(is.null(lst_names) || any(lst_names == ""))
stop("Deployment properties and parameters must be named", call.=FALSE)

for(i in seq_along(lst))
{
lst_i <- lst[[i]]
if(inherits(lst_i, "connection"))
{
on.exit(close(lst_i))
lst_i <- readLines(lst_i)
}

newprop <- sprintf('"%s": %s}', lst_names[i], paste0(lst_i, collapse="\n"))
if(!grepl("^\\{[[:space:]]*\\}$", props))
newprop <- paste(",", newprop)
props <- sub("\\}$", newprop, props)
}

props
}


is_file_spec <- function(x)
{
inherits(x, "connection") || (is.character(x) && length(x) == 1 && file.exists(x))
}
4 changes: 3 additions & 1 deletion man/az_template.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 417db35

Please sign in to comment.