diff --git a/src/Farmer/Arm/Web.fs b/src/Farmer/Arm/Web.fs index 2cdd1f7ac..4648fa1a3 100644 --- a/src/Farmer/Arm/Web.fs +++ b/src/Farmer/Arm/Web.fs @@ -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 = @@ -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 |} |} diff --git a/src/Farmer/Builders/Builders.WebApp.fs b/src/Farmer/Builders/Builders.WebApp.fs index 11f1b6534..a6650ed6f 100644 --- a/src/Farmer/Builders/Builders.WebApp.fs +++ b/src/Farmer/Builders/Builders.WebApp.fs @@ -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 = @@ -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) } diff --git a/src/Farmer/Farmer.fsproj b/src/Farmer/Farmer.fsproj index 725f4cd0d..77d124010 100644 --- a/src/Farmer/Farmer.fsproj +++ b/src/Farmer/Farmer.fsproj @@ -2,7 +2,7 @@ Farmer - 1.6.2 + 1.6.3 Farmer makes authoring ARM templates easy! Copyright 2019-2022 Compositional IT Ltd. Compositional IT diff --git a/src/Farmer/Types.fs b/src/Farmer/Types.fs index 4d5361eb9..8b6353ab8 100644 --- a/src/Farmer/Types.fs +++ b/src/Farmer/Types.fs @@ -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 diff --git a/src/Tests/WebApp.fs b/src/Tests/WebApp.fs index 7a2851e31..0fe218493 100644 --- a/src/Tests/WebApp.fs +++ b/src/Tests/WebApp.fs @@ -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 |> List.head + let nested = resources |> getResource + let expectedDomainName = "customDomain.io" + + // Testing HostnameBinding + let hostnameBinding = + nested.[0].Resources |> getResource |> 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 |> 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 |> 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 |> 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"