Skip to content

Commit

Permalink
Provide input parameters and key vault references to nested deploymen…
Browse files Browse the repository at this point in the history
…ts. (#788)
  • Loading branch information
ninjarobot authored Oct 12, 2021
1 parent 30d75f2 commit 70b0fb0
Show file tree
Hide file tree
Showing 6 changed files with 140 additions and 10 deletions.
4 changes: 3 additions & 1 deletion RELEASE_NOTES.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
Release Notes
=============
## vNext

## 1.6.18
* Resource Groups: Add support for multiple nested deployments targetting the same resource group
* Resource Groups: Provide input parameters and key vault references to nested deployments.

## 1.6.17
* Container Groups: Use an ARM expression to populate a secure environment variable.
Expand Down
2 changes: 2 additions & 0 deletions docs/content/api-overview/resources/resource-group.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ The Resource Group builder is always the top-level element of your deployment. I
| add_resource | Adds a resource to the template. |
| add_resources | Adds a collection of resources to the template. |
| add_arm_resources | Adds a collection of lower-level IArmResources to the template. |
| add_parameter_values | Adds a collection of parameter values to pass to the nested deployment's parameters. |
| add_secret_references | Adds a collection of key vault secret references to pass to the nested deployment's parameters. |
| output | Creates an output value that will be returned by the ARM template. Since Farmer does not require variables, and the only parameters supported are secure strings, these will typically be an ARM expressions that are generated at deployment-time, such as the publishing password of a web app or the fully-qualified domain name of a SQL instance etc. |
| outputs | Create multiple outputs. |
| add_tag | Add a tag to the resource group for top-level instances or to the deployment for nested resource groups |
Expand Down
50 changes: 41 additions & 9 deletions src/Farmer/Arm/ResourceGroup.fs
Original file line number Diff line number Diff line change
@@ -1,16 +1,37 @@
[<AutoOpen>]
module Farmer.Arm.ResourceGroup

open System.Collections.Generic
open Farmer
open Farmer.Writer
open System.Threading

let resourceGroupDeployment = ResourceType ("Microsoft.Resources/deployments","2020-10-01")
let resourceGroups = ResourceType ("Microsoft.Resources/resourceGroups", "2021-04-01")


type DeploymentMode = Incremental|Complete

type ParameterValue =
| ParameterValue of Name:string * Value:string
| KeyVaultReference of Name:string * KeyVaultResourceId:ResourceId * SecretName:string
/// Gets the key for this parameter in the nested deployment's 'parameters' dictionary.
member this.Key =
match this with
| ParameterValue (name, _) -> name
| KeyVaultReference (name, _, _) -> name
/// A parameter key value pair contains parameters objects of arbitrary structure to be
/// serialized as JSON for template parameters.
member internal this.ParamValue : IDictionary<string,obj> =
match this with
| ParameterValue (_, value) ->
dict [ "value", box value ]
| KeyVaultReference (_, kvResId, secretName) ->
dict [ "reference",
dict [
"keyVault", dict [ "id", kvResId.Eval() ] |> box
"secretName", secretName |> box
] |> box
]

/// Represents all configuration information to generate an ARM template.
type ResourceGroupDeployment =
{ TargetResourceGroup: ResourceName
Expand All @@ -19,11 +40,14 @@ type ResourceGroupDeployment =
Outputs : Map<string, string>
Location : Location
Resources : IArmResource list
/// Parameters provided to the deployment
ParameterValues : ParameterValue list
SubscriptionId : System.Guid option
Mode: DeploymentMode
Tags: Map<string,string> }
member this.ResourceId = resourceGroupDeployment.resourceId this.DeploymentName
member this.Parameters =
/// Secure parameters for resources defined in this template.
member this.Parameters =
[ for resource in this.Resources do
match resource with
| :? IParameters as p -> yield! p.SecureParameters
Expand All @@ -41,8 +65,12 @@ type ResourceGroupDeployment =
{ Parameters = this.Parameters
Outputs = this.Outputs |> Map.toList
Resources = this.Resources }
interface IParameters with
member this.SecureParameters = this.Parameters
/// Parameters to be emitted by the outer deployment to be passed to this deployment
interface IParameters with
/// Secure parameters that are not provided as input on this deployment
member this.SecureParameters =
let inputParameterKeys = this.ParameterValues |> Seq.map (fun p -> p.Key) |> Set
this.Parameters |> List.where(fun p -> inputParameterKeys |> Set.contains p.Key |> not)
interface IArmResource with
member this.ResourceId = this.ResourceId
member this.JsonModel =
Expand All @@ -52,10 +80,14 @@ type ResourceGroupDeployment =
subscriptionId = this.SubscriptionId |> Option.map string<System.Guid> |> Option.toObj
properties =
{| template = TemplateGeneration.processTemplate this.Template
parameters =
this.Parameters
|> Seq.map(fun (SecureParameter s) -> s, {| ``value`` = $"[parameters('%s{s}')]" |})
|> Map.ofSeq
parameters =
let parameters = Dictionary<string,obj>()
for secureParam in this.Parameters do
parameters.Add (secureParam.Key, dict ["value", $"[parameters('%s{secureParam.Value}')]"] )
// Let input params be used to satisfy parameters emitted for resources in the template
for inputParam in this.ParameterValues do
parameters.[inputParam.Key] <- inputParam.ParamValue
parameters
mode =
match this.Mode with
| Incremental -> "Incremental"
Expand Down
9 changes: 9 additions & 0 deletions src/Farmer/Builders/Builders.ResourceGroup.fs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ type ResourceGroupConfig =
Outputs : Map<string, string>
Location : Location
Resources : IArmResource list
ParameterValues : ParameterValue list
SubscriptionId : System.Guid option
Mode: DeploymentMode
Tags: Map<string,string> }
Expand Down Expand Up @@ -69,6 +70,7 @@ type ResourceGroupConfig =
Outputs = Map.merge (Map.toList this.Outputs) innerOutputs // New values overwrite old values so supply this.Outputs as newValues
Location = this.Location
Resources = this.Resources
ParameterValues = this.ParameterValues
SubscriptionId = this.SubscriptionId
Mode = this.Mode
Tags = this.Tags }
Expand Down Expand Up @@ -108,6 +110,7 @@ type DeploymentBuilder() =
Parameters = Set.empty
Outputs = Map.empty
Resources = List.empty
ParameterValues = List.empty
SubscriptionId = None
Location = Location.WestEurope
Mode = Incremental
Expand Down Expand Up @@ -186,6 +189,12 @@ type ResourceGroupBuilder () =
{ state with SubscriptionId = Some subscriptionId }
member this.SubscriptionId(state:ResourceGroupConfig, subscriptionId:string) =
{ state with SubscriptionId = Some (System.Guid subscriptionId) }
[<CustomOperation "add_parameter_values">]
member this.AddParameterValues(state:ResourceGroupConfig, parameters:(string*string) list) =
{ state with ParameterValues = state.ParameterValues @ (parameters |> List.map ParameterValue) }
[<CustomOperation "add_secret_references">]
member this.AddKeyVaultSecretReferences(state:ResourceGroupConfig, parameters:(string*ResourceId*string) list) =
{ state with ParameterValues = state.ParameterValues @ (parameters |> List.map KeyVaultReference) }

let resourceGroup = ResourceGroupBuilder()

Expand Down
2 changes: 2 additions & 0 deletions src/Farmer/Types.fs
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,8 @@ type SecureParameter =
member this.Value = match this with SecureParameter value -> value
/// Gets an ARM expression reference to the parameter e.g. parameters('my-password')
member this.ArmExpression = $"parameters('{this.Value}')" |> ArmExpression.create
/// Gets the key for this parameter in the ARM template 'parameters' dictionary.
member this.Key = match this with SecureParameter name -> name

/// Exposes parameters which are required by a specific IArmResource.
type IParameters =
Expand Down
83 changes: 83 additions & 0 deletions src/Tests/Template.fs
Original file line number Diff line number Diff line change
Expand Up @@ -288,4 +288,87 @@ let tests = testList "Template" [
let actual = jobj.SelectToken("$.resources[?(@.name=='inner1-deployment')].subscriptionId") |> string
Expect.equal actual "0c3054fb-f576-4458-acff-f2c29c4123e4" "Nested deployment didn't have correct subscriptionId"
}
test "Simple parameter serializes correctly for nested deployment" {
let p1 = ParameterValue(Name="param1", Value="value1")
let expectedP1 = """{
"param1": {
"value": "value1"
}
}"""
let p1Json = dict [ p1.Key, p1.ParamValue ] |> Serialization.toJson
Expect.equal p1Json expectedP1 "p1 didn't serialize correctly"
}
test "Key Vault reference parameter serializes correctly for nested deployment" {
let kvRef1 = KeyVaultReference("param1", vaults.resourceId "myvault", "secret1")
let kvRef1Json = dict [ kvRef1.Key, kvRef1.ParamValue ] |> Serialization.toJson
let expected = """{
"param1": {
"reference": {
"keyVault": {
"id": "[resourceId('Microsoft.KeyVault/vaults', 'myvault')]"
},
"secretName": "secret1"
}
}
}"""
Expect.equal kvRef1Json expected "Key vault reference parameter didn't serialize correctly"
}
test "Can add simple parameters to nested deployment" {
let inner1 = resourceGroup {
name "inner1"
add_resources [
vm { name "vm"; username "foo" }
]
add_parameter_values [
"param1", "value1"
"param2", "value2"
]
}
let outer = arm {
add_resource inner1
}

let deployment = outer |> findAzureResources<Models.Deployment> dummyClient.SerializationSettings
let nestedParamsObj = deployment.[0].Properties.Parameters :?> JObject
let nestedParams =
nestedParamsObj.Properties()
|> Seq.map (fun x -> x.Name, x.Value.SelectToken(".value").ToString())
|> Map.ofSeq

Expect.equal nestedParams.["param1"] "value1" "Parameter 'param1' not passed to nested template"
Expect.equal nestedParams.["param2"] "value2" "Parameters 'param2' not passed to nested template"
}
test "Can add key vault reference parameters to nested deployment" {
let inner1 = resourceGroup {
name "farmer-nested-params"
add_resources [
vm { name "vm"; username "foo" }
]
add_secret_references [
"password-for-vm", vaults.resourceId "myvault", "vm-password"
]
}
let outer = arm {
add_resource inner1
}

let deployment = outer |> findAzureResources<Models.Deployment> dummyClient.SerializationSettings
let nestedParamsObj = deployment.[0].Properties.Parameters :?> JObject
let nestedParams =
nestedParamsObj.Properties()
|> Seq.map (fun x -> x.Name, x.Value.SelectToken(".reference").ToString())
|> Map.ofSeq

let expected = """{
"keyVault": {
"id": "[resourceId('Microsoft.KeyVault/vaults', 'myvault')]"
},
"secretName": "vm-password"
}"""
Expect.equal nestedParams.["password-for-vm"] expected "Parameter 'password-for-vm' keyvault reference incorrect in nested template."
let fullTemplate = outer.Template |> Writer.toJson
let jobjTemplate = JObject.Parse fullTemplate
let parametersJson = jobjTemplate.SelectToken("$.parameters") |> string<JToken>
Expect.equal parametersJson "{}" "Outer template should not have parameter that is passed to inner template"
}
]

0 comments on commit 70b0fb0

Please sign in to comment.