Skip to content
Merged
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
10 changes: 8 additions & 2 deletions src/Farmer/Arm/Web.fs
Original file line number Diff line number Diff line change
Expand Up @@ -603,9 +603,13 @@ type Certificate =
SiteId: LinkedResource
ServicePlanId: LinkedResource
DomainName: string
KeyVaultId: string option
KeyVaultSecretName: string option
}

member this.ResourceName = ResourceName this.DomainName
// Use the domain name for the certificate resource name if this is an azure managed cert, else it needs to be the keyvault secret name to avoid conflicts when using the same keyvault cert across multiple apps.
// This hack ensures only one cert is created per resource group when using a keyvault certificate, even if you have multiple farmer web apps.
member this.ResourceName = if this.KeyVaultSecretName.IsSome then ResourceName this.KeyVaultSecretName.Value else ResourceName this.DomainName
member this.Thumbprint = this.GetThumbprintReference None

member this.GetThumbprintReference certificateResourceGroup =
Expand Down Expand Up @@ -636,7 +640,9 @@ type Certificate =
properties =
{|
serverFarmId = this.ServicePlanId.ResourceId.Eval()
canonicalName = this.DomainName
canonicalName = if this.KeyVaultId.IsSome then None else Some(this.DomainName)
keyVaultId = this.KeyVaultId
keyVaultSecretName = this.KeyVaultSecretName
|}
|}

Expand Down
15 changes: 12 additions & 3 deletions src/Farmer/Builders/Builders.WebApp.fs
Original file line number Diff line number Diff line change
Expand Up @@ -914,13 +914,20 @@ type WebAppConfig =

match customDomain with
| SecureDomain (customDomain, certOptions) ->
let cert =
{
let cert = {
Location = location
SiteId = Managed this.ResourceId
ServicePlanId = Managed this.ServicePlanId
DomainName = customDomain
}
KeyVaultId =
match certOptions with
| CustomCertificateFromKeyVault kvCert -> Some(kvCert.keyVaultId)
| _ -> None
KeyVaultSecretName =
match certOptions with
| CustomCertificateFromKeyVault kvCert -> Some(kvCert.keyVaultSecretName)
| _ -> None
}

// Get the resource group which contains the app service plan
let aspRgName =
Expand Down Expand Up @@ -963,6 +970,8 @@ type WebAppConfig =
match certOptions with
| AppManagedCertificate ->
Some(SniBased(cert.GetThumbprintReference aspRgName))
| CustomCertificateFromKeyVault kvCert ->
Some(SniBased(cert.GetThumbprintReference aspRgName))
| CustomCertificate thumbprint -> Some(SniBased thumbprint)
}

Expand Down
2 changes: 1 addition & 1 deletion src/Farmer/Farmer.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<PropertyGroup>
<!-- General -->
<AssemblyName>Farmer</AssemblyName>
<Version>1.6.2</Version>
<Version>1.6.3</Version>
<Description>Farmer makes authoring ARM templates easy!</Description>
<Copyright>Copyright 2019-2022 Compositional IT Ltd.</Copyright>
<Company>Compositional IT</Company>
Expand Down
6 changes: 6 additions & 0 deletions src/Farmer/Types.fs
Original file line number Diff line number Diff line change
Expand Up @@ -365,10 +365,16 @@ type ResourceRef<'TConfig> =
| LinkedResource r -> r
| AutoGeneratedResource r -> Managed(r.resourceId config)

type KeyVaultCertificate = {
keyVaultId: string
keyVaultSecretName: string
}

//Choose whether you'd like an auto generated app service managed certificate or if you have your own custom certificate of
type CertificateOptions =
| AppManagedCertificate
| CustomCertificate of thumbprint: ArmExpression
| CustomCertificateFromKeyVault of keyVaultCertificate: KeyVaultCertificate

type DomainConfig =
| SecureDomain of domain: string * cert: CertificateOptions
Expand Down
94 changes: 94 additions & 0 deletions src/Tests/WebApp.fs
Original file line number Diff line number Diff line change
Expand Up @@ -1228,6 +1228,100 @@ let tests =
$"hostnameBinding should have a {innerExpectedSslState} Ssl state inside the resourceGroupDeployment template"
}

test "Supports secure custom domains with custom certificate from Key Vault" {
let webappName = "test"

let keyVaultId = "keyVaultId"
let keyVaultSecretName = "keyVaultSecretName"

let keyVaultCertificate: KeyVaultCertificate = {
keyVaultId = keyVaultId
keyVaultSecretName = keyVaultSecretName
}

let domainConfig = SecureDomain("customDomain.io", CustomCertificateFromKeyVault keyVaultCertificate)

let resources =
webApp {
name webappName
custom_domain domainConfig
}
|> getResources

let wa = resources |> getResource<Web.Site> |> List.head
let nested = resources |> getResource<ResourceGroupDeployment>
let expectedDomainName = "customDomain.io"

// Testing HostnameBinding
let hostnameBinding =
nested.[0].Resources |> getResource<Web.HostNameBinding> |> List.head

let expectedSslState = None
let exepectedSiteId = (Managed(Arm.Web.sites.resourceId wa.Name))

Expect.equal
hostnameBinding.DomainName
expectedDomainName
$"HostnameBinding domain name should have {expectedDomainName}"

Expect.equal
hostnameBinding.SslState
expectedSslState
$"HostnameBinding should have a {expectedSslState} Ssl state"

Expect.equal
hostnameBinding.SiteId
exepectedSiteId
$"HostnameBinding SiteId should be {exepectedSiteId}"

// Testing certificate
let cert = nested.[1].Resources |> getResource<Web.Certificate> |> List.head

Expect.equal
cert.KeyVaultId.Value
keyVaultId
$"Certificate should be in KeyVault {keyVaultId}"

Expect.equal
cert.KeyVaultSecretName.Value
keyVaultSecretName
$"Certificate should be in KeyVault secret {keyVaultSecretName}"

// Testing certificate
let cert = nested.[1].Resources |> getResource<Web.Certificate> |> List.head

Expect.equal
cert.DomainName
expectedDomainName
$"Certificate domain name should have {expectedDomainName}"

// Testing hostname/certificate link.
let bindingDeployment = nested.[2]

let innerResource =
bindingDeployment.Resources |> getResource<Web.HostNameBinding> |> List.head

Expect.stringStarts
bindingDeployment.DeploymentName.Value
"[concat"
"resourceGroupDeployment name should start as a valid ARM expression"

Expect.stringEnds
bindingDeployment.DeploymentName.Value
")]"
"resourceGroupDeployment stage should end as a valid ARM expression"

Expect.equal
bindingDeployment.Resources.Length
1
"resourceGroupDeployment stage should only contain one resource"

Expect.equal
bindingDeployment.Dependencies.Count
1
"resourceGroupDeployment stage should only contain one dependencies"
}

test "Supports secure custom domains with app service managed certificate" {
let webappName = "test"

Expand Down
Loading