Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions RELEASE_NOTES.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
Release Notes
=============

## Unreleased
* Computation Expressions: Added `Zero`, `Combine`, `Delay`, `Run`, and `For` methods to `DeploymentBuilder` to enable control flow constructs in the `arm` builder computation expression.
* This allows for more flexible deployment composition using F#'s computation expression features
* The existing `output` method overloads for `Option` types (`string option` and `ArmExpression option`) can now be used more naturally
* Users can use copy-and-update patterns on deployment records for conditional composition
* Example: `output "vmIP" (myVm.PublicIpId |> Option.map (fun ip -> ip.ArmExpression))` - [link to PR]

## 1.9.25
* Service Bus: Support for minimum TLS version of 1.3.
* Storage Accounts: Support for requesting minimum TLS version of 1.3. The ARM resource itself currently falls back to 1.2.
Expand Down
51 changes: 50 additions & 1 deletion src/Farmer/Builders/Builders.ResourceGroup.fs
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ type ResourceGroupConfig = {
]

type DeploymentBuilder() =
member _.Yield _ = {
static member private EmptyState() = {
TargetResourceGroup = None
DeploymentName = AutoGeneratable.AutoGenerate()
Dependencies = Set.empty
Expand All @@ -144,6 +144,55 @@ type DeploymentBuilder() =
Tags = Map.empty
}

member _.Yield _ = DeploymentBuilder.EmptyState()

/// Returns empty state for if-then expressions without else branches.
/// Note: Custom operations (marked with [<CustomOperation>]) cannot be used inside control flow (if/match/for/while/try).
/// This is F# error FS3086. Use Option type overloads instead: output "key" (someOption |> Option.map ...)
member _.Zero() = DeploymentBuilder.EmptyState()

/// Combines two deployment states together.
/// Note: This enables control flow, but custom operations (like 'output', 'add_resource') still cannot be used inside.
/// Unlike seq/async which use yield/let! (not custom ops), Farmer operations are custom operations (error FS3086).
member _.Combine(state1: ResourceGroupConfig, state2: ResourceGroupConfig) = {
TargetResourceGroup = state2.TargetResourceGroup |> Option.orElse state1.TargetResourceGroup
DeploymentName =
match state2.DeploymentName with
| AutoGeneratedValue { contents = Some _ } -> state2.DeploymentName
| AutoGeneratedValue { contents = None } -> state1.DeploymentName
| FixedValue _ -> state2.DeploymentName
Dependencies = state1.Dependencies + state2.Dependencies
Parameters = state1.Parameters + state2.Parameters
Outputs = Map.merge (Map.toList state1.Outputs) state2.Outputs
Resources =
state1.Resources @ state2.Resources
|> List.distinctBy (fun r -> r.ResourceId, r.GetType().Name)
ParameterValues = state1.ParameterValues @ state2.ParameterValues
SubscriptionId = state2.SubscriptionId |> Option.orElse state1.SubscriptionId
Location =
match state2.Location with
| LocationExpression _ when state2.Location = Location.ResourceGroup -> state1.Location
| _ -> state2.Location
Mode = state2.Mode
Tags = Map.merge (Map.toList state1.Tags) state2.Tags
}

/// Delays computation for proper evaluation order.
member _.Delay(f: unit -> ResourceGroupConfig) = f

/// Runs the delayed computation.
member _.Run(f: unit -> ResourceGroupConfig) = f ()

/// Enables for loops over sequences.
/// Note: Custom operations cannot be used inside the loop body (F# error FS3086 for custom operations in control flow).
member this.For(sequence: seq<'T>, body: 'T -> ResourceGroupConfig) =
let mutable state = this.Zero()

for item in sequence do
state <- this.Combine(state, body item)

state

/// Creates an output value that will be returned by the ARM template.
[<CustomOperation "output">]
member _.Output(state, outputName, outputValue) : ResourceGroupConfig = {
Expand Down
76 changes: 76 additions & 0 deletions src/Tests/Template.fs
Original file line number Diff line number Diff line change
Expand Up @@ -559,4 +559,80 @@ let tests =
expected
"Parameter 'vault-secret-name' is incorrect."
}

test "Can use output with string Option type (None case)" {
let optionalValue: string option = None

let template =
arm {
location Location.NorthEurope
output "conditionalOutput" optionalValue
}
|> toTemplate

Expect.isEmpty template.outputs "Should have no outputs when value is None"
}

test "Can use output with string Option type (Some case)" {
let optionalValue = Some "test-value"

let template =
arm {
location Location.NorthEurope
output "conditionalOutput" optionalValue
}
|> toTemplate

Expect.equal template.outputs.Count 1 "Should have one output"
Expect.equal template.outputs.["conditionalOutput"].value "test-value" ""
}

test "Can use output with ArmExpression Option type (None)" {
let optionalExpr: ArmExpression option = None

let template =
arm {
location Location.NorthEurope
output "conditionalExpr" optionalExpr
}
|> toTemplate

Expect.isEmpty template.outputs "Should have no outputs when expression is None"
}

test "Can use output with ArmExpression Option type (Some)" {
let optionalExpr = Some(ArmExpression.create "resourceGroup().location")

let template =
arm {
location Location.NorthEurope
output "conditionalExpr" optionalExpr
}
|> toTemplate

Expect.equal template.outputs.Count 1 "Should have one output"
Expect.equal template.outputs.["conditionalExpr"].value "[resourceGroup().location]" ""
}

test "Copy-and-update pattern enables conditional deployment composition" {
let includeOutput = true

let baseDeployment = arm {
location Location.NorthEurope
add_resource (storageAccount { name "storage1" })
}

let deployment =
if includeOutput then
{
baseDeployment with
Outputs = baseDeployment.Outputs.Add("testOutput", "testValue")
}
else
baseDeployment

if includeOutput then
Expect.equal deployment.Outputs.Count 1 "Should have one output"
Expect.isTrue (deployment.Outputs.ContainsKey "testOutput") "Should have testOutput"
}
]