From f1ae6f1e13d30906d76623f2861935b2c4c756a4 Mon Sep 17 00:00:00 2001 From: Viktor Andersson Date: Sun, 5 Dec 2021 21:12:13 +0100 Subject: [PATCH 01/73] Add support for ip-restrictions for webApp/Functions. --- RELEASE_NOTES.md | 1 + .../api-overview/resources/functions.md | 2 ++ .../content/api-overview/resources/web-app.md | 2 ++ src/Farmer/Arm/Web.fs | 13 ++++++- src/Farmer/Builders/Builders.Functions.fs | 6 ++-- src/Farmer/Builders/Builders.WebApp.fs | 33 +++++++++++++++--- src/Farmer/Common.fs | 11 ++++++ src/Tests/Functions.fs | 34 +++++++++++++++++++ src/Tests/WebApp.fs | 33 ++++++++++++++++++ 9 files changed, 127 insertions(+), 8 deletions(-) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 85e4c107e..72428cfec 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -3,6 +3,7 @@ Release Notes ## 1.6.22 * WebApp: Fixed deployment name for nested template in app-managed certificate deployments +* WebApp/Functions: Add support for ip-restriction rules ## 1.6.21 * Alerts: Extend a list of possible criteria for time aggregations and operators diff --git a/docs/content/api-overview/resources/functions.md b/docs/content/api-overview/resources/functions.md index 10208fa72..35a40c68f 100644 --- a/docs/content/api-overview/resources/functions.md +++ b/docs/content/api-overview/resources/functions.md @@ -46,6 +46,8 @@ The Functions builder is used to create Azure Functions accounts. It abstracts t | add_slot | Adds a deployment slot to the app | | add_slots | Adds multiple deployment slots to the app | | health_check_path | Sets the path to your functions health check endpoint, which Azure load balancers will ping to determine which instances are healthy.| +| add_allowed_ip_restriction | Adds an 'allow' rule for an ip | +| add_denied_ip_restriction | Adds an 'deny' rule for an ip | #### Post-deployment Builder Keywords The Functions builder contains special commands that are executed *after* the ARM deployment is completed. diff --git a/docs/content/api-overview/resources/web-app.md b/docs/content/api-overview/resources/web-app.md index 28607f20d..5893187b2 100644 --- a/docs/content/api-overview/resources/web-app.md +++ b/docs/content/api-overview/resources/web-app.md @@ -57,6 +57,8 @@ The Web App builder is used to create Azure App Service accounts. It abstracts t | Web App | add_slots | Adds multiple deployment slots to the app | | Web App | health_check_path | Sets the path to your functions health check endpoint, which Azure load balancers will ping to determine which instances are healthy.| | Web App | custom_domain | Adds custom domain to the app, containing an app service managed certificate | +| Web App | add_allowed_ip_restriction | Adds an 'allow' rule for an ip | +| Web App | add_denied_ip_restriction | Adds an 'deny' rule for an ip | | Service Plan | service_plan_name | Sets the name of the service plan. If not set, uses the name of the web app postfixed with "-plan". | | Service Plan | runtime_stack | Sets the runtime stack. | | Service Plan | operating_system | Sets the operating system. If Linux, App Insights configuration settings will be omitted as they are not supported by Azure App Service. | diff --git a/src/Farmer/Arm/Web.fs b/src/Farmer/Arm/Web.fs index 03f4bc213..0374686de 100644 --- a/src/Farmer/Arm/Web.fs +++ b/src/Farmer/Arm/Web.fs @@ -196,7 +196,8 @@ type Site = Metadata : List AutoSwapSlotName: string option ZipDeployPath : (string * ZipDeploy.ZipDeployTarget * ZipDeploy.ZipDeploySlot) option - HealthCheckPath : string option } + HealthCheckPath : string option + IpSecurityRestrictions : IpSecurityRestriction list } /// Shorthand for SiteType.ResourceType member this.ResourceType = this.SiteType.ResourceType /// Shorthand for SiteType.ResourceName @@ -270,6 +271,16 @@ type Site = javaContainer = this.JavaContainer |> Option.toObj javaContainerVersion = this.JavaContainerVersion |> Option.toObj phpVersion = this.PhpVersion |> Option.toObj + ipSecurityRestrictions = + match this.IpSecurityRestrictions with + | [] -> null + | restrictions -> + restrictions + |> List.mapi (fun index restriction -> + {| ipAddress = $"%s{restriction.IpAddress.ToString()}/32" + name = restriction.Name + action = restriction.Action.ToString() + priority = index + 1 |}) |> box pythonVersion = this.PythonVersion |> Option.toObj http20Enabled = this.HTTP20Enabled |> Option.toNullable webSocketsEnabled = this.WebSocketsEnabled |> Option.toNullable diff --git a/src/Farmer/Builders/Builders.Functions.fs b/src/Farmer/Builders/Builders.Functions.fs index a9bf1db5d..20569365c 100644 --- a/src/Farmer/Builders/Builders.Functions.fs +++ b/src/Farmer/Builders/Builders.Functions.fs @@ -273,7 +273,8 @@ type FunctionsConfig = Some sc | _ -> None WorkerProcess = this.CommonWebConfig.WorkerProcess - HealthCheckPath = this.CommonWebConfig.HealthCheckPath } + HealthCheckPath = this.CommonWebConfig.HealthCheckPath + IpSecurityRestrictions = this.CommonWebConfig.IpSecurityRestrictions } match this.CommonWebConfig.ServicePlan with | DeployableResource this.Name.ResourceName resourceId -> @@ -343,7 +344,8 @@ type FunctionsBuilder() = Slots = Map.empty WorkerProcess = None ZipDeployPath = None - HealthCheckPath = None } + HealthCheckPath = None + IpSecurityRestrictions = [] } StorageAccount = derived (fun config -> let storage = config.Name.ResourceName.Map (sprintf "%sstorage") |> sanitiseStorage |> ResourceName storageAccounts.resourceId storage) diff --git a/src/Farmer/Builders/Builders.WebApp.fs b/src/Farmer/Builders/Builders.WebApp.fs index 660163d6e..7338a24ad 100644 --- a/src/Farmer/Builders/Builders.WebApp.fs +++ b/src/Farmer/Builders/Builders.WebApp.fs @@ -79,7 +79,8 @@ type SlotConfig = Identity: ManagedIdentity KeyVaultReferenceIdentity: UserAssignedIdentity option Tags: Map - Dependencies: ResourceId Set} + Dependencies: ResourceId Set + IpSecurityRestrictions: IpSecurityRestriction list } member this.ToSite (owner: Arm.Web.Site) = { owner with SiteType = SiteType.Slot (owner.Name/this.Name) @@ -88,7 +89,8 @@ type SlotConfig = AppSettings = owner.AppSettings |> Option.map (Map.merge ( this.AppSettings |> Map.toList)) ConnectionStrings = owner.ConnectionStrings |> Option.map (Map.merge (this.ConnectionStrings |> Map.toList)) Identity = this.Identity + owner.Identity - KeyVaultReferenceIdentity = this.KeyVaultReferenceIdentity |> Option.orElse owner.KeyVaultReferenceIdentity} + KeyVaultReferenceIdentity = this.KeyVaultReferenceIdentity |> Option.orElse owner.KeyVaultReferenceIdentity + IpSecurityRestrictions = this.IpSecurityRestrictions } type SlotBuilder() = member this.Yield _ = @@ -99,7 +101,8 @@ type SlotBuilder() = Identity = ManagedIdentity.Empty KeyVaultReferenceIdentity = None Tags = Map.empty - Dependencies = Set.empty} + Dependencies = Set.empty + IpSecurityRestrictions = [] } [] member this.Name (state,name) : SlotConfig = {state with Name = name} @@ -154,6 +157,15 @@ type SlotBuilder() = member this.AddConnectionStrings(state, connectionStrings:string list) :SlotConfig = connectionStrings |> List.fold (fun state key -> this.AddConnectionString(state, key)) state + + /// Add Allowed ip for ip security restrictions + [] + member this.AllowIp(state, name, ip) : SlotConfig = + { state with IpSecurityRestrictions = IpSecurityRestriction.Create name ip Allow :: state.IpSecurityRestrictions } + /// Add Denied ip for ip security restrictions + [] + member this.DenyIp(state, name, ip) : SlotConfig = + { state with IpSecurityRestrictions = IpSecurityRestriction.Create name ip Deny :: state.IpSecurityRestrictions } interface ITaggable with member _.Add state tags = { state with Tags = state.Tags |> Map.merge tags } interface IDependable with member _.Add state newDeps = { state with Dependencies = state.Dependencies + newDeps } @@ -176,7 +188,8 @@ type CommonWebConfig = Slots : Map WorkerProcess : Bitness option ZipDeployPath : (string*ZipDeploy.ZipDeploySlot) option - HealthCheckPath: string option } + HealthCheckPath: string option + IpSecurityRestrictions: IpSecurityRestriction list } type WebAppConfig = { CommonWebConfig: CommonWebConfig @@ -420,6 +433,7 @@ type WebAppConfig = AutoSwapSlotName = None ZipDeployPath = this.CommonWebConfig.ZipDeployPath |> Option.map (fun (path,slot) -> path, ZipDeploy.ZipDeployTarget.WebApp, slot ) HealthCheckPath = this.CommonWebConfig.HealthCheckPath + IpSecurityRestrictions = this.CommonWebConfig.IpSecurityRestrictions } match keyVault with @@ -540,7 +554,8 @@ type WebAppBuilder() = Slots = Map.empty WorkerProcess = None ZipDeployPath = None - HealthCheckPath = None } + HealthCheckPath = None + IpSecurityRestrictions = [] } Sku = Sku.F1 WorkerSize = Small WorkerCount = 1 @@ -866,3 +881,11 @@ module Extensions = [] /// Specifies the path Azure load balancers will ping to check for unhealthy instances. member this.HealthCheckPath(state:'T, healthCheckPath:string) = this.Map state (fun x -> {x with HealthCheckPath = Some(healthCheckPath)}) + /// Add Allowed ip for ip security restrictions + [] + member this.AllowIp(state:'T, name, ip) = + this.Map state (fun x -> { x with IpSecurityRestrictions = IpSecurityRestriction.Create name ip Allow :: x.IpSecurityRestrictions }) + /// Add Denied ip for ip security restrictions + [] + member this.DenyIp(state:'T, name, ip) = + this.Map state (fun x -> { x with IpSecurityRestrictions = IpSecurityRestriction.Create name ip Deny :: x.IpSecurityRestrictions }) diff --git a/src/Farmer/Common.fs b/src/Farmer/Common.fs index 39b54b67e..2d8fb2875 100644 --- a/src/Farmer/Common.fs +++ b/src/Farmer/Common.fs @@ -619,6 +619,17 @@ module WebApp = type ConnectionStringKind = MySql | SQLServer | SQLAzure | Custom | NotificationHub | ServiceBus | EventHub | ApiHub | DocDb | RedisCache | PostgreSQL type ExtensionName = ExtensionName of string type Bitness = Bits32 | Bits64 + type IpSecurityAction = + | Allow + | Deny + type IpSecurityRestriction = + { Name: string + IpAddress: System.Net.IPAddress + Action: IpSecurityAction } + static member Create name ip action = + { Name = name + IpAddress = ip + Action = action } module Extensions = /// The Microsoft.AspNetCore.AzureAppServices logging extension. let Logging = ExtensionName "Microsoft.AspNetCore.AzureAppServices.SiteExtension" diff --git a/src/Tests/Functions.fs b/src/Tests/Functions.fs index 9463fc4b6..9b651729a 100644 --- a/src/Tests/Functions.fs +++ b/src/Tests/Functions.fs @@ -364,4 +364,38 @@ let tests = testList "Functions tests" [ |> Seq.map (fun x-> x.ToObject<{|name:string;value:string|}> ()) Expect.contains appSettings {|name="APPINSIGHTS_INSTRUMENTATIONKEY"; value="[reference(resourceId('shared-group', 'Microsoft.Insights/components', 'theName'), '2014-04-01').InstrumentationKey]"|} "Invalid value for APPINSIGHTS_INSTRUMENTATIONKEY" } + + test "Supports adding ip restriction" { + let ip = System.Net.IPAddress.Parse "1.2.3.4" + let resources = functions { name "test"; add_allowed_ip_restriction "test-rule" ip } |> getResources + let site = resources |> getResource |> List.head + + let expectedRestriction = IpSecurityRestriction.Create "test-rule" ip Allow + Expect.equal site.IpSecurityRestrictions [ expectedRestriction ] "Should add expected ip security restriction" + } + test "Supports adding ip restriction for denied ip" { + let ip = System.Net.IPAddress.Parse "1.2.3.4" + let resources = functions { name "test"; add_denied_ip_restriction "test-rule" ip } |> getResources + let site = resources |> getResource |> List.head + + let expectedRestriction = IpSecurityRestriction.Create "test-rule" ip Deny + Expect.equal site.IpSecurityRestrictions [ expectedRestriction ] "Should add denied ip security restriction" + } + test "Supports adding different ip restrictions to site and slot" { + let siteIp = System.Net.IPAddress.Parse "1.2.3.4" + let slotIp = System.Net.IPAddress.Parse "4.3.2.1" + let warmupSlot = appSlot { name "warm-up"; add_allowed_ip_restriction "slot-rule" slotIp } + let resources = functions { name "test"; add_slot warmupSlot; add_allowed_ip_restriction "site-rule" siteIp } |> getResources + let slot = + resources + |> getResource + |> List.filter (fun x -> x.ResourceType = Arm.Web.slots) + |> List.head + let site = resources |> getResource |> List.head + + let expectedSlotRestriction = IpSecurityRestriction.Create "slot-rule" slotIp Allow + let expectedSiteRestriction = IpSecurityRestriction.Create "site-rule" siteIp Allow + Expect.equal slot.IpSecurityRestrictions [ expectedSlotRestriction ] "Slot should have correct allowed ip security restriction" + Expect.equal site.IpSecurityRestrictions [ expectedSiteRestriction ] "Site should have correct allowed ip security restriction" + } ] \ No newline at end of file diff --git a/src/Tests/WebApp.fs b/src/Tests/WebApp.fs index 7e73c1a7f..78937fd40 100644 --- a/src/Tests/WebApp.fs +++ b/src/Tests/WebApp.fs @@ -674,4 +674,37 @@ let tests = testList "Web App Tests" [ Expect.equal hostnameBinding.Length 0 $"There should not be a hostname binding as a result of choosing the 'NoDomain' option" } + test "Supports adding ip restriction for allowed ip" { + let ip = System.Net.IPAddress.Parse "1.2.3.4" + let resources = webApp { name "test"; add_allowed_ip_restriction "test-rule" ip } |> getResources + let site = resources |> getResource |> List.head + + let expectedRestriction = IpSecurityRestriction.Create "test-rule" ip Allow + Expect.equal site.IpSecurityRestrictions [ expectedRestriction ] "Should add allowed ip security restriction" + } + test "Supports adding ip restriction for denied ip" { + let ip = System.Net.IPAddress.Parse "1.2.3.4" + let resources = webApp { name "test"; add_denied_ip_restriction "test-rule" ip } |> getResources + let site = resources |> getResource |> List.head + + let expectedRestriction = IpSecurityRestriction.Create "test-rule" ip Deny + Expect.equal site.IpSecurityRestrictions [ expectedRestriction ] "Should add denied ip security restriction" + } + test "Supports adding different ip restrictions to site and slot" { + let siteIp = System.Net.IPAddress.Parse "1.2.3.4" + let slotIp = System.Net.IPAddress.Parse "4.3.2.1" + let warmupSlot = appSlot { name "warm-up"; add_allowed_ip_restriction "slot-rule" slotIp } + let resources = webApp { name "test"; add_slot warmupSlot; add_allowed_ip_restriction "site-rule" siteIp } |> getResources + let slot = + resources + |> getResource + |> List.filter (fun x -> x.ResourceType = Arm.Web.slots) + |> List.head + let site = resources |> getResource |> List.head + + let expectedSlotRestriction = IpSecurityRestriction.Create "slot-rule" slotIp Allow + let expectedSiteRestriction = IpSecurityRestriction.Create "site-rule" siteIp Allow + Expect.equal slot.IpSecurityRestrictions [ expectedSlotRestriction ] "Slot should have correct allowed ip security restriction" + Expect.equal site.IpSecurityRestrictions [ expectedSiteRestriction ] "Site should have correct allowed ip security restriction" + } ] From b78a5452f1fa5e351244c47e63632e0a6ee9e454 Mon Sep 17 00:00:00 2001 From: Nargiz Gasimova <3729486+nargiz@users.noreply.github.com> Date: Thu, 9 Dec 2021 12:29:34 +0000 Subject: [PATCH 02/73] allow slot_settings --- RELEASE_NOTES.md | 3 + .../api-overview/resources/functions.md | 2 + .../content/api-overview/resources/web-app.md | 2 + src/Farmer/Arm/Web.fs | 13 +++- src/Farmer/Builders/Builders.Functions.fs | 9 ++- src/Farmer/Builders/Builders.WebApp.fs | 25 +++++++- src/Tests/Functions.fs | 59 +++++++++++++++++++ src/Tests/WebApp.fs | 59 +++++++++++++++++++ 8 files changed, 168 insertions(+), 4 deletions(-) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 40401e07b..bcdfe3b75 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,5 +1,8 @@ Release Notes ============= +## vNext +* WebApp/Functions: Support for deployment slot settings with `slot_setting` and `slot_settings` + ## 1.6.26 * WebApp/Functions: Fix .NET on Linux deployments diff --git a/docs/content/api-overview/resources/functions.md b/docs/content/api-overview/resources/functions.md index 10208fa72..653a7c280 100644 --- a/docs/content/api-overview/resources/functions.md +++ b/docs/content/api-overview/resources/functions.md @@ -45,6 +45,8 @@ The Functions builder is used to create Azure Functions accounts. It abstracts t | publish_as | Specifies whether to publish function as code or as a docker container. | | add_slot | Adds a deployment slot to the app | | add_slots | Adds multiple deployment slots to the app | +| slot_setting | Sets a deployment slot setting of the function in the form "key" "value". | +| slot_settings | Sets a list of deployment slot setting of the function as tuples in the form of ("key", "value"). | | health_check_path | Sets the path to your functions health check endpoint, which Azure load balancers will ping to determine which instances are healthy.| #### Post-deployment Builder Keywords diff --git a/docs/content/api-overview/resources/web-app.md b/docs/content/api-overview/resources/web-app.md index 28607f20d..305a7b380 100644 --- a/docs/content/api-overview/resources/web-app.md +++ b/docs/content/api-overview/resources/web-app.md @@ -55,6 +55,8 @@ The Web App builder is used to create Azure App Service accounts. It abstracts t | Web App | add_private_endpoints | Adds private endpoints for this Webapp to the given subnets | | Web App | add_slot | Adds a deployment slot to the app | | Web App | add_slots | Adds multiple deployment slots to the app | +| Web App | slot_setting | Sets a deployment slot setting of the web app in the form "key" "value". | +| Web App | slot_settings | Sets a list of deployment slot setting of the web app as tuples in the form of ("key", "value"). | | Web App | health_check_path | Sets the path to your functions health check endpoint, which Azure load balancers will ping to determine which instances are healthy.| | Web App | custom_domain | Adds custom domain to the app, containing an app service managed certificate | | Service Plan | service_plan_name | Sets the name of the service plan. If not set, uses the name of the web app postfixed with "-plan". | diff --git a/src/Farmer/Arm/Web.fs b/src/Farmer/Arm/Web.fs index 37f736c21..5fad96159 100644 --- a/src/Farmer/Arm/Web.fs +++ b/src/Farmer/Arm/Web.fs @@ -9,7 +9,7 @@ open System let serverFarms = ResourceType ("Microsoft.Web/serverfarms", "2018-02-01") let sites = ResourceType ("Microsoft.Web/sites", "2020-06-01") -let config = ResourceType ("Microsoft.Web/sites/config", "2016-08-01") +let config = ResourceType ("Microsoft.Web/sites/config", "2020-06-01") let sourceControls = ResourceType ("Microsoft.Web/sites/sourcecontrols", "2019-08-01") let staticSites = ResourceType ("Microsoft.Web/staticSites", "2019-12-01-preview") let siteExtensions = ResourceType ("Microsoft.Web/sites/siteextensions", "2020-06-01") @@ -400,6 +400,17 @@ type Certificate = canonicalName = this.DomainName |} |} +type SlotConfigName = + { SiteName : ResourceName + SlotSettingNames: List } + interface IArmResource with + member this.ResourceId = config.resourceId(this.SiteName/"slotconfignames") + member this.JsonModel = + {| config.Create(this.SiteName/"slotconfignames", dependsOn = [ sites.resourceId this.SiteName]) with + kind = "string" + properties = {| appSettingNames = this.SlotSettingNames |} + |} :> _ + [] module SiteExtensions = type SiteExtension = diff --git a/src/Farmer/Builders/Builders.Functions.fs b/src/Farmer/Builders/Builders.Functions.fs index a9bf1db5d..1e326be85 100644 --- a/src/Farmer/Builders/Builders.Functions.fs +++ b/src/Farmer/Builders/Builders.Functions.fs @@ -323,6 +323,12 @@ type FunctionsConfig = { site with AppSettings = None; ConnectionStrings = None } for (_, slot) in this.CommonWebConfig.Slots |> Map.toSeq do slot.ToSite site + + if this.CommonWebConfig.SlotSettingNames <> List.Empty then + { + SiteName = this.Name.ResourceName; + SlotSettingNames = this.CommonWebConfig.SlotSettingNames; + } ] type FunctionsBuilder() = @@ -343,7 +349,8 @@ type FunctionsBuilder() = Slots = Map.empty WorkerProcess = None ZipDeployPath = None - HealthCheckPath = None } + HealthCheckPath = None + SlotSettingNames = List.Empty } StorageAccount = derived (fun config -> let storage = config.Name.ResourceName.Map (sprintf "%sstorage") |> sanitiseStorage |> ResourceName storageAccounts.resourceId storage) diff --git a/src/Farmer/Builders/Builders.WebApp.fs b/src/Farmer/Builders/Builders.WebApp.fs index f370b7042..ed3ca0936 100644 --- a/src/Farmer/Builders/Builders.WebApp.fs +++ b/src/Farmer/Builders/Builders.WebApp.fs @@ -178,7 +178,8 @@ type CommonWebConfig = Slots : Map WorkerProcess : Bitness option ZipDeployPath : (string*ZipDeploy.ZipDeploySlot) option - HealthCheckPath: string option } + HealthCheckPath: string option + SlotSettingNames: List } type WebAppConfig = { CommonWebConfig: CommonWebConfig @@ -540,6 +541,12 @@ type WebAppConfig = DomainName = customDomain SslState = SslDisabled } | NoDomain -> () + + if this.CommonWebConfig.SlotSettingNames <> List.Empty then + { + SiteName = this.Name.ResourceName; + SlotSettingNames = this.CommonWebConfig.SlotSettingNames; + } yield! (PrivateEndpoint.create location this.ResourceId ["sites"] this.PrivateEndpoints) ] @@ -562,7 +569,8 @@ type WebAppBuilder() = Slots = Map.empty WorkerProcess = None ZipDeployPath = None - HealthCheckPath = None } + HealthCheckPath = None + SlotSettingNames = List.empty } Sku = Sku.F1 WorkerSize = Small WorkerCount = 1 @@ -888,3 +896,16 @@ module Extensions = [] /// Specifies the path Azure load balancers will ping to check for unhealthy instances. member this.HealthCheckPath(state:'T, healthCheckPath:string) = this.Map state (fun x -> {x with HealthCheckPath = Some(healthCheckPath)}) + + /// Adds slot settings + [] + member this.AddSlotSetting (state:'T, key, value) = + let current = this.Get state + { current with Settings = current.Settings.Add(key, LiteralSetting value); SlotSettingNames = List.append current.SlotSettingNames [key] } + |> this.Wrap state + [] + member this.AddSlotSettings(state:'T, settings: (string*string) list) = + let current = this.Get state + settings + |> List.fold (fun (state:CommonWebConfig) (key, value: string) -> { state with Settings = state.Settings.Add(key, LiteralSetting value); SlotSettingNames = List.append state.SlotSettingNames [key] }) current + |> this.Wrap state diff --git a/src/Tests/Functions.fs b/src/Tests/Functions.fs index 9463fc4b6..0d21518b3 100644 --- a/src/Tests/Functions.fs +++ b/src/Tests/Functions.fs @@ -364,4 +364,63 @@ let tests = testList "Functions tests" [ |> Seq.map (fun x-> x.ToObject<{|name:string;value:string|}> ()) Expect.contains appSettings {|name="APPINSIGHTS_INSTRUMENTATIONKEY"; value="[reference(resourceId('shared-group', 'Microsoft.Insights/components', 'theName'), '2014-04-01').InstrumentationKey]"|} "Invalid value for APPINSIGHTS_INSTRUMENTATIONKEY" } + + test "Supports slot settings" { + let functionsApp = functions { name "test"; slot_settings [ "sticky_config", "sticky_config_value"; "another_sticky_config", "another_sticky_config_value" ]} + + let scn = functionsApp |> getResources |> getResource |> List.head + let ws = functionsApp |> getResources |> getResource |> List.head + + let template = arm{ add_resource functionsApp} + let jobj = template.Template |> Writer.toJson |> Newtonsoft.Json.Linq.JObject.Parse + + let appSettingNames = + jobj.SelectTokens $"$..resources[?(@name=='{functionsApp.Name.ResourceName.Value}/slotconfignames')].properties.appSettingNames[*]" + |> Seq.map (fun x -> x.ToString()) + + let dependencies = + jobj.SelectTokens $"$..resources[?(@name=='{functionsApp.Name.ResourceName.Value}/slotconfignames')].dependsOn[*]" + |> Seq.map (fun x -> x.ToString()) + + let expectedSettings = Map [ + "sticky_config", LiteralSetting "sticky_config_value" + "another_sticky_config", LiteralSetting "another_sticky_config_value" ] + + let settings = Expect.wantSome ws.AppSettings "AppSettings should be set" + Expect.containsAll settings expectedSettings "App settings should contain the slot settings" + Expect.sequenceEqual scn.SlotSettingNames ["sticky_config"; "another_sticky_config"] "Slot config names should be set" + Expect.equal scn.SiteName (ResourceName "test") "Parent name should be set" + Expect.sequenceEqual appSettingNames [ "sticky_config"; "another_sticky_config"] "Slot config names should be present in template" + Expect.sequenceEqual dependencies [ $"[resourceId('Microsoft.Web/sites', '{functionsApp.Name.ResourceName.Value}')]"] "Slot config names resource should depend on web site" + + } + + test "Supports slot setting" { + let functionsApp = functions { name "test"; slot_setting "sticky_config" "sticky_config_value" } + + let scn = functionsApp |> getResources |> getResource |> List.head + let ws = functionsApp |> getResources |> getResource |> List.head + + let template = arm{ add_resource functionsApp} + let jobj = template.Template |> Writer.toJson |> Newtonsoft.Json.Linq.JObject.Parse + + let appSettingNames = + jobj.SelectTokens $"$..resources[?(@name=='{functionsApp.Name.ResourceName.Value}/slotconfignames')].properties.appSettingNames[*]" + |> Seq.map (fun x -> x.ToString()) + + let dependencies = + jobj.SelectTokens $"$..resources[?(@name=='{functionsApp.Name.ResourceName.Value}/slotconfignames')].dependsOn[*]" + |> Seq.map (fun x -> x.ToString()) + + let expectedSettings = Map [ + "sticky_config", LiteralSetting "sticky_config_value" ] + + let settings = Expect.wantSome ws.AppSettings "AppSettings should be set" + Expect.containsAll settings expectedSettings "App settings should contain the slot setting" + Expect.sequenceEqual scn.SlotSettingNames ["sticky_config"] "Slot config name should be set" + Expect.equal scn.SiteName (ResourceName "test") "Parent name should be set" + Expect.sequenceEqual appSettingNames [ "sticky_config" ] "Slot config name should be present in template" + Expect.sequenceEqual dependencies [ $"[resourceId('Microsoft.Web/sites', '{functionsApp.Name.ResourceName.Value}')]"] "Slot config names resource should depend on web site" + + } ] \ No newline at end of file diff --git a/src/Tests/WebApp.fs b/src/Tests/WebApp.fs index c852370b2..341c7d015 100644 --- a/src/Tests/WebApp.fs +++ b/src/Tests/WebApp.fs @@ -710,4 +710,63 @@ let tests = testList "Web App Tests" [ Expect.equal hostnameBinding.Length 0 $"There should not be a hostname binding as a result of choosing the 'NoDomain' option" } + + test "Supports slot settings" { + let webApp = webApp { name "test"; slot_settings [ "sticky_config", "sticky_config_value"; "another_sticky_config", "another_sticky_config_value" ]} + + let scn = webApp |> getResources |> getResource |> List.head + let ws = webApp |> getResources |> getResource |> List.head + + let template = arm{ add_resource webApp} + let jobj = template.Template |> Writer.toJson |> Newtonsoft.Json.Linq.JObject.Parse + + let appSettingNames = + jobj.SelectTokens $"$..resources[?(@name=='{webApp.Name.ResourceName.Value}/slotconfignames')].properties.appSettingNames[*]" + |> Seq.map (fun x -> x.ToString()) + + let dependencies = + jobj.SelectTokens $"$..resources[?(@name=='{webApp.Name.ResourceName.Value}/slotconfignames')].dependsOn[*]" + |> Seq.map (fun x -> x.ToString()) + + let expectedSettings = Map [ + "sticky_config", LiteralSetting "sticky_config_value" + "another_sticky_config", LiteralSetting "another_sticky_config_value" ] + + let settings = Expect.wantSome ws.AppSettings "AppSettings should be set" + Expect.containsAll settings expectedSettings "App settings should contain the slot settings" + Expect.sequenceEqual scn.SlotSettingNames ["sticky_config"; "another_sticky_config"] "Slot config names should be set" + Expect.equal scn.SiteName (ResourceName "test") "Parent name should be set" + Expect.sequenceEqual appSettingNames [ "sticky_config"; "another_sticky_config"] "Slot config names should be present in template" + Expect.sequenceEqual dependencies [ $"[resourceId('Microsoft.Web/sites', '{webApp.Name.ResourceName.Value}')]"] "Slot config names resource should depend on web site" + + } + + test "Supports slot setting" { + let webApp = webApp { name "test"; slot_setting "sticky_config" "sticky_config_value" } + + let scn = webApp |> getResources |> getResource |> List.head + let ws = webApp |> getResources |> getResource |> List.head + + let template = arm{ add_resource webApp} + let jobj = template.Template |> Writer.toJson |> Newtonsoft.Json.Linq.JObject.Parse + + let appSettingNames = + jobj.SelectTokens $"$..resources[?(@name=='{webApp.Name.ResourceName.Value}/slotconfignames')].properties.appSettingNames[*]" + |> Seq.map (fun x -> x.ToString()) + + let dependencies = + jobj.SelectTokens $"$..resources[?(@name=='{webApp.Name.ResourceName.Value}/slotconfignames')].dependsOn[*]" + |> Seq.map (fun x -> x.ToString()) + + let expectedSettings = Map [ + "sticky_config", LiteralSetting "sticky_config_value" ] + + let settings = Expect.wantSome ws.AppSettings "AppSettings should be set" + Expect.containsAll settings expectedSettings "App settings should contain the slot setting" + Expect.sequenceEqual scn.SlotSettingNames ["sticky_config"] "Slot config name should be set" + Expect.equal scn.SiteName (ResourceName "test") "Parent name should be set" + Expect.sequenceEqual appSettingNames [ "sticky_config" ] "Slot config name should be present in template" + Expect.sequenceEqual dependencies [ $"[resourceId('Microsoft.Web/sites', '{webApp.Name.ResourceName.Value}')]"] "Slot config names resource should depend on web site" + + } ] From f529e222727ca2223b73f3fe78d208e7b316eca4 Mon Sep 17 00:00:00 2001 From: Nargiz Gasimova <3729486+nargiz@users.noreply.github.com> Date: Fri, 10 Dec 2021 12:03:43 +0000 Subject: [PATCH 03/73] update slotSettingNames to be set --- src/Farmer/Arm/Web.fs | 2 +- src/Farmer/Builders/Builders.Functions.fs | 4 ++-- src/Farmer/Builders/Builders.WebApp.fs | 10 +++++----- src/Tests/Functions.fs | 12 ++++++------ src/Tests/WebApp.fs | 12 ++++++------ 5 files changed, 20 insertions(+), 20 deletions(-) diff --git a/src/Farmer/Arm/Web.fs b/src/Farmer/Arm/Web.fs index 5fad96159..76eb090bd 100644 --- a/src/Farmer/Arm/Web.fs +++ b/src/Farmer/Arm/Web.fs @@ -402,7 +402,7 @@ type Certificate = type SlotConfigName = { SiteName : ResourceName - SlotSettingNames: List } + SlotSettingNames: string Set } interface IArmResource with member this.ResourceId = config.resourceId(this.SiteName/"slotconfignames") member this.JsonModel = diff --git a/src/Farmer/Builders/Builders.Functions.fs b/src/Farmer/Builders/Builders.Functions.fs index 1e326be85..48a11d9d0 100644 --- a/src/Farmer/Builders/Builders.Functions.fs +++ b/src/Farmer/Builders/Builders.Functions.fs @@ -324,7 +324,7 @@ type FunctionsConfig = for (_, slot) in this.CommonWebConfig.Slots |> Map.toSeq do slot.ToSite site - if this.CommonWebConfig.SlotSettingNames <> List.Empty then + if this.CommonWebConfig.SlotSettingNames <> Set.empty then { SiteName = this.Name.ResourceName; SlotSettingNames = this.CommonWebConfig.SlotSettingNames; @@ -350,7 +350,7 @@ type FunctionsBuilder() = WorkerProcess = None ZipDeployPath = None HealthCheckPath = None - SlotSettingNames = List.Empty } + SlotSettingNames = Set.empty } StorageAccount = derived (fun config -> let storage = config.Name.ResourceName.Map (sprintf "%sstorage") |> sanitiseStorage |> ResourceName storageAccounts.resourceId storage) diff --git a/src/Farmer/Builders/Builders.WebApp.fs b/src/Farmer/Builders/Builders.WebApp.fs index ed3ca0936..72522da78 100644 --- a/src/Farmer/Builders/Builders.WebApp.fs +++ b/src/Farmer/Builders/Builders.WebApp.fs @@ -179,7 +179,7 @@ type CommonWebConfig = WorkerProcess : Bitness option ZipDeployPath : (string*ZipDeploy.ZipDeploySlot) option HealthCheckPath: string option - SlotSettingNames: List } + SlotSettingNames: string Set } type WebAppConfig = { CommonWebConfig: CommonWebConfig @@ -542,7 +542,7 @@ type WebAppConfig = SslState = SslDisabled } | NoDomain -> () - if this.CommonWebConfig.SlotSettingNames <> List.Empty then + if this.CommonWebConfig.SlotSettingNames <> Set.empty then { SiteName = this.Name.ResourceName; SlotSettingNames = this.CommonWebConfig.SlotSettingNames; @@ -570,7 +570,7 @@ type WebAppBuilder() = WorkerProcess = None ZipDeployPath = None HealthCheckPath = None - SlotSettingNames = List.empty } + SlotSettingNames = Set.empty } Sku = Sku.F1 WorkerSize = Small WorkerCount = 1 @@ -901,11 +901,11 @@ module Extensions = [] member this.AddSlotSetting (state:'T, key, value) = let current = this.Get state - { current with Settings = current.Settings.Add(key, LiteralSetting value); SlotSettingNames = List.append current.SlotSettingNames [key] } + { current with Settings = current.Settings.Add(key, LiteralSetting value); SlotSettingNames =current.SlotSettingNames.Add(key) } |> this.Wrap state [] member this.AddSlotSettings(state:'T, settings: (string*string) list) = let current = this.Get state settings - |> List.fold (fun (state:CommonWebConfig) (key, value: string) -> { state with Settings = state.Settings.Add(key, LiteralSetting value); SlotSettingNames = List.append state.SlotSettingNames [key] }) current + |> List.fold (fun (state:CommonWebConfig) (key, value: string) -> { state with Settings = state.Settings.Add(key, LiteralSetting value); SlotSettingNames = state.SlotSettingNames.Add(key) }) current |> this.Wrap state diff --git a/src/Tests/Functions.fs b/src/Tests/Functions.fs index 0d21518b3..f91e14685 100644 --- a/src/Tests/Functions.fs +++ b/src/Tests/Functions.fs @@ -388,10 +388,10 @@ let tests = testList "Functions tests" [ let settings = Expect.wantSome ws.AppSettings "AppSettings should be set" Expect.containsAll settings expectedSettings "App settings should contain the slot settings" - Expect.sequenceEqual scn.SlotSettingNames ["sticky_config"; "another_sticky_config"] "Slot config names should be set" + Expect.containsAll scn.SlotSettingNames ["sticky_config"; "another_sticky_config"] "Slot config names should be set" Expect.equal scn.SiteName (ResourceName "test") "Parent name should be set" - Expect.sequenceEqual appSettingNames [ "sticky_config"; "another_sticky_config"] "Slot config names should be present in template" - Expect.sequenceEqual dependencies [ $"[resourceId('Microsoft.Web/sites', '{functionsApp.Name.ResourceName.Value}')]"] "Slot config names resource should depend on web site" + Expect.containsAll appSettingNames [ "sticky_config"; "another_sticky_config"] "Slot config names should be present in template" + Expect.containsAll dependencies [ $"[resourceId('Microsoft.Web/sites', '{functionsApp.Name.ResourceName.Value}')]"] "Slot config names resource should depend on web site" } @@ -417,10 +417,10 @@ let tests = testList "Functions tests" [ let settings = Expect.wantSome ws.AppSettings "AppSettings should be set" Expect.containsAll settings expectedSettings "App settings should contain the slot setting" - Expect.sequenceEqual scn.SlotSettingNames ["sticky_config"] "Slot config name should be set" + Expect.containsAll scn.SlotSettingNames ["sticky_config"] "Slot config name should be set" Expect.equal scn.SiteName (ResourceName "test") "Parent name should be set" - Expect.sequenceEqual appSettingNames [ "sticky_config" ] "Slot config name should be present in template" - Expect.sequenceEqual dependencies [ $"[resourceId('Microsoft.Web/sites', '{functionsApp.Name.ResourceName.Value}')]"] "Slot config names resource should depend on web site" + Expect.containsAll appSettingNames [ "sticky_config" ] "Slot config name should be present in template" + Expect.containsAll dependencies [ $"[resourceId('Microsoft.Web/sites', '{functionsApp.Name.ResourceName.Value}')]"] "Slot config names resource should depend on web site" } ] \ No newline at end of file diff --git a/src/Tests/WebApp.fs b/src/Tests/WebApp.fs index 341c7d015..47a891116 100644 --- a/src/Tests/WebApp.fs +++ b/src/Tests/WebApp.fs @@ -734,10 +734,10 @@ let tests = testList "Web App Tests" [ let settings = Expect.wantSome ws.AppSettings "AppSettings should be set" Expect.containsAll settings expectedSettings "App settings should contain the slot settings" - Expect.sequenceEqual scn.SlotSettingNames ["sticky_config"; "another_sticky_config"] "Slot config names should be set" + Expect.containsAll scn.SlotSettingNames ["sticky_config"; "another_sticky_config"] "Slot config names should be set" Expect.equal scn.SiteName (ResourceName "test") "Parent name should be set" - Expect.sequenceEqual appSettingNames [ "sticky_config"; "another_sticky_config"] "Slot config names should be present in template" - Expect.sequenceEqual dependencies [ $"[resourceId('Microsoft.Web/sites', '{webApp.Name.ResourceName.Value}')]"] "Slot config names resource should depend on web site" + Expect.containsAll appSettingNames [ "sticky_config"; "another_sticky_config"] "Slot config names should be present in template" + Expect.containsAll dependencies [ $"[resourceId('Microsoft.Web/sites', '{webApp.Name.ResourceName.Value}')]"] "Slot config names resource should depend on web site" } @@ -763,10 +763,10 @@ let tests = testList "Web App Tests" [ let settings = Expect.wantSome ws.AppSettings "AppSettings should be set" Expect.containsAll settings expectedSettings "App settings should contain the slot setting" - Expect.sequenceEqual scn.SlotSettingNames ["sticky_config"] "Slot config name should be set" + Expect.containsAll scn.SlotSettingNames ["sticky_config"] "Slot config name should be set" Expect.equal scn.SiteName (ResourceName "test") "Parent name should be set" - Expect.sequenceEqual appSettingNames [ "sticky_config" ] "Slot config name should be present in template" - Expect.sequenceEqual dependencies [ $"[resourceId('Microsoft.Web/sites', '{webApp.Name.ResourceName.Value}')]"] "Slot config names resource should depend on web site" + Expect.containsAll appSettingNames [ "sticky_config" ] "Slot config name should be present in template" + Expect.containsAll dependencies [ $"[resourceId('Microsoft.Web/sites', '{webApp.Name.ResourceName.Value}')]"] "Slot config names resource should depend on web site" } ] From 849db956b71153eee5e075af732ef784d58c881e Mon Sep 17 00:00:00 2001 From: Richard Sanderson-Pope Date: Fri, 21 Jan 2022 11:54:16 +0000 Subject: [PATCH 04/73] Add support for multiple custom domains --- src/Farmer/Builders/Builders.WebApp.fs | 120 ++++++++++++------------- src/Farmer/Types.fs | 6 +- src/Tests/WebApp.fs | 19 +++- 3 files changed, 80 insertions(+), 65 deletions(-) diff --git a/src/Farmer/Builders/Builders.WebApp.fs b/src/Farmer/Builders/Builders.WebApp.fs index f370b7042..784d0ca0c 100644 --- a/src/Farmer/Builders/Builders.WebApp.fs +++ b/src/Farmer/Builders/Builders.WebApp.fs @@ -202,7 +202,7 @@ type WebAppConfig = AutomaticLoggingExtension : bool SiteExtensions : ExtensionName Set PrivateEndpoints: (LinkedResource * string option) Set - CustomDomain : DomainConfig } + CustomDomains : Map } member this.Name = this.CommonWebConfig.Name /// Gets this web app's Server Plan's full resource ID. member this.ServicePlanId = this.CommonWebConfig.ServicePlan.resourceId this.Name.ResourceName @@ -482,64 +482,64 @@ type WebAppConfig = { site with AppSettings = None; ConnectionStrings = None } // Don't deploy production slot settings as they could cause an app restart for (_,slot) in this.CommonWebConfig.Slots |> Map.toSeq do slot.ToSite site - - match this.CustomDomain with - | SecureDomain (customDomain, certOptions) -> - let hostNameBinding = + + for customDomain in this.CustomDomains |> Map.toSeq |> Seq.map snd do + match customDomain with + | SecureDomain (customDomain, certOptions) -> + let hostNameBinding = + { Location = location + SiteId = Managed (Arm.Web.sites.resourceId this.Name.ResourceName) + DomainName = customDomain + SslState = SslDisabled } // Initially create non-secure host name binding, we link the certificate in a nested deployment below + let cert = + { Location = location + SiteId = Managed this.ResourceId + ServicePlanId = Managed this.ServicePlanId + DomainName = customDomain } + hostNameBinding + + // Get the resource group which contains the app service plan + let aspRgName = + match this.CommonWebConfig.ServicePlan with + | LinkedResource linked -> linked.ResourceId.ResourceGroup + | _ -> None + // Create a nested resource group deployment for the certificate - this isn't strictly necessary when the app & app service plan are in the same resource group + // however, when they are in different resource groups this is required to make the deployment succeed (there is an ARM bug which causes a Not Found / Conflict otherwise) + // To keep the code simple, I opted to always nest the certificate deployment. - TheRSP 2021-12-14 + let certRg = resourceGroup { + name (aspRgName |> Option.defaultValue "[resourceGroup().name]") + add_resource + { cert with + SiteId = Unmanaged cert.SiteId.ResourceId + ServicePlanId = Unmanaged cert.ServicePlanId.ResourceId } + depends_on cert.SiteId + depends_on (hostNameBindings.resourceId(cert.SiteId.Name, ResourceName cert.DomainName)) + } + yield! ((certRg :> IBuilder).BuildResources location) + + // Need to rename `location` binding to prevent conflict with `location` operator in resource group + let resourceLocation = location + // nested deployment to update hostname binding with specified SSL options + yield! (resourceGroup { + name "[resourceGroup().name]" + location resourceLocation + add_resource { hostNameBinding with + SiteId = + match hostNameBinding.SiteId with + | Managed id -> Unmanaged id + | x -> x + SslState = + match certOptions with + | AppManagedCertificate -> SniBased (cert.GetThumbprintReference aspRgName) + | CustomCertificate thumbprint -> SniBased thumbprint + } + depends_on certRg + } :> IBuilder).BuildResources location + | InsecureDomain customDomain -> { Location = location SiteId = Managed (Arm.Web.sites.resourceId this.Name.ResourceName) DomainName = customDomain - SslState = SslDisabled } // Initially create non-secure host name binding, we link the certificate in a nested deployment below - let cert = - { Location = location - SiteId = Managed this.ResourceId - ServicePlanId = Managed this.ServicePlanId - DomainName = customDomain } - hostNameBinding - - // Get the resource group which contains the app service plan - let aspRgName = - match this.CommonWebConfig.ServicePlan with - | LinkedResource linked -> linked.ResourceId.ResourceGroup - | _ -> None - // Create a nested resource group deployment for the certificate - this isn't strictly necessary when the app & app service plan are in the same resource group - // however, when they are in different resource groups this is required to make the deployment succeed (there is an ARM bug which causes a Not Found / Conflict otherwise) - // To keep the code simple, I opted to always nest the certificate deployment. - TheRSP 2021-12-14 - let certRg = resourceGroup { - name (aspRgName |> Option.defaultValue "[resourceGroup().name]") - add_resource - { cert with - SiteId = Unmanaged cert.SiteId.ResourceId - ServicePlanId = Unmanaged cert.ServicePlanId.ResourceId } - depends_on cert.SiteId - depends_on (hostNameBindings.resourceId(cert.SiteId.Name, ResourceName cert.DomainName)) - } - yield! ((certRg :> IBuilder).BuildResources location) - - // Need to rename `location` binding to prevent conflict with `location` operator in resource group - let resourceLocation = location - // nested deployment to update hostname binding with specified SSL options - yield! (resourceGroup { - name "[resourceGroup().name]" - location resourceLocation - add_resource { hostNameBinding with - SiteId = - match hostNameBinding.SiteId with - | Managed id -> Unmanaged id - | x -> x - SslState = - match certOptions with - | AppManagedCertificate -> SniBased (cert.GetThumbprintReference aspRgName) - | CustomCertificate thumbprint -> SniBased thumbprint - } - depends_on certRg - } :> IBuilder).BuildResources location - | InsecureDomain customDomain -> - { Location = location - SiteId = Managed (Arm.Web.sites.resourceId this.Name.ResourceName) - DomainName = customDomain - SslState = SslDisabled } - | NoDomain -> () + SslState = SslDisabled } yield! (PrivateEndpoint.create location this.ResourceId ["sites"] this.PrivateEndpoints) ] @@ -583,7 +583,7 @@ type WebAppBuilder() = AutomaticLoggingExtension = true SiteExtensions = Set.empty PrivateEndpoints = Set.empty - CustomDomain = NoDomain} + CustomDomains = Map.empty } member _.Run(state:WebAppConfig) = if state.Name.ResourceName = ResourceName.Empty then raiseFarmer "No Web App name has been set." { state with @@ -683,9 +683,9 @@ type WebAppBuilder() = member _.DefaultLogging (state:WebAppConfig, setting) = { state with AutomaticLoggingExtension = setting } //Add Custom domain to you web app [] - member _.CustomDomain(state:WebAppConfig, domainConfig) = { state with CustomDomain = domainConfig } - member _.CustomDomain(state:WebAppConfig, customDomain) = { state with CustomDomain = SecureDomain (customDomain,AppManagedCertificate) } - member _.CustomDomain(state:WebAppConfig, (customDomain,thumbprint)) = { state with CustomDomain = SecureDomain (customDomain,CustomCertificate thumbprint) } + member _.AddCustomDomain(state:WebAppConfig, domainConfig:DomainConfig) = { state with CustomDomains = state.CustomDomains |> Map.add domainConfig.DomainName domainConfig } + member this.AddCustomDomain(state:WebAppConfig, customDomain) = this.AddCustomDomain (state, SecureDomain (customDomain,AppManagedCertificate)) + member this.AddCustomDomain(state:WebAppConfig, (customDomain,thumbprint)) = this.AddCustomDomain (state, SecureDomain (customDomain,CustomCertificate thumbprint)) interface IPrivateEndpoints with member _.Add state endpoints = { state with PrivateEndpoints = state.PrivateEndpoints |> Set.union endpoints} interface ITaggable with member _.Add state tags = { state with Tags = state.Tags |> Map.merge tags } diff --git a/src/Farmer/Types.fs b/src/Farmer/Types.fs index c44b7d4d9..f46bdc679 100644 --- a/src/Farmer/Types.fs +++ b/src/Farmer/Types.fs @@ -244,7 +244,11 @@ type CertificateOptions = type DomainConfig = | SecureDomain of domain:string * cert:CertificateOptions | InsecureDomain of domain:string - | NoDomain + member this.DomainName = + match this with + | SecureDomain (domainName,_) + | InsecureDomain (domainName) -> domainName + [] module ResourceRef = diff --git a/src/Tests/WebApp.fs b/src/Tests/WebApp.fs index c852370b2..c9d7926a1 100644 --- a/src/Tests/WebApp.fs +++ b/src/Tests/WebApp.fs @@ -700,14 +700,25 @@ let tests = testList "Web App Tests" [ Expect.isEmpty nestedDeployments $"Only secured domains need nested deployments" } - test "Supports no domains" { + test "Supports multiple custom domains" { let webappName = "test" - let resources = webApp { name webappName; custom_domain NoDomain } |> getResources + let resources = + webApp { + name webappName + custom_domain "secure.io" + custom_domain (DomainConfig.InsecureDomain "insecure.io") + } |> getResources let wa = resources |> getResource |> List.head + let exepectedSiteId = (Managed (Arm.Web.sites.resourceId wa.Name)) + //Testing HostnameBinding - let hostnameBinding = resources |> getResource + let hostnameBindings = resources |> getResource + let secureBinding = hostnameBindings |> List.filter (fun x->x.DomainName = "secure.io") |> List.head + let insecureBinding = hostnameBindings |> List.filter (fun x->x.DomainName = "insecure.io") |> List.head + + Expect.equal secureBinding.SiteId exepectedSiteId $"HostnameBinding SiteId should be {exepectedSiteId}" + Expect.equal insecureBinding.SiteId exepectedSiteId $"HostnameBinding SiteId should be {exepectedSiteId}" - Expect.equal hostnameBinding.Length 0 $"There should not be a hostname binding as a result of choosing the 'NoDomain' option" } ] From 106596df6e9163e04cebc63f8f1e00b1f4d1b718 Mon Sep 17 00:00:00 2001 From: Richard Sanderson-Pope Date: Fri, 21 Jan 2022 13:46:49 +0000 Subject: [PATCH 05/73] Use domainName as certificate name --- src/Farmer/Arm/Web.fs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Farmer/Arm/Web.fs b/src/Farmer/Arm/Web.fs index 37f736c21..117a48255 100644 --- a/src/Farmer/Arm/Web.fs +++ b/src/Farmer/Arm/Web.fs @@ -380,7 +380,7 @@ type Certificate = SiteId: LinkedResource ServicePlanId: LinkedResource DomainName: string } - member this.ResourceName = this.SiteId.Name.Map (sprintf "%s-cert") + member this.ResourceName = ResourceName this.DomainName member this.Thumbprint = this.GetThumbprintReference None member this.GetThumbprintReference certificateResourceGroup = ArmExpression.reference({certificates.resourceId this.ResourceName with ResourceGroup = certificateResourceGroup}).Map(sprintf "%s.Thumbprint") From de2b2e8863c6697f6b3cbfab25c0a58051f092a0 Mon Sep 17 00:00:00 2001 From: Richard Sanderson-Pope Date: Fri, 21 Jan 2022 14:26:39 +0000 Subject: [PATCH 06/73] update docs + release notes --- RELEASE_NOTES.md | 3 +++ docs/content/api-overview/resources/web-app.md | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 40401e07b..f37ff8e33 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,5 +1,8 @@ Release Notes ============= +## 1.6.27 +* WebApp: Allow multiple custom domains + ## 1.6.26 * WebApp/Functions: Fix .NET on Linux deployments diff --git a/docs/content/api-overview/resources/web-app.md b/docs/content/api-overview/resources/web-app.md index 28607f20d..feb416b81 100644 --- a/docs/content/api-overview/resources/web-app.md +++ b/docs/content/api-overview/resources/web-app.md @@ -56,7 +56,7 @@ The Web App builder is used to create Azure App Service accounts. It abstracts t | Web App | add_slot | Adds a deployment slot to the app | | Web App | add_slots | Adds multiple deployment slots to the app | | Web App | health_check_path | Sets the path to your functions health check endpoint, which Azure load balancers will ping to determine which instances are healthy.| -| Web App | custom_domain | Adds custom domain to the app, containing an app service managed certificate | +| Web App | custom_domain | Adds a custom domain to the app. Optionally this can also generate an SSL certificate for the domain. This operator can be used multiple times to add multiple domains | | Service Plan | service_plan_name | Sets the name of the service plan. If not set, uses the name of the web app postfixed with "-plan". | | Service Plan | runtime_stack | Sets the runtime stack. | | Service Plan | operating_system | Sets the operating system. If Linux, App Insights configuration settings will be omitted as they are not supported by Azure App Service. | From edc6aaeb103b689439dada4e0cf8b694354d4444 Mon Sep 17 00:00:00 2001 From: isaac Date: Sun, 23 Jan 2022 01:54:38 +0100 Subject: [PATCH 07/73] Only turn on "auto" extension for Windows --- src/Farmer/Builders/Builders.WebApp.fs | 14 +++++++++----- src/Tests/WebApp.fs | 12 +++++++++--- 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/src/Farmer/Builders/Builders.WebApp.fs b/src/Farmer/Builders/Builders.WebApp.fs index b68a14524..90bc8fc97 100644 --- a/src/Farmer/Builders/Builders.WebApp.fs +++ b/src/Farmer/Builders/Builders.WebApp.fs @@ -497,16 +497,16 @@ type WebAppConfig = hostNameBinding // Get the resource group which contains the app service plan - let aspRgName = + let aspRgName = match this.CommonWebConfig.ServicePlan with | LinkedResource linked -> linked.ResourceId.ResourceGroup | _ -> None // Create a nested resource group deployment for the certificate - this isn't strictly necessary when the app & app service plan are in the same resource group // however, when they are in different resource groups this is required to make the deployment succeed (there is an ARM bug which causes a Not Found / Conflict otherwise) // To keep the code simple, I opted to always nest the certificate deployment. - TheRSP 2021-12-14 - let certRg = resourceGroup { + let certRg = resourceGroup { name (aspRgName |> Option.defaultValue "[resourceGroup().name]") - add_resource + add_resource { cert with SiteId = Unmanaged cert.SiteId.ResourceId ServicePlanId = Unmanaged cert.ServicePlanId.ResourceId } @@ -591,8 +591,12 @@ type WebAppBuilder() = // its important to only add this extension if we're not using Web App for Containers - if we are // then this will generate an error during deployment: // No route registered for '/api/siteextensions/Microsoft.AspNetCore.AzureAppServices.SiteExtension' - | { Runtime = Runtime.DotNetCore _; AutomaticLoggingExtension = true ; DockerImage = None } -> - state.SiteExtensions.Add WebApp.Extensions.Logging + | { Runtime = DotNetCore _ + AutomaticLoggingExtension = true + DockerImage = None + CommonWebConfig = { OperatingSystem = Windows } } + -> + state.SiteExtensions.Add Extensions.Logging | _ -> state.SiteExtensions DockerImage = diff --git a/src/Tests/WebApp.fs b/src/Tests/WebApp.fs index ab9464da3..31914b24d 100644 --- a/src/Tests/WebApp.fs +++ b/src/Tests/WebApp.fs @@ -280,7 +280,7 @@ let tests = testList "Web App Tests" [ let wa : Site = webApp { name "testsite" } |> getResourceAtIndex 3 wa |> hasSetting "APPINSIGHTS_INSTRUMENTATIONKEY" "Missing Windows instrumentation key" - let wa : Site = webApp { name "testsite"; operating_system Linux } |> getResourceAtIndex 3 + let wa : Site = webApp { name "testsite"; operating_system Linux } |> getResourceAtIndex 2 wa |> hasSetting "APPINSIGHTS_INSTRUMENTATIONKEY" "Missing Linux instrumentation key" let wa : Site = webApp { name "testsite"; app_insights_off } |> getResourceAtIndex 2 @@ -558,7 +558,7 @@ let tests = testList "Web App Tests" [ Expect.containsAll (theSlot.Identity.UserAssigned) [identity18.UserAssignedIdentity; identity21.UserAssignedIdentity] "Slot should have both user assigned identities" Expect.equal theSlot.KeyVaultReferenceIdentity (Some identity21.UserAssignedIdentity) "Slot should have correct keyvault identity" } - + test "WebApp with slot can use AutoSwapSlotName" { let warmupSlot = appSlot { name "warm-up"; autoSlotSwapName "production" } let site:WebAppConfig = webApp { name "slots"; add_slot warmupSlot } @@ -567,7 +567,7 @@ let tests = testList "Web App Tests" [ let slot: Site = site |> getResourceAtIndex 4 - + Expect.equal slot.Name "slots/warm-up" "Should be expected slot" Expect.equal slot.SiteConfig.AutoSwapSlotName "production" "Should use provided auto swap slot name" } @@ -704,4 +704,10 @@ let tests = testList "Web App Tests" [ Expect.equal hostnameBinding.Length 0 $"There should not be a hostname binding as a result of choosing the 'NoDomain' option" } + + test "Linux automatically turns off logging extension" { + let wa = webApp { name "siteX"; operating_system Linux } + let extensions = wa |> getResources |> getResource + Expect.isEmpty extensions "Should not be any extensions" + } ] From 48626d19e78af2c2cd321e3ec77ebf060e62384b Mon Sep 17 00:00:00 2001 From: isaac Date: Sun, 23 Jan 2022 01:56:56 +0100 Subject: [PATCH 08/73] Release notes --- RELEASE_NOTES.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index e9eb1d146..a66fad729 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,5 +1,7 @@ Release Notes ============= +## vNext +* WebApps/Functions: Don't turn on Logging Extension for Linux App Service. ## 1.6.25 * CosmosDb: Add support for serverless capacity mode. From de2fb64a5af244e2aecc293daa5d51bf5f17548f Mon Sep 17 00:00:00 2001 From: Richard SP Date: Mon, 24 Jan 2022 08:46:27 +0000 Subject: [PATCH 09/73] Expanded description of custom_domain operator --- docs/content/api-overview/resources/web-app.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/content/api-overview/resources/web-app.md b/docs/content/api-overview/resources/web-app.md index feb416b81..0602c5a1a 100644 --- a/docs/content/api-overview/resources/web-app.md +++ b/docs/content/api-overview/resources/web-app.md @@ -56,7 +56,7 @@ The Web App builder is used to create Azure App Service accounts. It abstracts t | Web App | add_slot | Adds a deployment slot to the app | | Web App | add_slots | Adds multiple deployment slots to the app | | Web App | health_check_path | Sets the path to your functions health check endpoint, which Azure load balancers will ping to determine which instances are healthy.| -| Web App | custom_domain | Adds a custom domain to the app. Optionally this can also generate an SSL certificate for the domain. This operator can be used multiple times to add multiple domains | +| Web App | custom_domain | Adds a custom domain to the app. By default this will produce an AppService-managed SSL certificate for your domain as well. Through the overloads of this operator, you can provide a custom certificate thumbprint or choose not to use SSL. You can use this operator multiple times to add multiple custom domains. | | Service Plan | service_plan_name | Sets the name of the service plan. If not set, uses the name of the web app postfixed with "-plan". | | Service Plan | runtime_stack | Sets the runtime stack. | | Service Plan | operating_system | Sets the operating system. If Linux, App Insights configuration settings will be omitted as they are not supported by Azure App Service. | From 7c8fcfc7b3292de8e4acf74cbd55a5b583a9e7a9 Mon Sep 17 00:00:00 2001 From: Justin Wilkinson Date: Fri, 28 Jan 2022 08:53:44 +0000 Subject: [PATCH 10/73] Fix host name binding error when deploying multiple custom domains --- src/Farmer/Arm/Web.fs | 7 +++-- src/Farmer/Builders/Builders.WebApp.fs | 36 ++++++++++++++++++-------- src/Tests/WebApp.fs | 30 +++++++++++++++++++++ 3 files changed, 60 insertions(+), 13 deletions(-) diff --git a/src/Farmer/Arm/Web.fs b/src/Farmer/Arm/Web.fs index 117a48255..a5446f5dd 100644 --- a/src/Farmer/Arm/Web.fs +++ b/src/Farmer/Arm/Web.fs @@ -350,7 +350,8 @@ type HostNameBinding = { Location: Location SiteId: LinkedResource DomainName: string - SslState: SslState } + SslState: SslState + DependsOn: ResourceId Set } member this.SiteResourceId = match this.SiteId with | Managed id -> id.Name @@ -360,7 +361,9 @@ type HostNameBinding = member this.Dependencies = [ match this.SiteId with | Managed resid -> resid - | _ -> () ] + | _ -> () + + yield! this.DependsOn ] member this.ResourceId = hostNameBindings.resourceId (this.SiteResourceId, ResourceName this.DomainName) interface IArmResource with diff --git a/src/Farmer/Builders/Builders.WebApp.fs b/src/Farmer/Builders/Builders.WebApp.fs index 784d0ca0c..0b78b3bae 100644 --- a/src/Farmer/Builders/Builders.WebApp.fs +++ b/src/Farmer/Builders/Builders.WebApp.fs @@ -483,14 +483,27 @@ type WebAppConfig = for (_,slot) in this.CommonWebConfig.Slots |> Map.toSeq do slot.ToSite site + // Host Name Bindings must be deployed sequentially to avoid an error, as the site cannot be modified concurrently. + // To do so we add a dependency to the previous binding. + let mutable previousHostNameBinding = None for customDomain in this.CustomDomains |> Map.toSeq |> Seq.map snd do + let dependsOn = + match previousHostNameBinding with + | Some previous -> Set.singleton previous + | None -> Set.empty + + let hostNameBinding = + { Location = location + SiteId = Managed (Arm.Web.sites.resourceId this.Name.ResourceName) + DomainName = customDomain.DomainName + SslState = SslDisabled // Initially create non-secure host name binding, we link the certificate in a nested deployment below if this is a secure domain. + DependsOn = dependsOn } + + hostNameBinding + + previousHostNameBinding <- Some hostNameBinding.ResourceId match customDomain with | SecureDomain (customDomain, certOptions) -> - let hostNameBinding = - { Location = location - SiteId = Managed (Arm.Web.sites.resourceId this.Name.ResourceName) - DomainName = customDomain - SslState = SslDisabled } // Initially create non-secure host name binding, we link the certificate in a nested deployment below let cert = { Location = location SiteId = Managed this.ResourceId @@ -513,7 +526,7 @@ type WebAppConfig = SiteId = Unmanaged cert.SiteId.ResourceId ServicePlanId = Unmanaged cert.ServicePlanId.ResourceId } depends_on cert.SiteId - depends_on (hostNameBindings.resourceId(cert.SiteId.Name, ResourceName cert.DomainName)) + depends_on hostNameBinding.ResourceId } yield! ((certRg :> IBuilder).BuildResources location) @@ -532,14 +545,11 @@ type WebAppConfig = match certOptions with | AppManagedCertificate -> SniBased (cert.GetThumbprintReference aspRgName) | CustomCertificate thumbprint -> SniBased thumbprint + DependsOn = Set.empty // Don't want the dependency in this nested template. } depends_on certRg } :> IBuilder).BuildResources location - | InsecureDomain customDomain -> - { Location = location - SiteId = Managed (Arm.Web.sites.resourceId this.Name.ResourceName) - DomainName = customDomain - SslState = SslDisabled } + | _ -> () yield! (PrivateEndpoint.create location this.ResourceId ["sites"] this.PrivateEndpoints) ] @@ -686,6 +696,10 @@ type WebAppBuilder() = member _.AddCustomDomain(state:WebAppConfig, domainConfig:DomainConfig) = { state with CustomDomains = state.CustomDomains |> Map.add domainConfig.DomainName domainConfig } member this.AddCustomDomain(state:WebAppConfig, customDomain) = this.AddCustomDomain (state, SecureDomain (customDomain,AppManagedCertificate)) member this.AddCustomDomain(state:WebAppConfig, (customDomain,thumbprint)) = this.AddCustomDomain (state, SecureDomain (customDomain,CustomCertificate thumbprint)) + [] + member this.AddCustomDomains(state, customDomains:string list) = customDomains |> List.fold (fun state domain -> this.AddCustomDomain(state, domain)) state + member this.AddCustomDomains(state, domainConfigs:DomainConfig list) = domainConfigs |> List.fold (fun state domain -> this.AddCustomDomain(state, domain)) state + member this.AddCustomDomains(state, customDomainsWithThumprint:(string * ArmExpression) list) = customDomainsWithThumprint |> List.fold (fun state domain -> this.AddCustomDomain(state, domain)) state interface IPrivateEndpoints with member _.Add state endpoints = { state with PrivateEndpoints = state.PrivateEndpoints |> Set.union endpoints} interface ITaggable with member _.Add state tags = { state with Tags = state.Tags |> Map.merge tags } diff --git a/src/Tests/WebApp.fs b/src/Tests/WebApp.fs index c9d7926a1..acd11d656 100644 --- a/src/Tests/WebApp.fs +++ b/src/Tests/WebApp.fs @@ -719,6 +719,36 @@ let tests = testList "Web App Tests" [ Expect.equal secureBinding.SiteId exepectedSiteId $"HostnameBinding SiteId should be {exepectedSiteId}" Expect.equal insecureBinding.SiteId exepectedSiteId $"HostnameBinding SiteId should be {exepectedSiteId}" + } + + test "Assigns dependencies to host names when deploying multiple custom domains" { + let webappName = "test" + let resources = + webApp { + name webappName + custom_domains ["secure1.io" ; "secure2.io" ; "secure3.io"] + } |> getResources + let wa = resources |> getResource |> List.head + + let exepectedSiteId = (Managed (Arm.Web.sites.resourceId wa.Name)) + //Testing HostnameBinding + let hostnameBindings = resources |> getResource + let secureBinding1 = hostnameBindings |> List.filter(fun x -> x.DomainName = "secure1.io") |> List.head + let secureBinding2 = hostnameBindings |> List.filter(fun x -> x.DomainName = "secure2.io") |> List.head + let secureBinding3 = hostnameBindings |> List.filter(fun x -> x.DomainName = "secure3.io") |> List.head + let nestedResourceGroupHostNameUpdates = + resources + |> getResource + |> Seq.map(fun x -> getResource(x.Resources)) + |> Seq.filter(fun x -> x.Length > 0) + + Expect.all nestedResourceGroupHostNameUpdates (fun x -> x.Head.DependsOn.IsEmpty) "No dependencies expected on nested template" + Expect.equal secureBinding1.SiteId exepectedSiteId $"HostnameBinding SiteId should be {exepectedSiteId}" + Expect.equal secureBinding2.SiteId exepectedSiteId $"HostnameBinding SiteId should be {exepectedSiteId}" + Expect.equal secureBinding3.SiteId exepectedSiteId $"HostnameBinding SiteId should be {exepectedSiteId}" + Expect.isEmpty secureBinding1.DependsOn "First host name binding should have no dependency" + Expect.contains (secureBinding2.DependsOn |> Seq.map(ResourceId.Eval)) "[resourceId('Microsoft.Web/sites/hostNameBindings', 'test', 'secure1.io')]" "Second host name binding should depend on first" + Expect.contains (secureBinding3.DependsOn |> Seq.map(ResourceId.Eval)) "[resourceId('Microsoft.Web/sites/hostNameBindings', 'test', 'secure2.io')]" "Third host name binding depends on second" } ] From 6c136c20250169713df7d4673546697c82f89886 Mon Sep 17 00:00:00 2001 From: Steven Belskie Date: Sun, 30 Jan 2022 15:59:42 -0500 Subject: [PATCH 11/73] Add support for custom docker port for web apps --- RELEASE_NOTES.md | 1 + src/Farmer/Builders/Builders.WebApp.fs | 16 +++++++++++++--- src/Tests/WebApp.fs | 18 ++++++++++++++++++ 3 files changed, 32 insertions(+), 3 deletions(-) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index cc3e95f20..68dc55b69 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -3,6 +3,7 @@ Release Notes ## vNext * WebApps/Functions: Don't turn on Logging Extension for Linux App Service. * WebApps/Functions: Fix .NET 5/6 on Linux deployments. +* WebApp: Support custom port for docker container with `docker_port` ## 1.6.25 * CosmosDb: Add support for serverless capacity mode. diff --git a/src/Farmer/Builders/Builders.WebApp.fs b/src/Farmer/Builders/Builders.WebApp.fs index 96c12dfe8..4b0833818 100644 --- a/src/Farmer/Builders/Builders.WebApp.fs +++ b/src/Farmer/Builders/Builders.WebApp.fs @@ -62,6 +62,7 @@ type Runtime = module AppSettings = let WebsiteNodeDefaultVersion version = "WEBSITE_NODE_DEFAULT_VERSION", version let RunFromPackage = "WEBSITE_RUN_FROM_PACKAGE", "1" + let WebSitePort (port:int) = "WEBSITE_PORT", port.ToString() let publishingPassword (name:ResourceName) = let resourceId = config.resourceId (name, ResourceName "publishingCredentials") @@ -202,7 +203,8 @@ type WebAppConfig = AutomaticLoggingExtension : bool SiteExtensions : ExtensionName Set PrivateEndpoints: (LinkedResource * string option) Set - CustomDomain : DomainConfig } + CustomDomain : DomainConfig + DockerPort: int option } member this.Name = this.CommonWebConfig.Name /// Gets this web app's Server Plan's full resource ID. member this.ServicePlanId = this.CommonWebConfig.ServicePlan.resourceId this.Name.ResourceName @@ -284,6 +286,10 @@ type WebAppConfig = | _ , None -> () + match this.DockerPort with + | Some port -> AppSettings.WebSitePort port + | None -> () + if this.DockerCi then "DOCKER_ENABLE_CI", "true" ] @@ -583,7 +589,8 @@ type WebAppBuilder() = AutomaticLoggingExtension = true SiteExtensions = Set.empty PrivateEndpoints = Set.empty - CustomDomain = NoDomain} + CustomDomain = NoDomain + DockerPort = None } member _.Run(state:WebAppConfig) = if state.Name.ResourceName = ResourceName.Empty then raiseFarmer "No Web App name has been set." { state with @@ -690,7 +697,10 @@ type WebAppBuilder() = member _.CustomDomain(state:WebAppConfig, domainConfig) = { state with CustomDomain = domainConfig } member _.CustomDomain(state:WebAppConfig, customDomain) = { state with CustomDomain = SecureDomain (customDomain,AppManagedCertificate) } member _.CustomDomain(state:WebAppConfig, (customDomain,thumbprint)) = { state with CustomDomain = SecureDomain (customDomain,CustomCertificate thumbprint) } - + /// Map specified port traffic from your docker container to port 80 for App Service + [] + member _.DockerPort(state: WebAppConfig, dockerPort:int) = { state with DockerPort = Some(dockerPort)} + interface IPrivateEndpoints with member _.Add state endpoints = { state with PrivateEndpoints = state.PrivateEndpoints |> Set.union endpoints} interface ITaggable with member _.Add state tags = { state with Tags = state.Tags |> Map.merge tags } interface IDependable with member _.Add state newDeps = { state with Dependencies = state.Dependencies + newDeps } diff --git a/src/Tests/WebApp.fs b/src/Tests/WebApp.fs index 0e9518b6a..3661b9e24 100644 --- a/src/Tests/WebApp.fs +++ b/src/Tests/WebApp.fs @@ -716,4 +716,22 @@ let tests = testList "Web App Tests" [ let extensions = wa |> getResources |> getResource Expect.isEmpty extensions "Should not be any extensions" } + + test "Supports docker ports with WEBSITE_PORT"{ + let wa = webApp { name "testApp"; docker_port 8080; } + let port = Expect.wantSome wa.DockerPort "Docker port should be set" + Expect.equal port 8080 "Docker port should 8080" + + let site = wa |> getResources|> getResource |> List.head + + let settings = Expect.wantSome site.AppSettings "AppSettings should be set" + let (hasValue, value) = settings.TryGetValue("WEBSITE_PORT"); + + Expect.isTrue hasValue "WEBSITE_PORT should be set" + Expect.equal value.Value "8080" "WEBSITE_PORT should be 8080" + + let defaultWa = webApp { name "testApp"; } + Expect.isNone defaultWa.DockerPort "Docker port should not be set" + } + ] From aa9c0930f72035caf55ac9c57e96e085db7a6ace Mon Sep 17 00:00:00 2001 From: Steven Belskie Date: Sun, 30 Jan 2022 16:11:18 -0500 Subject: [PATCH 12/73] Update web app documentation for docker_port --- docs/content/api-overview/resources/web-app.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/content/api-overview/resources/web-app.md b/docs/content/api-overview/resources/web-app.md index 28607f20d..f3aa8124f 100644 --- a/docs/content/api-overview/resources/web-app.md +++ b/docs/content/api-overview/resources/web-app.md @@ -57,6 +57,7 @@ The Web App builder is used to create Azure App Service accounts. It abstracts t | Web App | add_slots | Adds multiple deployment slots to the app | | Web App | health_check_path | Sets the path to your functions health check endpoint, which Azure load balancers will ping to determine which instances are healthy.| | Web App | custom_domain | Adds custom domain to the app, containing an app service managed certificate | +| Web App | docker_port | Adds `WEBSITE_PORT` setting to map custom docker port to app service port 80 | | Service Plan | service_plan_name | Sets the name of the service plan. If not set, uses the name of the web app postfixed with "-plan". | | Service Plan | runtime_stack | Sets the runtime stack. | | Service Plan | operating_system | Sets the operating system. If Linux, App Insights configuration settings will be omitted as they are not supported by Azure App Service. | From 47f8b3814efeae822962888f83eeeec09d91b727 Mon Sep 17 00:00:00 2001 From: Steven Belskie Date: Sun, 30 Jan 2022 16:33:53 -0500 Subject: [PATCH 13/73] Fix typo in App Setting (WEBSITE_PORT -> WEBSITES_PORT) --- docs/content/api-overview/resources/web-app.md | 2 +- src/Farmer/Builders/Builders.WebApp.fs | 4 ++-- src/Tests/WebApp.fs | 8 ++++---- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/content/api-overview/resources/web-app.md b/docs/content/api-overview/resources/web-app.md index f3aa8124f..17fdc0951 100644 --- a/docs/content/api-overview/resources/web-app.md +++ b/docs/content/api-overview/resources/web-app.md @@ -57,7 +57,7 @@ The Web App builder is used to create Azure App Service accounts. It abstracts t | Web App | add_slots | Adds multiple deployment slots to the app | | Web App | health_check_path | Sets the path to your functions health check endpoint, which Azure load balancers will ping to determine which instances are healthy.| | Web App | custom_domain | Adds custom domain to the app, containing an app service managed certificate | -| Web App | docker_port | Adds `WEBSITE_PORT` setting to map custom docker port to app service port 80 | +| Web App | docker_port | Adds `WEBSITES_PORT` setting to map custom docker port to app service port 80 | | Service Plan | service_plan_name | Sets the name of the service plan. If not set, uses the name of the web app postfixed with "-plan". | | Service Plan | runtime_stack | Sets the runtime stack. | | Service Plan | operating_system | Sets the operating system. If Linux, App Insights configuration settings will be omitted as they are not supported by Azure App Service. | diff --git a/src/Farmer/Builders/Builders.WebApp.fs b/src/Farmer/Builders/Builders.WebApp.fs index 4b0833818..33eee06ae 100644 --- a/src/Farmer/Builders/Builders.WebApp.fs +++ b/src/Farmer/Builders/Builders.WebApp.fs @@ -62,7 +62,7 @@ type Runtime = module AppSettings = let WebsiteNodeDefaultVersion version = "WEBSITE_NODE_DEFAULT_VERSION", version let RunFromPackage = "WEBSITE_RUN_FROM_PACKAGE", "1" - let WebSitePort (port:int) = "WEBSITE_PORT", port.ToString() + let WebsitesPort (port:int) = "WEBSITES_PORT", port.ToString() let publishingPassword (name:ResourceName) = let resourceId = config.resourceId (name, ResourceName "publishingCredentials") @@ -287,7 +287,7 @@ type WebAppConfig = () match this.DockerPort with - | Some port -> AppSettings.WebSitePort port + | Some port -> AppSettings.WebsitesPort port | None -> () if this.DockerCi then "DOCKER_ENABLE_CI", "true" diff --git a/src/Tests/WebApp.fs b/src/Tests/WebApp.fs index 3661b9e24..84a727cee 100644 --- a/src/Tests/WebApp.fs +++ b/src/Tests/WebApp.fs @@ -717,7 +717,7 @@ let tests = testList "Web App Tests" [ Expect.isEmpty extensions "Should not be any extensions" } - test "Supports docker ports with WEBSITE_PORT"{ + test "Supports docker ports with WEBSITES_PORT"{ let wa = webApp { name "testApp"; docker_port 8080; } let port = Expect.wantSome wa.DockerPort "Docker port should be set" Expect.equal port 8080 "Docker port should 8080" @@ -725,10 +725,10 @@ let tests = testList "Web App Tests" [ let site = wa |> getResources|> getResource |> List.head let settings = Expect.wantSome site.AppSettings "AppSettings should be set" - let (hasValue, value) = settings.TryGetValue("WEBSITE_PORT"); + let (hasValue, value) = settings.TryGetValue("WEBSITES_PORT"); - Expect.isTrue hasValue "WEBSITE_PORT should be set" - Expect.equal value.Value "8080" "WEBSITE_PORT should be 8080" + Expect.isTrue hasValue "WEBSITES_PORT should be set" + Expect.equal value.Value "8080" "WEBSITES_PORT should be 8080" let defaultWa = webApp { name "testApp"; } Expect.isNone defaultWa.DockerPort "Docker port should not be set" From 8518bb5cb68b9aadc05bd3e8bb68546d6782cd00 Mon Sep 17 00:00:00 2001 From: Amine Mejaouel <45773049+amine-mejaouel@users.noreply.github.com> Date: Sun, 6 Feb 2022 21:37:24 +0100 Subject: [PATCH 14/73] Make connection_string and connections_strings available for azure functions --- src/Farmer/Builders/Builders.Functions.fs | 3 +- src/Farmer/Builders/Builders.WebApp.fs | 35 ++++++++++++++--------- src/Tests/Functions.fs | 16 +++++++++++ 3 files changed, 39 insertions(+), 15 deletions(-) diff --git a/src/Farmer/Builders/Builders.Functions.fs b/src/Farmer/Builders/Builders.Functions.fs index a9bf1db5d..eef32ec72 100644 --- a/src/Farmer/Builders/Builders.Functions.fs +++ b/src/Farmer/Builders/Builders.Functions.fs @@ -200,7 +200,7 @@ type FunctionsConfig = Location = location Cors = this.CommonWebConfig.Cors Tags = this.Tags - ConnectionStrings = Some Map.empty + ConnectionStrings = Some this.CommonWebConfig.ConnectionStrings AppSettings = Some functionsSettings Identity = this.CommonWebConfig.Identity KeyVaultReferenceIdentity = this.CommonWebConfig.KeyVaultReferenceIdentity @@ -331,6 +331,7 @@ type FunctionsBuilder() = { Name = WebAppName.Empty AlwaysOn = false AppInsights = Some (derived (fun name -> components.resourceId (name-"ai"))) + ConnectionStrings = Map.empty Cors = None FTPState = None HTTPSOnly = false diff --git a/src/Farmer/Builders/Builders.WebApp.fs b/src/Farmer/Builders/Builders.WebApp.fs index 96c12dfe8..a9b7f4bdc 100644 --- a/src/Farmer/Builders/Builders.WebApp.fs +++ b/src/Farmer/Builders/Builders.WebApp.fs @@ -166,6 +166,7 @@ type CommonWebConfig = { Name : WebAppName AlwaysOn : bool AppInsights : ResourceRef option + ConnectionStrings : Map Cors : Cors option FTPState : FTPState option HTTPSOnly : bool @@ -185,7 +186,6 @@ type WebAppConfig = HTTP20Enabled : bool option ClientAffinityEnabled : bool option WebSocketsEnabled: bool option - ConnectionStrings : Map Dependencies : ResourceId Set Tags : Map Sku : Sku @@ -329,7 +329,7 @@ type WebAppConfig = KeyVaultReferenceIdentity = this.CommonWebConfig.KeyVaultReferenceIdentity Cors = this.CommonWebConfig.Cors Tags = this.Tags - ConnectionStrings = Some this.ConnectionStrings + ConnectionStrings = Some this.CommonWebConfig.ConnectionStrings WorkerProcess = this.CommonWebConfig.WorkerProcess AppSettings = Some siteSettings Kind = [ @@ -550,6 +550,7 @@ type WebAppBuilder() = { Name = WebAppName.Empty AlwaysOn = false AppInsights = Some (derived (fun name -> components.resourceId (name-"ai"))) + ConnectionStrings = Map.empty Cors = None HTTPSOnly = false Identity = ManagedIdentity.Empty @@ -572,7 +573,6 @@ type WebAppBuilder() = HTTP20Enabled = None ClientAffinityEnabled = None WebSocketsEnabled = None - ConnectionStrings = Map.empty Tags = Map.empty Dependencies = Set.empty Runtime = Runtime.DotNetCoreLts @@ -624,17 +624,6 @@ type WebAppBuilder() = /// Sets the node version of the web app. [] member _.NodeVersion(state:WebAppConfig, version) = { state with WebsiteNodeDefaultVersion = Some version } - /// Creates a set of connection strings of the web app whose values will be supplied as secret parameters. - [] - member _.AddConnectionString(state:WebAppConfig, key) = - { state with ConnectionStrings = state.ConnectionStrings.Add(key, (ParameterSetting (SecureParameter key), Custom)) } - member _.AddConnectionString(state:WebAppConfig, (key, value:ArmExpression)) = - { state with ConnectionStrings = state.ConnectionStrings.Add(key, (ExpressionSetting value, Custom)) } - /// Creates a set of connection strings of the web app whose values will be supplied as secret parameters. - [] - member this.AddConnectionStrings(state:WebAppConfig, connectionStrings) = - connectionStrings - |> List.fold (fun (state:WebAppConfig) (key:string) -> this.AddConnectionString(state, key)) state /// Enables HTTP 2.0 for this webapp. [] member _.Http20Enabled(state:WebAppConfig) = { state with HTTP20Enabled = Some true } @@ -777,6 +766,24 @@ module Extensions = settings |> List.fold (fun (state:CommonWebConfig) (key, value:ArmExpression) -> { state with Settings = state.Settings.Add(key, ExpressionSetting value) }) current |> this.Wrap state + /// Creates a set of connection strings of the web app whose values will be supplied as secret parameters. + [] + member this.AddConnectionString(state:'T, key) = + let current = this.Get state + { current with ConnectionStrings = current.ConnectionStrings.Add(key, (ParameterSetting (SecureParameter key), Custom)) } + |> this.Wrap state + member this.AddConnectionString(state:'T, (key, value:ArmExpression)) = + let current = this.Get state + { current with ConnectionStrings = current.ConnectionStrings.Add(key, (ExpressionSetting value, Custom)) } + |> this.Wrap state + /// Creates a set of connection strings of the web app whose values will be supplied as secret parameters. + [] + member this.AddConnectionStrings(state:'T, connectionStrings) = + let current = this.Get state + connectionStrings + |> List.fold (fun (state:CommonWebConfig) (key, value:ArmExpression) -> + { state with ConnectionStrings = state.ConnectionStrings.Add(key, (ExpressionSetting value, Custom)) }) current + |> this.Wrap state /// Sets an app setting of the web app in the form "key" "value". [] member this.AddIdentity (state:'T, identity:UserAssignedIdentity) = diff --git a/src/Tests/Functions.fs b/src/Tests/Functions.fs index 9463fc4b6..3b7d33c1b 100644 --- a/src/Tests/Functions.fs +++ b/src/Tests/Functions.fs @@ -364,4 +364,20 @@ let tests = testList "Functions tests" [ |> Seq.map (fun x-> x.ToObject<{|name:string;value:string|}> ()) Expect.contains appSettings {|name="APPINSIGHTS_INSTRUMENTATIONKEY"; value="[reference(resourceId('shared-group', 'Microsoft.Insights/components', 'theName'), '2014-04-01').InstrumentationKey]"|} "Invalid value for APPINSIGHTS_INSTRUMENTATIONKEY" } + + test "Function app correctly adds connection strings" { + let sa = storageAccount { name "foo" } + let wa = + let resources = functions { name "test"; connection_string "a"; connection_string ("b", sa.Key) } |> getResources + resources |> getResource |> List.head + + let expected = [ + "a", (ParameterSetting(SecureParameter "a"), Custom) + "b", (ExpressionSetting sa.Key, Custom) + ] + let parameters = wa :> IParameters + + Expect.equal wa.ConnectionStrings (Map expected |> Some) "Missing connections" + Expect.equal parameters.SecureParameters [ SecureParameter "a" ] "Missing parameter" + } ] \ No newline at end of file From 87a72a1dcef144eb45d1972c1540dac99e572ade Mon Sep 17 00:00:00 2001 From: Amine Mejaouel <45773049+amine-mejaouel@users.noreply.github.com> Date: Sun, 6 Feb 2022 21:38:05 +0100 Subject: [PATCH 15/73] Add connection string docs for Azure Functions --- docs/content/api-overview/resources/functions.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/content/api-overview/resources/functions.md b/docs/content/api-overview/resources/functions.md index 10208fa72..229f45777 100644 --- a/docs/content/api-overview/resources/functions.md +++ b/docs/content/api-overview/resources/functions.md @@ -35,6 +35,8 @@ The Functions builder is used to create Azure Functions accounts. It abstracts t | setting | Sets an app setting of the web app in the form "key" "value". | | secret_setting | Sets a "secret" app setting of the function. You must supply the "key", whilst the value will be supplied as a secure parameter or an ARM expression. | | settings | Sets a list of app setting of the web app as tuples in the form of ("key", "value"). | +| connection_string | Creates a connection string whose value is supplied as secret parameter, or as an ARM expression in the tupled form of ("key", expr). | +| connection_strings | Creates a set of connection strings whose values will be supplied as secret parameters. | | depends_on | [Sets dependencies for the web app.](../../dependencies/) | | enable_cors | Enables CORS support for the app. Either specify AllOrigins or a list of valid URIs. | | enable_cors_credentials | Allows CORS requests with credentials. | From 934eaaf6587af65b011dd03cea81ff401278eed3 Mon Sep 17 00:00:00 2001 From: Nargiz Gasimova <3729486+nargiz@users.noreply.github.com> Date: Fri, 11 Feb 2022 11:41:09 +0000 Subject: [PATCH 16/73] ZoneRedundant for service plan --- RELEASE_NOTES.md | 1 + .../content/api-overview/resources/web-app.md | 1 + src/Farmer/Arm/Web.fs | 4 +++- src/Farmer/Builders/Builders.Functions.fs | 1 + src/Farmer/Builders/Builders.ServicePlan.fs | 5 +++++ src/Farmer/Builders/Builders.WebApp.fs | 10 ++++++++-- src/Tests/AllTests.fs | 1 + src/Tests/ServicePlan.fs | 19 +++++++++++++++++++ src/Tests/Tests.fsproj | 1 + src/Tests/WebApp.fs | 7 +++++++ 10 files changed, 47 insertions(+), 3 deletions(-) create mode 100644 src/Tests/ServicePlan.fs diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index cc3e95f20..a0df5a853 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -3,6 +3,7 @@ Release Notes ## vNext * WebApps/Functions: Don't turn on Logging Extension for Linux App Service. * WebApps/Functions: Fix .NET 5/6 on Linux deployments. +* ServicePlan/WebApp: Support for enabling ZoneDedundant ## 1.6.25 * CosmosDb: Add support for serverless capacity mode. diff --git a/docs/content/api-overview/resources/web-app.md b/docs/content/api-overview/resources/web-app.md index 28607f20d..83d6e728d 100644 --- a/docs/content/api-overview/resources/web-app.md +++ b/docs/content/api-overview/resources/web-app.md @@ -63,6 +63,7 @@ The Web App builder is used to create Azure App Service accounts. It abstracts t | Service Plan | sku | Sets the sku of the service plan. | | Service Plan | worker_size | Sets the size of the service plan worker. | | Service Plan | number_of_workers | Sets the number of instances on the service plan. | +| Service Plan | enable_zone_redundant | Enables ZoneRedundant on the service plan. | > **Farmer also comes with a dedicated Service Plan builder** that contains all of the above keywords that apply to a Service Plan. > diff --git a/src/Farmer/Arm/Web.fs b/src/Farmer/Arm/Web.fs index 37f736c21..15e28a230 100644 --- a/src/Farmer/Arm/Web.fs +++ b/src/Farmer/Arm/Web.fs @@ -29,6 +29,7 @@ type ServerFarm = WorkerCount : int MaximumElasticWorkerCount : int option OperatingSystem : OS + ZoneRedundant : bool option Tags: Map } member this.IsDynamic = match this.Sku, this.WorkerSize with @@ -106,7 +107,8 @@ type ServerFarm = computeMode = if this.IsDynamic then "Dynamic" else null perSiteScaling = if this.IsDynamic then Nullable() else Nullable false reserved = this.Reserved - maximumElasticWorkerCount = this.MaximumElasticWorkerCount |> Option.toNullable |} + maximumElasticWorkerCount = this.MaximumElasticWorkerCount |> Option.toNullable + zoneRedundant = this.ZoneRedundant |> Option.toNullable |} kind = this.Kind |> Option.toObj |} diff --git a/src/Farmer/Builders/Builders.Functions.fs b/src/Farmer/Builders/Builders.Functions.fs index a9bf1db5d..5261dbafb 100644 --- a/src/Farmer/Builders/Builders.Functions.fs +++ b/src/Farmer/Builders/Builders.Functions.fs @@ -284,6 +284,7 @@ type FunctionsConfig = WorkerCount = 0 MaximumElasticWorkerCount = None OperatingSystem = this.CommonWebConfig.OperatingSystem + ZoneRedundant = None Tags = this.Tags } | _ -> () diff --git a/src/Farmer/Builders/Builders.ServicePlan.fs b/src/Farmer/Builders/Builders.ServicePlan.fs index 49f9147b1..12552daf8 100644 --- a/src/Farmer/Builders/Builders.ServicePlan.fs +++ b/src/Farmer/Builders/Builders.ServicePlan.fs @@ -12,6 +12,7 @@ type ServicePlanConfig = WorkerCount : int MaximumElasticWorkerCount : int option OperatingSystem : OS + ZoneRedundant : bool option Tags : Map } interface IBuilder with member this.ResourceId = serverFarms.resourceId this.Name @@ -23,6 +24,7 @@ type ServicePlanConfig = OperatingSystem = this.OperatingSystem WorkerCount = this.WorkerCount MaximumElasticWorkerCount = this.MaximumElasticWorkerCount + ZoneRedundant = this.ZoneRedundant Tags = this.Tags } ] @@ -34,6 +36,7 @@ type ServicePlanBuilder() = WorkerCount = 1 MaximumElasticWorkerCount = None OperatingSystem = Windows + ZoneRedundant = None Tags = Map.empty } [] /// Sets the name of the Server Farm. @@ -56,6 +59,8 @@ type ServicePlanBuilder() = [] /// Configures this server farm to host serverless functions, not web apps. member _.Serverless(state:ServicePlanConfig) = { state with Sku = Dynamic; WorkerSize = Serverless } + [] + member _.ZoneRedundant(state:ServicePlanConfig) = {state with ZoneRedundant = Some true} interface ITaggable with member _.Add state tags = { state with Tags = state.Tags |> Map.merge tags } let servicePlan = ServicePlanBuilder() \ No newline at end of file diff --git a/src/Farmer/Builders/Builders.WebApp.fs b/src/Farmer/Builders/Builders.WebApp.fs index 96c12dfe8..e45f65c6e 100644 --- a/src/Farmer/Builders/Builders.WebApp.fs +++ b/src/Farmer/Builders/Builders.WebApp.fs @@ -202,7 +202,8 @@ type WebAppConfig = AutomaticLoggingExtension : bool SiteExtensions : ExtensionName Set PrivateEndpoints: (LinkedResource * string option) Set - CustomDomain : DomainConfig } + CustomDomain : DomainConfig + ZoneRedundant : bool option } member this.Name = this.CommonWebConfig.Name /// Gets this web app's Server Plan's full resource ID. member this.ServicePlanId = this.CommonWebConfig.ServicePlan.resourceId this.Name.ResourceName @@ -467,6 +468,7 @@ type WebAppConfig = WorkerCount = this.WorkerCount MaximumElasticWorkerCount = this.MaximumElasticWorkerCount OperatingSystem = this.CommonWebConfig.OperatingSystem + ZoneRedundant = this.ZoneRedundant Tags = this.Tags} | _ -> () @@ -583,7 +585,8 @@ type WebAppBuilder() = AutomaticLoggingExtension = true SiteExtensions = Set.empty PrivateEndpoints = Set.empty - CustomDomain = NoDomain} + CustomDomain = NoDomain + ZoneRedundant = None} member _.Run(state:WebAppConfig) = if state.Name.ResourceName = ResourceName.Empty then raiseFarmer "No Web App name has been set." { state with @@ -690,6 +693,9 @@ type WebAppBuilder() = member _.CustomDomain(state:WebAppConfig, domainConfig) = { state with CustomDomain = domainConfig } member _.CustomDomain(state:WebAppConfig, customDomain) = { state with CustomDomain = SecureDomain (customDomain,AppManagedCertificate) } member _.CustomDomain(state:WebAppConfig, (customDomain,thumbprint)) = { state with CustomDomain = SecureDomain (customDomain,CustomCertificate thumbprint) } + /// Enables the zone redundancy in service plan + [] + member this.ZoneRedundant(state:WebAppConfig) = {state with ZoneRedundant = Some true} interface IPrivateEndpoints with member _.Add state endpoints = { state with PrivateEndpoints = state.PrivateEndpoints |> Set.union endpoints} interface ITaggable with member _.Add state tags = { state with Tags = state.Tags |> Map.merge tags } diff --git a/src/Tests/AllTests.fs b/src/Tests/AllTests.fs index 7a7d2efd1..27309a37a 100644 --- a/src/Tests/AllTests.fs +++ b/src/Tests/AllTests.fs @@ -60,6 +60,7 @@ let allTests = WebApp.tests Dashboards.tests Alerts.tests + ServicePlan.tests ] testList "Control" [ if (hasEnv "TF_BUILD" "True" && notEnv "BUILD_REASON" "PullRequest") || hasEnv "FARMER_E2E" "True" then diff --git a/src/Tests/ServicePlan.fs b/src/Tests/ServicePlan.fs new file mode 100644 index 000000000..17fe2ab67 --- /dev/null +++ b/src/Tests/ServicePlan.fs @@ -0,0 +1,19 @@ +module ServicePlan + +open Expecto +open Farmer +open Farmer.Builders +open Farmer.Arm +open System + +let getResource<'T when 'T :> IArmResource> (data:IArmResource list) = data |> List.choose(function :? 'T as x -> Some x | _ -> None) +let getResources (v:IBuilder) = v.BuildResources Location.WestEurope + +let tests = testList "Service Plan Tests"[ + test "Enable zoneRedundant in service plan" { + let resources = webApp { name "test"; enable_zone_redundant } |> getResources + let sf = resources |> getResource |> List.head + + Expect.equal sf.ZoneRedundant (Some true) "ZoneRedundant should be enabled" + } +] \ No newline at end of file diff --git a/src/Tests/Tests.fsproj b/src/Tests/Tests.fsproj index 241a78a48..72bf80c6d 100644 --- a/src/Tests/Tests.fsproj +++ b/src/Tests/Tests.fsproj @@ -7,6 +7,7 @@ + diff --git a/src/Tests/WebApp.fs b/src/Tests/WebApp.fs index 0e9518b6a..c128345b5 100644 --- a/src/Tests/WebApp.fs +++ b/src/Tests/WebApp.fs @@ -716,4 +716,11 @@ let tests = testList "Web App Tests" [ let extensions = wa |> getResources |> getResource Expect.isEmpty extensions "Should not be any extensions" } + + test "Web App enables zoneRedundant in service plan" { + let resources = webApp { name "test"; enable_zone_redundant } |> getResources + let sf = resources |> getResource |> List.head + + Expect.equal sf.ZoneRedundant (Some true) "ZoneRedundant should be enabled" + } ] From f9de8d421454a9abe2f7affcd5642856a1cc01b7 Mon Sep 17 00:00:00 2001 From: Nargiz Gasimova <3729486+nargiz@users.noreply.github.com> Date: Fri, 11 Feb 2022 14:58:25 +0000 Subject: [PATCH 17/73] more test cases for service plan --- src/Tests/ServicePlan.fs | 32 +++++++++++++++++++++++++++----- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/src/Tests/ServicePlan.fs b/src/Tests/ServicePlan.fs index 17fe2ab67..b43f8382f 100644 --- a/src/Tests/ServicePlan.fs +++ b/src/Tests/ServicePlan.fs @@ -9,11 +9,33 @@ open System let getResource<'T when 'T :> IArmResource> (data:IArmResource list) = data |> List.choose(function :? 'T as x -> Some x | _ -> None) let getResources (v:IBuilder) = v.BuildResources Location.WestEurope -let tests = testList "Service Plan Tests"[ +let tests = testList "Service Plan Tests" [ + test "Basic service plan does not have zone redundancy" { + let servicePlan = servicePlan { name "test" } + let sf = servicePlan |> getResources |> getResource |> List.head + + let template = arm{ add_resource servicePlan} + let jobj = template.Template |> Writer.toJson |> Newtonsoft.Json.Linq.JObject.Parse + + let zoneRedundant = + jobj.SelectToken($"$..resources[?(@.type=='Microsoft.Web/serverfarms')].properties.zoneRedundant") + + Expect.equal sf.ZoneRedundant None "ZoneRedundant should not be set" + Expect.isNull zoneRedundant "Template should not include zone redundancy information" + } + test "Enable zoneRedundant in service plan" { - let resources = webApp { name "test"; enable_zone_redundant } |> getResources - let sf = resources |> getResource |> List.head + let servicePlan = servicePlan { name "test"; enable_zone_redundant } + let sf = servicePlan |> getResources |> getResource |> List.head + + let template = arm{ add_resource servicePlan} + let jobj = template.Template |> Writer.toJson |> Newtonsoft.Json.Linq.JObject.Parse + + let zoneRedundant = + jobj.SelectToken($"$..resources[?(@.type=='Microsoft.Web/serverfarms')].properties.zoneRedundant") - Expect.equal sf.ZoneRedundant (Some true) "ZoneRedundant should be enabled" - } + Expect.equal sf.ZoneRedundant (Some true) "ZoneRedundant should be enabled" + Expect.isNotNull zoneRedundant "Template should include zone redundancy information" + Expect.equal (zoneRedundant.ToString().ToLower()) "true" "ZoneRedundant should be set to true" + } ] \ No newline at end of file From 5dab8e7607f7f705d3f3b09ac2558e4b274e87c3 Mon Sep 17 00:00:00 2001 From: isaac Date: Sat, 12 Feb 2022 00:12:57 +0100 Subject: [PATCH 18/73] Tiny cleanup --- src/Farmer/Builders/Builders.WebApp.fs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/Farmer/Builders/Builders.WebApp.fs b/src/Farmer/Builders/Builders.WebApp.fs index 33eee06ae..c0daed8f4 100644 --- a/src/Farmer/Builders/Builders.WebApp.fs +++ b/src/Farmer/Builders/Builders.WebApp.fs @@ -285,11 +285,9 @@ type WebAppConfig = | Linux, Some _ | _ , None -> () - - match this.DockerPort with - | Some port -> AppSettings.WebsitesPort port - | None -> () - + + yield! this.DockerPort |> Option.mapList AppSettings.WebsitesPort + if this.DockerCi then "DOCKER_ENABLE_CI", "true" ] @@ -699,7 +697,7 @@ type WebAppBuilder() = member _.CustomDomain(state:WebAppConfig, (customDomain,thumbprint)) = { state with CustomDomain = SecureDomain (customDomain,CustomCertificate thumbprint) } /// Map specified port traffic from your docker container to port 80 for App Service [] - member _.DockerPort(state: WebAppConfig, dockerPort:int) = { state with DockerPort = Some(dockerPort)} + member _.DockerPort(state: WebAppConfig, dockerPort:int) = { state with DockerPort = Some dockerPort } interface IPrivateEndpoints with member _.Add state endpoints = { state with PrivateEndpoints = state.PrivateEndpoints |> Set.union endpoints} interface ITaggable with member _.Add state tags = { state with Tags = state.Tags |> Map.merge tags } From db6733191c089d4e514877e893430af0d63a66f2 Mon Sep 17 00:00:00 2001 From: isaac Date: Sat, 12 Feb 2022 00:15:10 +0100 Subject: [PATCH 19/73] Fix release notes --- RELEASE_NOTES.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 68dc55b69..c11c78840 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,9 +1,11 @@ Release Notes ============= ## vNext +* WebApp: Support custom port for docker container with `docker_port` + +## 1.6.26 * WebApps/Functions: Don't turn on Logging Extension for Linux App Service. * WebApps/Functions: Fix .NET 5/6 on Linux deployments. -* WebApp: Support custom port for docker container with `docker_port` ## 1.6.25 * CosmosDb: Add support for serverless capacity mode. From 41864e009647568ee5613e6ceebc88f2a3d07ba6 Mon Sep 17 00:00:00 2001 From: Dave Curylo Date: Sun, 13 Feb 2022 11:11:23 -0500 Subject: [PATCH 20/73] Support for CIDR network blocks on WebApp IP address restrictions --- src/Farmer/Arm/Web.fs | 10 +- src/Farmer/Builders/Builders.WebApp.fs | 20 +++- src/Farmer/Common.fs | 152 ++++++++++++------------- src/Tests/Functions.fs | 8 +- src/Tests/WebApp.fs | 8 +- 5 files changed, 105 insertions(+), 93 deletions(-) diff --git a/src/Farmer/Arm/Web.fs b/src/Farmer/Arm/Web.fs index b82cee2e6..53ba80bde 100644 --- a/src/Farmer/Arm/Web.fs +++ b/src/Farmer/Arm/Web.fs @@ -277,12 +277,12 @@ type Site = match this.IpSecurityRestrictions with | [] -> null | restrictions -> - restrictions + restrictions |> List.mapi (fun index restriction -> - {| ipAddress = $"%s{restriction.IpAddress.ToString()}/32" - name = restriction.Name - action = restriction.Action.ToString() - priority = index + 1 |}) |> box + {| ipAddress = IPAddressCidr.format restriction.IpAddressCidr + name = restriction.Name + action = restriction.Action.ToString() + priority = index + 1 |}) |> box pythonVersion = this.PythonVersion |> Option.toObj http20Enabled = this.HTTP20Enabled |> Option.toNullable webSocketsEnabled = this.WebSocketsEnabled |> Option.toNullable diff --git a/src/Farmer/Builders/Builders.WebApp.fs b/src/Farmer/Builders/Builders.WebApp.fs index e8ce16b00..314fe0eff 100644 --- a/src/Farmer/Builders/Builders.WebApp.fs +++ b/src/Farmer/Builders/Builders.WebApp.fs @@ -163,12 +163,24 @@ type SlotBuilder() = /// Add Allowed ip for ip security restrictions [] - member this.AllowIp(state, name, ip) : SlotConfig = - { state with IpSecurityRestrictions = IpSecurityRestriction.Create name ip Allow :: state.IpSecurityRestrictions } + member _.AllowIp(state, name, cidr:IPAddressCidr) : SlotConfig = + { state with IpSecurityRestrictions = IpSecurityRestriction.Create name cidr Allow :: state.IpSecurityRestrictions } + member this.AllowIp(state, name, ip:Net.IPAddress) : SlotConfig = + let cidr = { Address = ip; Prefix = 32 } + this.AllowIp(state, name, cidr) + member this.AllowIp(state, name, ip:string) : SlotConfig = + let cidr = { Address = Net.IPAddress.Parse ip; Prefix = 32 } + this.AllowIp(state, name, cidr) /// Add Denied ip for ip security restrictions [] - member this.DenyIp(state, name, ip) : SlotConfig = - { state with IpSecurityRestrictions = IpSecurityRestriction.Create name ip Deny :: state.IpSecurityRestrictions } + member _.DenyIp(state, name, cidr:IPAddressCidr) : SlotConfig = + { state with IpSecurityRestrictions = IpSecurityRestriction.Create name cidr Deny :: state.IpSecurityRestrictions } + member this.DenyIp(state, name, ip:Net.IPAddress) : SlotConfig = + let cidr = { Address = ip; Prefix = 32 } + this.DenyIp(state, name, cidr) + member this.DenyIp(state, name, ip:string) : SlotConfig = + let cidr = { Address = Net.IPAddress.Parse ip; Prefix = 32 } + this.DenyIp(state, name, cidr) interface ITaggable with member _.Add state tags = { state with Tags = state.Tags |> Map.merge tags } interface IDependable with member _.Add state newDeps = { state with Dependencies = state.Dependencies + newDeps } diff --git a/src/Farmer/Common.fs b/src/Farmer/Common.fs index 4b1f40158..c794170e3 100644 --- a/src/Farmer/Common.fs +++ b/src/Farmer/Common.fs @@ -595,6 +595,79 @@ module Storage = [] type StorageService = Blobs | Tables | Files | Queues +/// A network represented by an IP address and CIDR prefix. +type public IPAddressCidr = + { Address : System.Net.IPAddress + Prefix : int } + +/// Functions for IP networks and CIDR notation. +module IPAddressCidr = + let parse (s:string) : IPAddressCidr = + match s.Split([|'/'|], System.StringSplitOptions.RemoveEmptyEntries) with + [| ip; prefix |] -> + { Address = System.Net.IPAddress.Parse (ip.Trim ()) + Prefix = int prefix } + | _ -> raise (System.ArgumentOutOfRangeException "Malformed CIDR, expecting an IP and prefix separated by '/'") + let safeParse (s:string) : Result = + try parse s |> Ok + with ex -> Error ex + let format (cidr:IPAddressCidr) = $"{cidr.Address}/{cidr.Prefix}" + /// Gets uint32 representation of an IP address. + let private num (ip:System.Net.IPAddress) = + ip.GetAddressBytes() |> Array.rev |> fun bytes -> BitConverter.ToUInt32 (bytes, 0) + /// Gets IP address from uint32 representations + let private ofNum (num:uint32) = + num |> BitConverter.GetBytes |> Array.rev |> System.Net.IPAddress + let private ipRangeNums (cidr:IPAddressCidr) = + let ipNumber = cidr.Address |> num + let mask = 0xffffffffu <<< (32 - cidr.Prefix) + ipNumber &&& mask, ipNumber ||| (mask ^^^ 0xffffffffu) + /// Indicates if one CIDR block can fit entirely within another CIDR block + let contains (inner:IPAddressCidr) (outer:IPAddressCidr) = + // outer |> IPAddressCidr.contains inner + let innerStart, innerFinish = ipRangeNums inner + let outerStart, outerFinish = ipRangeNums outer + outerStart <= innerStart && outerFinish >= innerFinish + /// Calculates a range of IP addresses from an CIDR block. + let ipRange (cidr:IPAddressCidr) = + let first, last = ipRangeNums cidr + first |> ofNum, last |> ofNum + /// Sequence of IP addresses for a CIDR block. + let addresses (cidr:IPAddressCidr) = + let first, last = ipRangeNums cidr + seq { for i in first..last do ofNum i } + /// Carve a subnet out of an address space. + let carveAddressSpace (addressSpace:IPAddressCidr) (subnetSizes:int list) = [ + let addressSpaceStart, addressSpaceEnd = addressSpace |> ipRangeNums + let mutable startAddress = addressSpaceStart |> ofNum + let mutable index = 0 + for size in subnetSizes do + index <- index + 1 + let cidr = { Address = startAddress; Prefix = size } + let first, last = cidr |> ipRangeNums + let overlapping = first < (startAddress |> num) + let last, cidr = + if overlapping then + let cidr = { Address = ofNum (last + 1u); Prefix = size } + let _, last = cidr |> ipRangeNums + last, cidr + else + last, cidr + if last <= addressSpaceEnd then + startAddress <- (last + 1u) |> ofNum + cidr + else + raise (IndexOutOfRangeException $"Unable to create subnet {index} of /{size}") + ] + + /// The first two addresses are the network address and gateway address + /// so not assignable. + let assignable (cidr:IPAddressCidr) = + if cidr.Prefix < 31 then // only has 2 addresses + cidr |> addresses |> Seq.skip 2 + else + Seq.empty + module WebApp = type WorkerSize = Small | Medium | Large | Serverless type Cors = AllOrigins | SpecificOrigins of origins : Uri list * allowCredentials : bool option @@ -641,11 +714,11 @@ module WebApp = | Deny type IpSecurityRestriction = { Name: string - IpAddress: System.Net.IPAddress + IpAddressCidr: IPAddressCidr Action: IpSecurityAction } - static member Create name ip action = + static member Create name cidr action = { Name = name - IpAddress = ip + IpAddressCidr = cidr Action = action } module Extensions = /// The Microsoft.AspNetCore.AzureAppServices logging extension. @@ -1513,79 +1586,6 @@ module DataLake = | Commitment_1PB | Commitment_5PB -/// A network represented by an IP address and CIDR prefix. -type public IPAddressCidr = - { Address : System.Net.IPAddress - Prefix : int } - -/// Functions for IP networks and CIDR notation. -module IPAddressCidr = - let parse (s:string) : IPAddressCidr = - match s.Split([|'/'|], System.StringSplitOptions.RemoveEmptyEntries) with - [| ip; prefix |] -> - { Address = System.Net.IPAddress.Parse (ip.Trim ()) - Prefix = int prefix } - | _ -> raise (System.ArgumentOutOfRangeException "Malformed CIDR, expecting an IP and prefix separated by '/'") - let safeParse (s:string) : Result = - try parse s |> Ok - with ex -> Error ex - let format (cidr:IPAddressCidr) = $"{cidr.Address}/{cidr.Prefix}" - /// Gets uint32 representation of an IP address. - let private num (ip:System.Net.IPAddress) = - ip.GetAddressBytes() |> Array.rev |> fun bytes -> BitConverter.ToUInt32 (bytes, 0) - /// Gets IP address from uint32 representations - let private ofNum (num:uint32) = - num |> BitConverter.GetBytes |> Array.rev |> System.Net.IPAddress - let private ipRangeNums (cidr:IPAddressCidr) = - let ipNumber = cidr.Address |> num - let mask = 0xffffffffu <<< (32 - cidr.Prefix) - ipNumber &&& mask, ipNumber ||| (mask ^^^ 0xffffffffu) - /// Indicates if one CIDR block can fit entirely within another CIDR block - let contains (inner:IPAddressCidr) (outer:IPAddressCidr) = - // outer |> IPAddressCidr.contains inner - let innerStart, innerFinish = ipRangeNums inner - let outerStart, outerFinish = ipRangeNums outer - outerStart <= innerStart && outerFinish >= innerFinish - /// Calculates a range of IP addresses from an CIDR block. - let ipRange (cidr:IPAddressCidr) = - let first, last = ipRangeNums cidr - first |> ofNum, last |> ofNum - /// Sequence of IP addresses for a CIDR block. - let addresses (cidr:IPAddressCidr) = - let first, last = ipRangeNums cidr - seq { for i in first..last do ofNum i } - /// Carve a subnet out of an address space. - let carveAddressSpace (addressSpace:IPAddressCidr) (subnetSizes:int list) = [ - let addressSpaceStart, addressSpaceEnd = addressSpace |> ipRangeNums - let mutable startAddress = addressSpaceStart |> ofNum - let mutable index = 0 - for size in subnetSizes do - index <- index + 1 - let cidr = { Address = startAddress; Prefix = size } - let first, last = cidr |> ipRangeNums - let overlapping = first < (startAddress |> num) - let last, cidr = - if overlapping then - let cidr = { Address = ofNum (last + 1u); Prefix = size } - let _, last = cidr |> ipRangeNums - last, cidr - else - last, cidr - if last <= addressSpaceEnd then - startAddress <- (last + 1u) |> ofNum - cidr - else - raise (IndexOutOfRangeException $"Unable to create subnet {index} of /{size}") - ] - - /// The first two addresses are the network address and gateway address - /// so not assignable. - let assignable (cidr:IPAddressCidr) = - if cidr.Prefix < 31 then // only has 2 addresses - cidr |> addresses |> Seq.skip 2 - else - Seq.empty - module Network = type SubnetDelegationService = SubnetDelegationService of string with diff --git a/src/Tests/Functions.fs b/src/Tests/Functions.fs index 9b651729a..6fdb9fcf7 100644 --- a/src/Tests/Functions.fs +++ b/src/Tests/Functions.fs @@ -366,7 +366,7 @@ let tests = testList "Functions tests" [ } test "Supports adding ip restriction" { - let ip = System.Net.IPAddress.Parse "1.2.3.4" + let ip = IPAddressCidr.parse "1.2.3.4/32" let resources = functions { name "test"; add_allowed_ip_restriction "test-rule" ip } |> getResources let site = resources |> getResource |> List.head @@ -374,7 +374,7 @@ let tests = testList "Functions tests" [ Expect.equal site.IpSecurityRestrictions [ expectedRestriction ] "Should add expected ip security restriction" } test "Supports adding ip restriction for denied ip" { - let ip = System.Net.IPAddress.Parse "1.2.3.4" + let ip = IPAddressCidr.parse "1.2.3.4/32" let resources = functions { name "test"; add_denied_ip_restriction "test-rule" ip } |> getResources let site = resources |> getResource |> List.head @@ -382,8 +382,8 @@ let tests = testList "Functions tests" [ Expect.equal site.IpSecurityRestrictions [ expectedRestriction ] "Should add denied ip security restriction" } test "Supports adding different ip restrictions to site and slot" { - let siteIp = System.Net.IPAddress.Parse "1.2.3.4" - let slotIp = System.Net.IPAddress.Parse "4.3.2.1" + let siteIp = IPAddressCidr.parse "1.2.3.4/32" + let slotIp = IPAddressCidr.parse "4.3.2.1/32" let warmupSlot = appSlot { name "warm-up"; add_allowed_ip_restriction "slot-rule" slotIp } let resources = functions { name "test"; add_slot warmupSlot; add_allowed_ip_restriction "site-rule" siteIp } |> getResources let slot = diff --git a/src/Tests/WebApp.fs b/src/Tests/WebApp.fs index 30234d968..8c46b836c 100644 --- a/src/Tests/WebApp.fs +++ b/src/Tests/WebApp.fs @@ -711,7 +711,7 @@ let tests = testList "Web App Tests" [ Expect.equal hostnameBinding.Length 0 $"There should not be a hostname binding as a result of choosing the 'NoDomain' option" } test "Supports adding ip restriction for allowed ip" { - let ip = System.Net.IPAddress.Parse "1.2.3.4" + let ip = IPAddressCidr.parse "1.2.3.4/32" let resources = webApp { name "test"; add_allowed_ip_restriction "test-rule" ip } |> getResources let site = resources |> getResource |> List.head @@ -719,7 +719,7 @@ let tests = testList "Web App Tests" [ Expect.equal site.IpSecurityRestrictions [ expectedRestriction ] "Should add allowed ip security restriction" } test "Supports adding ip restriction for denied ip" { - let ip = System.Net.IPAddress.Parse "1.2.3.4" + let ip = IPAddressCidr.parse "1.2.3.4/32" let resources = webApp { name "test"; add_denied_ip_restriction "test-rule" ip } |> getResources let site = resources |> getResource |> List.head @@ -727,8 +727,8 @@ let tests = testList "Web App Tests" [ Expect.equal site.IpSecurityRestrictions [ expectedRestriction ] "Should add denied ip security restriction" } test "Supports adding different ip restrictions to site and slot" { - let siteIp = System.Net.IPAddress.Parse "1.2.3.4" - let slotIp = System.Net.IPAddress.Parse "4.3.2.1" + let siteIp = IPAddressCidr.parse "1.2.3.4/32" + let slotIp = IPAddressCidr.parse "4.3.2.1/32" let warmupSlot = appSlot { name "warm-up"; add_allowed_ip_restriction "slot-rule" slotIp } let resources = webApp { name "test"; add_slot warmupSlot; add_allowed_ip_restriction "site-rule" siteIp } |> getResources let slot = From 561d3b75eeed8e2ac2aad2fe862e4ff8be50307e Mon Sep 17 00:00:00 2001 From: Dave Curylo Date: Sun, 13 Feb 2022 21:05:54 -0500 Subject: [PATCH 21/73] Appending webapp restriction rules to end of list --- src/Farmer/Builders/Builders.WebApp.fs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Farmer/Builders/Builders.WebApp.fs b/src/Farmer/Builders/Builders.WebApp.fs index 314fe0eff..cbe347f74 100644 --- a/src/Farmer/Builders/Builders.WebApp.fs +++ b/src/Farmer/Builders/Builders.WebApp.fs @@ -164,7 +164,7 @@ type SlotBuilder() = /// Add Allowed ip for ip security restrictions [] member _.AllowIp(state, name, cidr:IPAddressCidr) : SlotConfig = - { state with IpSecurityRestrictions = IpSecurityRestriction.Create name cidr Allow :: state.IpSecurityRestrictions } + { state with IpSecurityRestrictions = state.IpSecurityRestrictions @ [IpSecurityRestriction.Create name cidr Allow] } member this.AllowIp(state, name, ip:Net.IPAddress) : SlotConfig = let cidr = { Address = ip; Prefix = 32 } this.AllowIp(state, name, cidr) @@ -174,7 +174,7 @@ type SlotBuilder() = /// Add Denied ip for ip security restrictions [] member _.DenyIp(state, name, cidr:IPAddressCidr) : SlotConfig = - { state with IpSecurityRestrictions = IpSecurityRestriction.Create name cidr Deny :: state.IpSecurityRestrictions } + { state with IpSecurityRestrictions = state.IpSecurityRestrictions @ [IpSecurityRestriction.Create name cidr Deny] } member this.DenyIp(state, name, ip:Net.IPAddress) : SlotConfig = let cidr = { Address = ip; Prefix = 32 } this.DenyIp(state, name, cidr) From 31ff0cacec0d7229bd6456e7ae6f4d6436900844 Mon Sep 17 00:00:00 2001 From: Dave Curylo Date: Sun, 13 Feb 2022 21:16:41 -0500 Subject: [PATCH 22/73] Update RELEASE_NOTES.md --- RELEASE_NOTES.md | 1 + 1 file changed, 1 insertion(+) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index cc3e95f20..f81a98d90 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,6 +1,7 @@ Release Notes ============= ## vNext +* Functions: Make connection_string available for Azure Functions in addtion to WebApps. * WebApps/Functions: Don't turn on Logging Extension for Linux App Service. * WebApps/Functions: Fix .NET 5/6 on Linux deployments. From c3515dc4524f7a202940d41512538f18f374131d Mon Sep 17 00:00:00 2001 From: Dave Curylo Date: Sun, 13 Feb 2022 21:25:54 -0500 Subject: [PATCH 23/73] Clean up 1.6.26 release notes entries. --- RELEASE_NOTES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index c11c78840..c42ac7984 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -2,9 +2,9 @@ Release Notes ============= ## vNext * WebApp: Support custom port for docker container with `docker_port` +* WebApps/Functions: Don't turn on Logging Extension for Linux App Service. ## 1.6.26 -* WebApps/Functions: Don't turn on Logging Extension for Linux App Service. * WebApps/Functions: Fix .NET 5/6 on Linux deployments. ## 1.6.25 From 3d0f1eb146a6a99d244a6fc53ddb7a155f43d50c Mon Sep 17 00:00:00 2001 From: Dave Curylo Date: Sun, 13 Feb 2022 21:28:13 -0500 Subject: [PATCH 24/73] Moving to correct version in release notes --- RELEASE_NOTES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 1e8cce10b..5a0ac11b6 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -2,6 +2,7 @@ Release Notes ============= ## vNext * WebApp: Support custom port for docker container with `docker_port` +* WebApp/Functions: Add support for ip-restriction rules ## 1.6.26 * WebApps/Functions: Don't turn on Logging Extension for Linux App Service. @@ -26,7 +27,6 @@ Release Notes * Log Analytics: Add CustomerId configuration member to Log Analytics * Service Bus: Added additional overloads for topic.duplicate_detection and queue.duplicate_detection * WebApp: Fixed deployment name for nested template in app-managed certificate deployments -* WebApp/Functions: Add support for ip-restriction rules ## 1.6.21 * Alerts: Extend a list of possible criteria for time aggregations and operators From 21686064528c74401f5984abc3fe9876429a9fb4 Mon Sep 17 00:00:00 2001 From: Nargiz Gasimova <3729486+nargiz@users.noreply.github.com> Date: Mon, 14 Feb 2022 10:41:10 +0000 Subject: [PATCH 25/73] use FeatureFlag for ZoneRedundant --- src/Farmer/Arm/Web.fs | 4 ++-- src/Farmer/Builders/Builders.ServicePlan.fs | 6 +++--- src/Farmer/Builders/Builders.WebApp.fs | 15 +++++++-------- src/Tests/ServicePlan.fs | 19 +++++++++++++++++-- src/Tests/WebApp.fs | 14 ++++++-------- 5 files changed, 35 insertions(+), 23 deletions(-) diff --git a/src/Farmer/Arm/Web.fs b/src/Farmer/Arm/Web.fs index 15e28a230..9c0240166 100644 --- a/src/Farmer/Arm/Web.fs +++ b/src/Farmer/Arm/Web.fs @@ -29,7 +29,7 @@ type ServerFarm = WorkerCount : int MaximumElasticWorkerCount : int option OperatingSystem : OS - ZoneRedundant : bool option + ZoneRedundant : FeatureFlag option Tags: Map } member this.IsDynamic = match this.Sku, this.WorkerSize with @@ -108,7 +108,7 @@ type ServerFarm = perSiteScaling = if this.IsDynamic then Nullable() else Nullable false reserved = this.Reserved maximumElasticWorkerCount = this.MaximumElasticWorkerCount |> Option.toNullable - zoneRedundant = this.ZoneRedundant |> Option.toNullable |} + zoneRedundant = this.ZoneRedundant |> Option.map(fun f -> f.AsBoolean) |> Option.toNullable |} kind = this.Kind |> Option.toObj |} diff --git a/src/Farmer/Builders/Builders.ServicePlan.fs b/src/Farmer/Builders/Builders.ServicePlan.fs index 12552daf8..b211a27e2 100644 --- a/src/Farmer/Builders/Builders.ServicePlan.fs +++ b/src/Farmer/Builders/Builders.ServicePlan.fs @@ -12,7 +12,7 @@ type ServicePlanConfig = WorkerCount : int MaximumElasticWorkerCount : int option OperatingSystem : OS - ZoneRedundant : bool option + ZoneRedundant : FeatureFlag option Tags : Map } interface IBuilder with member this.ResourceId = serverFarms.resourceId this.Name @@ -59,8 +59,8 @@ type ServicePlanBuilder() = [] /// Configures this server farm to host serverless functions, not web apps. member _.Serverless(state:ServicePlanConfig) = { state with Sku = Dynamic; WorkerSize = Serverless } - [] - member _.ZoneRedundant(state:ServicePlanConfig) = {state with ZoneRedundant = Some true} + [] + member _.ZoneRedundant(state:ServicePlanConfig, flag:FeatureFlag) = {state with ZoneRedundant = Some flag} interface ITaggable with member _.Add state tags = { state with Tags = state.Tags |> Map.merge tags } let servicePlan = ServicePlanBuilder() \ No newline at end of file diff --git a/src/Farmer/Builders/Builders.WebApp.fs b/src/Farmer/Builders/Builders.WebApp.fs index 3e82dbbfd..46370452a 100644 --- a/src/Farmer/Builders/Builders.WebApp.fs +++ b/src/Farmer/Builders/Builders.WebApp.fs @@ -204,9 +204,8 @@ type WebAppConfig = SiteExtensions : ExtensionName Set PrivateEndpoints: (LinkedResource * string option) Set CustomDomain : DomainConfig - ZoneRedundant : bool option - DockerPort: int option } - + DockerPort: int option + ZoneRedundant : FeatureFlag option } member this.Name = this.CommonWebConfig.Name /// Gets this web app's Server Plan's full resource ID. member this.ServicePlanId = this.CommonWebConfig.ServicePlan.resourceId this.Name.ResourceName @@ -591,8 +590,8 @@ type WebAppBuilder() = SiteExtensions = Set.empty PrivateEndpoints = Set.empty CustomDomain = NoDomain - ZoneRedundant = None - DockerPort = None } + DockerPort = None + ZoneRedundant = None } member _.Run(state:WebAppConfig) = if state.Name.ResourceName = ResourceName.Empty then raiseFarmer "No Web App name has been set." @@ -700,12 +699,12 @@ type WebAppBuilder() = member _.CustomDomain(state:WebAppConfig, domainConfig) = { state with CustomDomain = domainConfig } member _.CustomDomain(state:WebAppConfig, customDomain) = { state with CustomDomain = SecureDomain (customDomain,AppManagedCertificate) } member _.CustomDomain(state:WebAppConfig, (customDomain,thumbprint)) = { state with CustomDomain = SecureDomain (customDomain,CustomCertificate thumbprint) } - /// Enables the zone redundancy in service plan - [] - member this.ZoneRedundant(state:WebAppConfig) = {state with ZoneRedundant = Some true} /// Map specified port traffic from your docker container to port 80 for App Service [] member _.DockerPort(state: WebAppConfig, dockerPort:int) = { state with DockerPort = Some dockerPort } + /// Enables the zone redundancy in service plan + [] + member this.ZoneRedundant(state:WebAppConfig, flag:FeatureFlag) = {state with ZoneRedundant = Some flag} interface IPrivateEndpoints with member _.Add state endpoints = { state with PrivateEndpoints = state.PrivateEndpoints |> Set.union endpoints} interface ITaggable with member _.Add state tags = { state with Tags = state.Tags |> Map.merge tags } diff --git a/src/Tests/ServicePlan.fs b/src/Tests/ServicePlan.fs index b43f8382f..47af24405 100644 --- a/src/Tests/ServicePlan.fs +++ b/src/Tests/ServicePlan.fs @@ -25,7 +25,7 @@ let tests = testList "Service Plan Tests" [ } test "Enable zoneRedundant in service plan" { - let servicePlan = servicePlan { name "test"; enable_zone_redundant } + let servicePlan = servicePlan { name "test"; zone_redundant Enabled } let sf = servicePlan |> getResources |> getResource |> List.head let template = arm{ add_resource servicePlan} @@ -34,8 +34,23 @@ let tests = testList "Service Plan Tests" [ let zoneRedundant = jobj.SelectToken($"$..resources[?(@.type=='Microsoft.Web/serverfarms')].properties.zoneRedundant") - Expect.equal sf.ZoneRedundant (Some true) "ZoneRedundant should be enabled" + Expect.equal sf.ZoneRedundant (Some Enabled) "ZoneRedundant should be enabled" Expect.isNotNull zoneRedundant "Template should include zone redundancy information" Expect.equal (zoneRedundant.ToString().ToLower()) "true" "ZoneRedundant should be set to true" } + + test "Disable zoneRedundant in service plan" { + let servicePlan = servicePlan { name "test"; zone_redundant Disabled } + let sf = servicePlan |> getResources |> getResource |> List.head + + let template = arm{ add_resource servicePlan} + let jobj = template.Template |> Writer.toJson |> Newtonsoft.Json.Linq.JObject.Parse + + let zoneRedundant = + jobj.SelectToken($"$..resources[?(@.type=='Microsoft.Web/serverfarms')].properties.zoneRedundant") + + Expect.equal sf.ZoneRedundant (Some Disabled) "ZoneRedundant should be disabled" + Expect.isNotNull zoneRedundant "Template should include zone redundancy information" + Expect.equal (zoneRedundant.ToString().ToLower()) "false" "ZoneRedundant should be set to false" + } ] \ No newline at end of file diff --git a/src/Tests/WebApp.fs b/src/Tests/WebApp.fs index 77acf4a29..96e29bbc4 100644 --- a/src/Tests/WebApp.fs +++ b/src/Tests/WebApp.fs @@ -717,14 +717,6 @@ let tests = testList "Web App Tests" [ Expect.isEmpty extensions "Should not be any extensions" } - - test "Web App enables zoneRedundant in service plan" { - let resources = webApp { name "test"; enable_zone_redundant } |> getResources - let sf = resources |> getResource |> List.head - - Expect.equal sf.ZoneRedundant (Some true) "ZoneRedundant should be enabled" - } - test "Supports docker ports with WEBSITES_PORT"{ let wa = webApp { name "testApp"; docker_port 8080; } let port = Expect.wantSome wa.DockerPort "Docker port should be set" @@ -742,4 +734,10 @@ let tests = testList "Web App Tests" [ Expect.isNone defaultWa.DockerPort "Docker port should not be set" } + test "Web App enables zoneRedundant in service plan" { + let resources = webApp { name "test"; zone_redundant Enabled } |> getResources + let sf = resources |> getResource |> List.head + + Expect.equal sf.ZoneRedundant (Some Enabled) "ZoneRedundant should be enabled" + } ] From 04bc03387b2e388aa0ba017f8259c10aed69621b Mon Sep 17 00:00:00 2001 From: Nargiz Gasimova <3729486+nargiz@users.noreply.github.com> Date: Mon, 14 Feb 2022 11:13:29 +0000 Subject: [PATCH 26/73] update docs --- docs/content/api-overview/resources/web-app.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/content/api-overview/resources/web-app.md b/docs/content/api-overview/resources/web-app.md index b491f36e7..2ceb0cad2 100644 --- a/docs/content/api-overview/resources/web-app.md +++ b/docs/content/api-overview/resources/web-app.md @@ -64,7 +64,7 @@ The Web App builder is used to create Azure App Service accounts. It abstracts t | Service Plan | sku | Sets the sku of the service plan. | | Service Plan | worker_size | Sets the size of the service plan worker. | | Service Plan | number_of_workers | Sets the number of instances on the service plan. | -| Service Plan | enable_zone_redundant | Enables ZoneRedundant on the service plan. | +| Service Plan | zone_redundant | Enables ZoneRedundant on the service plan. | > **Farmer also comes with a dedicated Service Plan builder** that contains all of the above keywords that apply to a Service Plan. > From d23098fbd73189d90b35a2d59bad744dcda4d000 Mon Sep 17 00:00:00 2001 From: Dave Curylo Date: Tue, 15 Feb 2022 22:27:57 -0500 Subject: [PATCH 27/73] Update RELEASE_NOTES for version 1.6.27 --- RELEASE_NOTES.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 7ca2be62a..13ee74909 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,8 +1,8 @@ Release Notes ============= -## vNext -* Functions: Make connection_string available for Azure Functions in addtion to WebApps. +## 1.6.27 +* Functions: Make connection_string available for Azure Functions in addition to WebApps. * WebApp: Support custom port for docker container with `docker_port` * WebApps/Functions: Add support for ip-restriction rules * WebApps/Functions: Don't turn on Logging Extension for Linux App Service. From 7b0610bdd87adc1dc01d4b6bb75744a5e23204f3 Mon Sep 17 00:00:00 2001 From: Dave Curylo Date: Tue, 15 Feb 2022 22:28:43 -0500 Subject: [PATCH 28/73] 1.6.27 release updates --- src/Farmer/Farmer.fsproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Farmer/Farmer.fsproj b/src/Farmer/Farmer.fsproj index 3b86d2ae8..ea5834103 100644 --- a/src/Farmer/Farmer.fsproj +++ b/src/Farmer/Farmer.fsproj @@ -2,7 +2,7 @@ Farmer - 1.6.26 + 1.6.27 Farmer makes authoring ARM templates easy! Copyright 2019-2022 Compositional IT Ltd. Compositional IT From a6194287baec35529dd7e1d1bf1a4f118dc62f6b Mon Sep 17 00:00:00 2001 From: Dave Curylo Date: Tue, 15 Feb 2022 22:41:35 -0500 Subject: [PATCH 29/73] Bundle LICENSE and README in nuget --- src/Farmer/Farmer.fsproj | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/Farmer/Farmer.fsproj b/src/Farmer/Farmer.fsproj index ea5834103..41ca03c18 100644 --- a/src/Farmer/Farmer.fsproj +++ b/src/Farmer/Farmer.fsproj @@ -21,7 +21,8 @@ azure;resource-manager;template;dsl;fsharp;infrastructure-as-code https://raw.githubusercontent.com/CompositionalIT/farmer/master/RELEASE_NOTES.md https://compositionalit.github.io/farmer - MIT + LICENSE + readme.md true git https://github.com/CompositionalIT/farmer @@ -50,6 +51,11 @@ + + + + + From 868787b322ad75b4e02c8e4dc5e56405b28cd2d8 Mon Sep 17 00:00:00 2001 From: Dave Curylo Date: Wed, 16 Feb 2022 07:38:43 -0500 Subject: [PATCH 30/73] Cleanup release notes for 1.6.27 release --- RELEASE_NOTES.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index a27b73eed..7a025f19b 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -2,11 +2,11 @@ Release Notes ============= ## 1.6.27 -* Functions: Make connection_string available for Azure Functions in addition to WebApps. -* WebApp: Support custom port for docker container with `docker_port` +* Functions: Make `connection_string` available for Azure Functions in addition to WebApps. * WebApps/Functions: Add support for ip-restriction rules * WebApps/Functions: Don't turn on Logging Extension for Linux App Service. -* WebApp: Allow multiple custom domains +* WebApps: Allow multiple custom domains +* WebApps: Support custom port for docker container with `docker_port` ## 1.6.26 * WebApps/Functions: Fix .NET 5/6 on Linux deployments. From 07e5284ae0e99168daa0fedeb9dd02f93d7fbe97 Mon Sep 17 00:00:00 2001 From: "C5ALLIANCE\\Michael.Wade" Date: Wed, 2 Feb 2022 11:58:38 +0000 Subject: [PATCH 31/73] Re-applied changes following pull from remote farmer master. --- RELEASE_NOTES.md | 4 ++++ docs/content/api-overview/resources/web-app.md | 2 +- src/Farmer/Builders/Builders.WebApp.fs | 5 ++++- src/Tests/WebApp.fs | 5 ++++- 4 files changed, 13 insertions(+), 3 deletions(-) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 7a025f19b..0f5279969 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,6 +1,9 @@ Release Notes ============= +## vNext +* WebApps/Functions: Specify connection string types + ## 1.6.27 * Functions: Make `connection_string` available for Azure Functions in addition to WebApps. * WebApps/Functions: Add support for ip-restriction rules @@ -11,6 +14,7 @@ Release Notes ## 1.6.26 * WebApps/Functions: Fix .NET 5/6 on Linux deployments. + ## 1.6.25 * CosmosDb: Add support for serverless capacity mode. * WebApps/Functions: Fix autoSwapSlotName for app slots. diff --git a/docs/content/api-overview/resources/web-app.md b/docs/content/api-overview/resources/web-app.md index ae7b06ac1..c7f10904d 100644 --- a/docs/content/api-overview/resources/web-app.md +++ b/docs/content/api-overview/resources/web-app.md @@ -28,7 +28,7 @@ The Web App builder is used to create Azure App Service accounts. It abstracts t | Web App | setting | Sets an app setting of the web app in the form "key" "value". | | Web App | secret_setting | Sets a "secret" app setting of the web app. You must supply the "key", whilst the value will be supplied as a secure parameter. | | Web App | settings | Sets a list of app setting of the web app as tuples in the form of ("key", "value"). | -| Web App | connection_string | Creates a connection string whose value is supplied as secret parameter, or as an ARM expression in the tupled form of ("key", expr). | +| Web App | connection_string | Creates a connection string whose value is supplied as secret parameter, or as an ARM expression in the tupled form of ("key", expr), or with the connection string type ("key", expr, SQLAzure). | | Web App | connection_strings | Creates a set of connection strings of the web app whose values will be supplied as secret parameters. | | Web App | ftp_state | Allows to enable or disable FTP and FTPS. | | Web App | https_only | Disables http for this webapp so that only HTTPS is used. | diff --git a/src/Farmer/Builders/Builders.WebApp.fs b/src/Farmer/Builders/Builders.WebApp.fs index 29a1ff4df..44acbc63d 100644 --- a/src/Farmer/Builders/Builders.WebApp.fs +++ b/src/Farmer/Builders/Builders.WebApp.fs @@ -823,9 +823,12 @@ module Extensions = { current with ConnectionStrings = current.ConnectionStrings.Add(key, (ParameterSetting (SecureParameter key), Custom)) } |> this.Wrap state member this.AddConnectionString(state:'T, (key, value:ArmExpression)) = + this.AddConnectionString(state, (key, value, Custom)) + member this.AddConnectionString(state:'T, (key, value:ArmExpression, kind)) = let current = this.Get state - { current with ConnectionStrings = current.ConnectionStrings.Add(key, (ExpressionSetting value, Custom)) } + { current with ConnectionStrings = current.ConnectionStrings.Add(key, (ExpressionSetting value, kind)) } |> this.Wrap state + /// Creates a set of connection strings of the web app whose values will be supplied as secret parameters. [] member this.AddConnectionStrings(state:'T, connectionStrings) = diff --git a/src/Tests/WebApp.fs b/src/Tests/WebApp.fs index 378e7ce51..755babbc3 100644 --- a/src/Tests/WebApp.fs +++ b/src/Tests/WebApp.fs @@ -64,12 +64,13 @@ let tests = testList "Web App Tests" [ test "Web App correctly adds connection strings" { let sa = storageAccount { name "foo" } let wa = - let resources = webApp { name "test"; connection_string "a"; connection_string ("b", sa.Key) } |> getResources + let resources = webApp { name "test"; connection_string "a"; connection_string ("b", sa.Key); connection_string ("c", ArmExpression.create("c"), SQLAzure) } |> getResources resources |> getResource |> List.head let expected = [ "a", (ParameterSetting(SecureParameter "a"), Custom) "b", (ExpressionSetting sa.Key, Custom) + "c", (ExpressionSetting (ArmExpression.create("c")), SQLAzure) ] let parameters = wa :> IParameters @@ -686,6 +687,8 @@ let tests = testList "Web App Tests" [ let resources = webApp { name webappName; custom_domain (DomainConfig.InsecureDomain "customDomain.io") } |> getResources let wa = resources |> getResource |> List.head + let exepectedSiteId = (Managed (Arm.Web.sites.resourceId wa.Name)) + //Testing HostnameBinding let hostnameBinding = resources |> getResource |> List.head let expectedSslState = SslState.SslDisabled From 8403226c2fcdbab820e1825d97184ed6fc8a3c5e Mon Sep 17 00:00:00 2001 From: "C5ALLIANCE\\Michael.Wade" Date: Thu, 17 Feb 2022 10:14:13 +0000 Subject: [PATCH 32/73] Removed un-intended change --- src/Tests/WebApp.fs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Tests/WebApp.fs b/src/Tests/WebApp.fs index 755babbc3..1188234af 100644 --- a/src/Tests/WebApp.fs +++ b/src/Tests/WebApp.fs @@ -687,8 +687,6 @@ let tests = testList "Web App Tests" [ let resources = webApp { name webappName; custom_domain (DomainConfig.InsecureDomain "customDomain.io") } |> getResources let wa = resources |> getResource |> List.head - let exepectedSiteId = (Managed (Arm.Web.sites.resourceId wa.Name)) - //Testing HostnameBinding let hostnameBinding = resources |> getResource |> List.head let expectedSslState = SslState.SslDisabled From c676c74015596e1a9050435f890b2c121ce16891 Mon Sep 17 00:00:00 2001 From: "C5ALLIANCE\\Michael.Wade" Date: Thu, 17 Feb 2022 10:15:42 +0000 Subject: [PATCH 33/73] Removed un-intended change 2 --- RELEASE_NOTES.md | 1 - 1 file changed, 1 deletion(-) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 0f5279969..2957f76dd 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -14,7 +14,6 @@ Release Notes ## 1.6.26 * WebApps/Functions: Fix .NET 5/6 on Linux deployments. - ## 1.6.25 * CosmosDb: Add support for serverless capacity mode. * WebApps/Functions: Fix autoSwapSlotName for app slots. From 6e80b44ec699433f4a89cf754e2ce07a21a1ef44 Mon Sep 17 00:00:00 2001 From: Dave Curylo Date: Mon, 28 Feb 2022 22:36:30 -0500 Subject: [PATCH 34/73] 1.6.28 release --- RELEASE_NOTES.md | 4 +++- src/Farmer/Farmer.fsproj | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 3f7bb72c0..0c1ef9e82 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,6 +1,9 @@ Release Notes ============= +## 1.6.28 +* ServicePlan/WebApp: Support for enabling ZoneDedundant + ## 1.6.27 * Functions: Make `connection_string` available for Azure Functions in addition to WebApps. * WebApps/Functions: Add support for ip-restriction rules @@ -10,7 +13,6 @@ Release Notes ## 1.6.26 * WebApps/Functions: Fix .NET 5/6 on Linux deployments. -* ServicePlan/WebApp: Support for enabling ZoneDedundant ## 1.6.25 * CosmosDb: Add support for serverless capacity mode. diff --git a/src/Farmer/Farmer.fsproj b/src/Farmer/Farmer.fsproj index ea5834103..e935d6d22 100644 --- a/src/Farmer/Farmer.fsproj +++ b/src/Farmer/Farmer.fsproj @@ -2,7 +2,7 @@ Farmer - 1.6.27 + 1.6.28 Farmer makes authoring ARM templates easy! Copyright 2019-2022 Compositional IT Ltd. Compositional IT From c554b65cd714636cace3350e650c6dbdbbb4ac47 Mon Sep 17 00:00:00 2001 From: Prashant Pathak Date: Fri, 4 Mar 2022 12:54:16 +0000 Subject: [PATCH 35/73] Allow adding an IP with CIDR as a string to a web app --- samples/scripts/webapp-storage.fsx | 2 ++ src/Farmer/Builders/Builders.WebApp.fs | 12 +++++++++--- src/Farmer/Common.fs | 19 +++++++++++-------- src/Tests/WebApp.fs | 4 ++-- 4 files changed, 24 insertions(+), 13 deletions(-) diff --git a/samples/scripts/webapp-storage.fsx b/samples/scripts/webapp-storage.fsx index f91c056dd..84d1ef573 100644 --- a/samples/scripts/webapp-storage.fsx +++ b/samples/scripts/webapp-storage.fsx @@ -15,6 +15,8 @@ let myWebApp = webApp { sku WebApp.Sku.S1 app_insights_off setting "storage_key" myStorage.Key + add_allowed_ip_restriction "allow everything" "0.0.0.0/0" + add_denied_ip_restriction "deny" "1.2.3.4/31" } let deployment = arm { diff --git a/src/Farmer/Builders/Builders.WebApp.fs b/src/Farmer/Builders/Builders.WebApp.fs index 93f1a2bcf..42ddfe4f4 100644 --- a/src/Farmer/Builders/Builders.WebApp.fs +++ b/src/Farmer/Builders/Builders.WebApp.fs @@ -169,7 +169,7 @@ type SlotBuilder() = let cidr = { Address = ip; Prefix = 32 } this.AllowIp(state, name, cidr) member this.AllowIp(state, name, ip:string) : SlotConfig = - let cidr = { Address = Net.IPAddress.Parse ip; Prefix = 32 } + let cidr = IPAddressCidr.parse ip this.AllowIp(state, name, cidr) /// Add Denied ip for ip security restrictions [] @@ -179,7 +179,7 @@ type SlotBuilder() = let cidr = { Address = ip; Prefix = 32 } this.DenyIp(state, name, cidr) member this.DenyIp(state, name, ip:string) : SlotConfig = - let cidr = { Address = Net.IPAddress.Parse ip; Prefix = 32 } + let cidr = IPAddressCidr.parse ip this.DenyIp(state, name, cidr) interface ITaggable with member _.Add state tags = { state with Tags = state.Tags |> Map.merge tags } interface IDependable with member _.Add state newDeps = { state with Dependencies = state.Dependencies + newDeps } @@ -957,9 +957,15 @@ module Extensions = member this.HealthCheckPath(state:'T, healthCheckPath:string) = this.Map state (fun x -> {x with HealthCheckPath = Some(healthCheckPath)}) /// Add Allowed ip for ip security restrictions [] - member this.AllowIp(state:'T, name, ip) = + member this.AllowIp(state:'T, name, ip:IPAddressCidr) = + this.Map state (fun x -> { x with IpSecurityRestrictions = IpSecurityRestriction.Create name ip Allow :: x.IpSecurityRestrictions }) + member this.AllowIp(state:'T, name, ip:string) = + let ip = IPAddressCidr.parse ip this.Map state (fun x -> { x with IpSecurityRestrictions = IpSecurityRestriction.Create name ip Allow :: x.IpSecurityRestrictions }) /// Add Denied ip for ip security restrictions [] member this.DenyIp(state:'T, name, ip) = this.Map state (fun x -> { x with IpSecurityRestrictions = IpSecurityRestriction.Create name ip Deny :: x.IpSecurityRestrictions }) + member this.DenyIp(state:'T, name, ip:string) = + let ip = IPAddressCidr.parse ip + this.Map state (fun x -> { x with IpSecurityRestrictions = IpSecurityRestriction.Create name ip Deny :: x.IpSecurityRestrictions }) diff --git a/src/Farmer/Common.fs b/src/Farmer/Common.fs index c794170e3..7e54eb8d3 100644 --- a/src/Farmer/Common.fs +++ b/src/Farmer/Common.fs @@ -597,27 +597,30 @@ module Storage = /// A network represented by an IP address and CIDR prefix. type public IPAddressCidr = - { Address : System.Net.IPAddress + { Address : Net.IPAddress Prefix : int } /// Functions for IP networks and CIDR notation. module IPAddressCidr = let parse (s:string) : IPAddressCidr = - match s.Split([|'/'|], System.StringSplitOptions.RemoveEmptyEntries) with - [| ip; prefix |] -> - { Address = System.Net.IPAddress.Parse (ip.Trim ()) + match s.Split([|'/'|], StringSplitOptions.RemoveEmptyEntries) with + | [| ip; prefix |] -> + { Address = Net.IPAddress.Parse (ip.Trim()) Prefix = int prefix } - | _ -> raise (System.ArgumentOutOfRangeException "Malformed CIDR, expecting an IP and prefix separated by '/'") - let safeParse (s:string) : Result = + | [| ip |] -> + { Address = Net.IPAddress.Parse (ip.Trim()) + Prefix = 32 } + | _ -> raise (ArgumentOutOfRangeException "Malformed CIDR, expecting an IP and prefix separated by '/'") + let safeParse (s:string) : Result = try parse s |> Ok with ex -> Error ex let format (cidr:IPAddressCidr) = $"{cidr.Address}/{cidr.Prefix}" /// Gets uint32 representation of an IP address. - let private num (ip:System.Net.IPAddress) = + let private num (ip:Net.IPAddress) = ip.GetAddressBytes() |> Array.rev |> fun bytes -> BitConverter.ToUInt32 (bytes, 0) /// Gets IP address from uint32 representations let private ofNum (num:uint32) = - num |> BitConverter.GetBytes |> Array.rev |> System.Net.IPAddress + num |> BitConverter.GetBytes |> Array.rev |> Net.IPAddress let private ipRangeNums (cidr:IPAddressCidr) = let ipNumber = cidr.Address |> num let mask = 0xffffffffu <<< (32 - cidr.Prefix) diff --git a/src/Tests/WebApp.fs b/src/Tests/WebApp.fs index 58938f502..7c0262c90 100644 --- a/src/Tests/WebApp.fs +++ b/src/Tests/WebApp.fs @@ -752,11 +752,11 @@ let tests = testList "Web App Tests" [ Expect.contains (secureBinding3.DependsOn |> Seq.map(ResourceId.Eval)) "[resourceId('Microsoft.Web/sites/hostNameBindings', 'test', 'secure2.io')]" "Third host name binding depends on second" } test "Supports adding ip restriction for allowed ip" { - let ip = IPAddressCidr.parse "1.2.3.4/32" + let ip = "1.2.3.4/32" let resources = webApp { name "test"; add_allowed_ip_restriction "test-rule" ip } |> getResources let site = resources |> getResource |> List.head - let expectedRestriction = IpSecurityRestriction.Create "test-rule" ip Allow + let expectedRestriction = IpSecurityRestriction.Create "test-rule" (IPAddressCidr.parse ip) Allow Expect.equal site.IpSecurityRestrictions [ expectedRestriction ] "Should add allowed ip security restriction" } test "Supports adding ip restriction for denied ip" { From 696806e33ec63ddf32cb3bdcc4216feb6fbfa8dc Mon Sep 17 00:00:00 2001 From: Steffen Forkmann Date: Mon, 7 Mar 2022 10:19:54 +0100 Subject: [PATCH 36/73] Use --only-show-errors - fixes #884 --- src/Farmer/Deploy.fs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Farmer/Deploy.fs b/src/Farmer/Deploy.fs index 4e20941ef..f6607102f 100644 --- a/src/Farmer/Deploy.fs +++ b/src/Farmer/Deploy.fs @@ -45,7 +45,7 @@ module Az = let azProcess = ProcessStartInfo( FileName = azCliPath.Value, - Arguments = $"%s{arguments} --output json", + Arguments = $"%s{arguments} --output json --only-show-errors", UseShellExecute = false, RedirectStandardOutput = true, RedirectStandardError = true) From 9afadfb41a0ee4880d7577c348b85199bae76137 Mon Sep 17 00:00:00 2001 From: Tim Forkmann Date: Mon, 7 Mar 2022 10:25:21 -0500 Subject: [PATCH 37/73] fix core and memory --- samples/scripts/container-app.fsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/samples/scripts/container-app.fsx b/samples/scripts/container-app.fsx index 48901c29a..76161aa0e 100644 --- a/samples/scripts/container-app.fsx +++ b/samples/scripts/container-app.fsx @@ -30,8 +30,8 @@ let env = container { name "queuereaderapp" public_docker_image "mcr.microsoft.com/azuredocs/containerapps-queuereader" "" - cpu_cores 1.0 - memory 1.0 + cpu_cores 0.25 + memory 0.5 } ] replicas 1 10 From bbb7d876fca283b39065e895b861621ce3cdb2e9 Mon Sep 17 00:00:00 2001 From: Tim Forkmann Date: Mon, 7 Mar 2022 10:34:25 -0500 Subject: [PATCH 38/73] add hint to turn on ressource provider --- docs/content/api-overview/resources/container-apps.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/content/api-overview/resources/container-apps.md b/docs/content/api-overview/resources/container-apps.md index 8726446bd..3861bd16e 100644 --- a/docs/content/api-overview/resources/container-apps.md +++ b/docs/content/api-overview/resources/container-apps.md @@ -11,6 +11,11 @@ The Container Apps builder is used to create Azure Container Apps. * Container Environment (`Microsoft.Web/kubeEnvironments`) * Container App (`Microsoft.Web/containerApps`) +### Turn on Resource Provider +Before you deploy your container app, you need to turn on the Container Apps resource provider in your Azure subscription. + +Get sure you have the following providers registered: `Microsoft.Kubernetes` and `Microsoft.ContainerService`. + #### Container Environment Builder The Container Environment builder (`containerEnvironment`) defines settings for the Kubernetes envirionment that hosts the container apps. From 0ae6dea5d5afe594fd73815e9070c1e430d40825 Mon Sep 17 00:00:00 2001 From: Dave Curylo Date: Mon, 7 Mar 2022 14:53:43 -0500 Subject: [PATCH 39/73] 1.6.29 release --- RELEASE_NOTES.md | 3 +++ src/Farmer/Farmer.fsproj | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 0c1ef9e82..ed1eb9321 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,6 +1,9 @@ Release Notes ============= +## 1.6.29 +* CLI: include `--only-show-error` option when executing Azure CLI commands. + ## 1.6.28 * ServicePlan/WebApp: Support for enabling ZoneDedundant diff --git a/src/Farmer/Farmer.fsproj b/src/Farmer/Farmer.fsproj index 21cd7e5d5..48d328499 100644 --- a/src/Farmer/Farmer.fsproj +++ b/src/Farmer/Farmer.fsproj @@ -2,7 +2,7 @@ Farmer - 1.6.28 + 1.6.29 Farmer makes authoring ARM templates easy! Copyright 2019-2022 Compositional IT Ltd. Compositional IT From a06cae7edb500e1b3061bb6ab2e40fd62d1a4d59 Mon Sep 17 00:00:00 2001 From: Richard Sanderson-Pope Date: Wed, 9 Mar 2022 11:50:35 +0000 Subject: [PATCH 40/73] Added support for WebApp vnet integration --- src/Farmer/Aliases.fs | 15 +++++++ src/Farmer/Arm/Network.fs | 45 +++++++++++++++++-- src/Farmer/Arm/Web.fs | 38 +++++++++++++--- src/Farmer/Builders/Builders.Functions.fs | 14 +++++- src/Farmer/Builders/Builders.Storage.fs | 1 + .../Builders/Builders.VirtualNetwork.fs | 7 ++- src/Farmer/Builders/Builders.WebApp.fs | 39 ++++++++++++---- src/Farmer/Builders/Extensions.fs | 3 +- src/Farmer/Farmer.fsproj | 1 - src/Tests/test-data/diagnostics.json | 2 +- src/Tests/test-data/lots-of-resources.json | 6 +-- 11 files changed, 145 insertions(+), 26 deletions(-) diff --git a/src/Farmer/Aliases.fs b/src/Farmer/Aliases.fs index 7ad438d09..e5cdd269b 100644 --- a/src/Farmer/Aliases.fs +++ b/src/Farmer/Aliases.fs @@ -1,4 +1,19 @@ [] module Farmer.Aliases +[] +module BuilderExtensions = + open Farmer.Builders + open Farmer.Arm.Network + type IPrivateEndpoints<'TConfig> with + member this.AddPrivateEndpoint(state:'TConfig, subnetId:LinkedResource) = this.AddPrivateEndpoint (state, SubnetReference.create subnetId) + member this.AddPrivateEndpoint(state:'TConfig, subnet:SubnetConfig) = this.AddPrivateEndpoint (state, SubnetReference.create subnet) + member this.AddPrivateEndpoint(state, (subnetRef:LinkedResource,epName)) = this.AddPrivateEndpoint (state, (SubnetReference.create subnetRef, epName)) + member this.AddPrivateEndpoint(state:'TConfig, (vnetRef, subnetName):LinkedResource * ResourceName) = this.AddPrivateEndpoint (state, SubnetReference.create (vnetRef,subnetName)) + member this.AddPrivateEndpoint(state, (vnetRef,subnetName,epName):LinkedResource * ResourceName * string) = this.AddPrivateEndpoint (state, ((SubnetReference.create (vnetRef, subnetName)), epName)) + member this.AddPrivateEndpoint(state:'TConfig, (vnet, subnetName):VirtualNetworkConfig * ResourceName) = this.AddPrivateEndpoint (state, SubnetReference.create (vnet,subnetName)) + + member this.AddPrivateEndpoints(state:'TConfig, subnetIds:LinkedResource list) = this.AddPrivateEndpoints (state, subnetIds |> List.map SubnetReference.create |> Set) + member this.AddPrivateEndpoints(state:'TConfig, subnets:SubnetConfig list) = this.AddPrivateEndpoints (state, subnets |> List.map SubnetReference.create |> Set) + let arm = Farmer.Builders.ResourceGroup.DeploymentBuilder () \ No newline at end of file diff --git a/src/Farmer/Arm/Network.fs b/src/Farmer/Arm/Network.fs index a015502c0..c5cd662b3 100644 --- a/src/Farmer/Arm/Network.fs +++ b/src/Farmer/Arm/Network.fs @@ -20,6 +20,43 @@ let localNetworkGateways = ResourceType ("Microsoft.Network/localNetworkGateways let privateEndpoints = ResourceType ("Microsoft.Network/privateEndpoints", "2020-07-01") let virtualNetworkPeering = ResourceType ("Microsoft.Network/virtualNetworks/virtualNetworkPeerings","2020-05-01") +type SubnetReference = + | ViaManagedVNet of (ResourceId * ResourceName) + | Direct of LinkedResource + member this.ResourceId :ResourceId = + match this with + | ViaManagedVNet (vnetId,subnet) -> + { vnetId with + Type = subnets + Segments = [subnet] } + | Direct subnet -> subnet.ResourceId + member this.Dependency = + match this with + | ViaManagedVNet (vnetId,_) + | Direct (Managed vnetId) -> Some vnetId + | _ -> None + static member private validateVNetId vnetId = + if vnetId.Type.Type <> virtualNetworks.Type then + raiseFarmer $"given resource was not of type '{virtualNetworks.Type}'." + static member private validateSubnetId subnetId = + if subnetId.Type.Type <> subnets.Type then + raiseFarmer $"given resource was not of type '{subnets.Type}'." + static member private deriveSubnetId (vnetId:ResourceId) subnetName = + SubnetReference.validateVNetId vnetId + { vnetId with Type = subnets; Segments = [subnetName] } + static member create(vnetRef:LinkedResource, subnetName:ResourceName) = + SubnetReference.validateVNetId vnetRef.ResourceId + match vnetRef with + | Managed vnetId -> + ViaManagedVNet (vnetId, subnetName) + | Unmanaged vnetId -> + Direct (Unmanaged (SubnetReference.deriveSubnetId vnetId subnetName) ) + static member create(subnetRef:LinkedResource) = + SubnetReference.validateSubnetId subnetRef.ResourceId + Direct subnetRef + + static member create(subnetId:ResourceId) = () + type PublicIpAddress = { Name : ResourceName Location : Location @@ -419,13 +456,13 @@ type ExpressRouteCircuitAuthorization = type PrivateEndpoint = { Name: ResourceName Location: Location - Subnet: LinkedResource + Subnet: SubnetReference Resource: LinkedResource GroupIds: string list} static member create location (resourceId:ResourceId) groupIds = Set.toSeq >> Seq.map - (fun (subnet: LinkedResource, epName:string option) -> - { Name = epName |> Option.defaultValue $"{resourceId.Name.Value}-ep-{subnet.Name.Value}" |> ResourceName + (fun (subnet: SubnetReference, epName:string option) -> + { Name = epName |> Option.defaultValue $"{resourceId.Name.Value}-ep-{subnet.ResourceId.Name.Value}" |> ResourceName Location = location Subnet = subnet Resource = Managed resourceId @@ -434,7 +471,7 @@ type PrivateEndpoint = member this.ResourceId = privateEndpoints.resourceId this.Name member this.JsonModel = let dependencies = - [ match this.Subnet with | Managed x -> x | _ -> () + [ yield! this.Subnet.Dependency |> Option.toList match this.Resource with | Managed x -> x | _ -> () ] {| privateEndpoints.Create(this.Name, this.Location, dependencies) with properties = diff --git a/src/Farmer/Arm/Web.fs b/src/Farmer/Arm/Web.fs index 53ba80bde..d7c9545b3 100644 --- a/src/Farmer/Arm/Web.fs +++ b/src/Farmer/Arm/Web.fs @@ -8,7 +8,7 @@ open Farmer.WebApp open System let serverFarms = ResourceType ("Microsoft.Web/serverfarms", "2018-02-01") -let sites = ResourceType ("Microsoft.Web/sites", "2020-06-01") +let sites = ResourceType ("Microsoft.Web/sites", "2021-03-01") let config = ResourceType ("Microsoft.Web/sites/config", "2016-08-01") let sourceControls = ResourceType ("Microsoft.Web/sites/sourcecontrols", "2019-08-01") let staticSites = ResourceType ("Microsoft.Web/staticSites", "2019-12-01-preview") @@ -18,6 +18,8 @@ let certificates = ResourceType ("Microsoft.Web/certificates", "2019-08-01") let hostNameBindings = ResourceType ("Microsoft.Web/sites/hostNameBindings", "2020-12-01") let containerApps = ResourceType ("Microsoft.Web/containerApps", "2021-03-01") let kubeEnvironments = ResourceType ("Microsoft.Web/kubeEnvironments", "2021-02-01") +let virtualNetworkConnections = ResourceType ("Microsoft.Web/sites/virtualNetworkConnections", "2021-03-01") +let slotsVirtualNetworkConnections = ResourceType ("Microsoft.Web/sites/slots/virtualNetworkConnections", "2021-03-01") let private mapOrNull f = Option.map (Map.toList >> List.map f) >> Option.defaultValue Unchecked.defaultof<_> @@ -160,8 +162,7 @@ type SiteType = match this with | Slot _ -> slots | Site _ -> sites - - + [] type FTPState = | AllAllowed @@ -199,7 +200,8 @@ type Site = AutoSwapSlotName: string option ZipDeployPath : (string * ZipDeploy.ZipDeployTarget * ZipDeploy.ZipDeploySlot) option HealthCheckPath : string option - IpSecurityRestrictions : IpSecurityRestriction list } + IpSecurityRestrictions : IpSecurityRestriction list + LinkToSubnet : SubnetReference option } /// Shorthand for SiteType.ResourceType member this.ResourceType = this.SiteType.ResourceType /// Shorthand for SiteType.ResourceName @@ -234,7 +236,7 @@ type Site = interface IArmResource with member this.ResourceId = sites.resourceId this.Name member this.JsonModel = - let dependencies = this.Dependencies + (Set this.Identity.Dependencies) + let dependencies = this.Dependencies + (Set this.Identity.Dependencies) + (this.LinkToSubnet |> Option.bind (fun x -> x.Dependency) |> Option.toList |> Set.ofList) let keyvaultId = match (this.KeyVaultReferenceIdentity, this.Identity) with | Some x, _ @@ -251,6 +253,10 @@ type Site = httpsOnly = this.HTTPSOnly clientAffinityEnabled = match this.ClientAffinityEnabled with Some v -> box v | None -> null keyVaultReferenceIdentity = keyvaultId + virtualNetworkSubnetId = + match this.LinkToSubnet with + | None -> null + | Some id -> id.ResourceId.ArmExpression.Eval() siteConfig = {| alwaysOn = this.AlwaysOn appSettings = @@ -301,6 +307,8 @@ type Site = |> Option.toObj healthCheckPath = this.HealthCheckPath |> Option.toObj autoSwapSlotName = this.AutoSwapSlotName |> Option.toObj + vnetName = this.LinkToSubnet |> Option.map (fun x -> x.ResourceId.Segments[0].Value) |> Option.toObj + vnetRouteAllEnabled = this.LinkToSubnet |> function | Some _ -> Nullable true | None -> Nullable() |} |} |} @@ -323,6 +331,26 @@ module Sites = isManualIntegration = this.ContinuousIntegration.AsBoolean |> not |} |} +type VirtualNetworkConnection = + { Site: Site + Subnet: ResourceId + Dependencies: ResourceId list} + member this.Name = this.Site.Name / this.Subnet.Name + member this.SiteId = this.Site.ResourceType.resourceId this.Site.Name + interface IArmResource with + member this.ResourceId = virtualNetworkConnections.resourceId this.Name + member this.JsonModel = + let resourceType = + match this.Site.SiteType with + | Site _ -> virtualNetworkConnections + | Slot _ -> slotsVirtualNetworkConnections + {| resourceType.Create (this.Name, dependsOn=[this.SiteId; yield! this.Dependencies]) with + properties = + {| vnetResourceId = this.Subnet.ArmExpression.Eval() + isSwift = true + |} + |} :> _ + type StaticSite = { Name : ResourceName Location : Location diff --git a/src/Farmer/Builders/Builders.Functions.fs b/src/Farmer/Builders/Builders.Functions.fs index 0e81adb70..93c4cf458 100644 --- a/src/Farmer/Builders/Builders.Functions.fs +++ b/src/Farmer/Builders/Builders.Functions.fs @@ -274,7 +274,8 @@ type FunctionsConfig = | _ -> None WorkerProcess = this.CommonWebConfig.WorkerProcess HealthCheckPath = this.CommonWebConfig.HealthCheckPath - IpSecurityRestrictions = this.CommonWebConfig.IpSecurityRestrictions } + IpSecurityRestrictions = this.CommonWebConfig.IpSecurityRestrictions + LinkToSubnet = this.CommonWebConfig.RouteViaSubnet } match this.CommonWebConfig.ServicePlan with | DeployableResource this.Name.ResourceName resourceId -> @@ -317,6 +318,13 @@ type FunctionsConfig = | Some _ | None -> () + + match this.CommonWebConfig.RouteViaSubnet with + | None -> () + | Some subnetRef -> + { Site = site + Subnet = subnetRef.ResourceId + Dependencies = subnetRef.Dependency |> Option.toList } if Map.isEmpty this.CommonWebConfig.Slots then site @@ -346,7 +354,9 @@ type FunctionsBuilder() = WorkerProcess = None ZipDeployPath = None HealthCheckPath = None - IpSecurityRestrictions = [] } + IpSecurityRestrictions = [] + RouteViaSubnet = None + PrivateEndpoints = Set.empty} StorageAccount = derived (fun config -> let storage = config.Name.ResourceName.Map (sprintf "%sstorage") |> sanitiseStorage |> ResourceName storageAccounts.resourceId storage) diff --git a/src/Farmer/Builders/Builders.Storage.fs b/src/Farmer/Builders/Builders.Storage.fs index 44d5347a5..1f9e72a7a 100644 --- a/src/Farmer/Builders/Builders.Storage.fs +++ b/src/Farmer/Builders/Builders.Storage.fs @@ -2,6 +2,7 @@ module Farmer.Builders.Storage open Farmer +open Farmer.Builders open Farmer.Storage open Farmer.Arm.RoleAssignment open Farmer.Arm.Storage diff --git a/src/Farmer/Builders/Builders.VirtualNetwork.fs b/src/Farmer/Builders/Builders.VirtualNetwork.fs index 4d9ddafd6..bf4a1d73e 100644 --- a/src/Farmer/Builders/Builders.VirtualNetwork.fs +++ b/src/Farmer/Builders/Builders.VirtualNetwork.fs @@ -2,6 +2,7 @@ module Farmer.Builders.VirtualNetwork open Farmer +open Farmer.Builders open Farmer.Network open Farmer.Arm.Network @@ -386,4 +387,8 @@ type VNetPeeringSpecBuilder() = member _.GatewayTransit(state:VNetPeeringSpec, transit) = {state with Transit = transit} interface IDependable with member _.Add state resources = {state with DependsOn = state.DependsOn |> Set.union resources} -let vnetPeering = VNetPeeringSpecBuilder () \ No newline at end of file +let vnetPeering = VNetPeeringSpecBuilder () + +type SubnetReference with + static member create (vnetConfig:VirtualNetworkConfig, subnetName) = ViaManagedVNet (vnetConfig.ResourceId,subnetName) + static member create (vnetConfig:SubnetConfig) = Direct (Managed (vnetConfig:>IBuilder).ResourceId) \ No newline at end of file diff --git a/src/Farmer/Builders/Builders.WebApp.fs b/src/Farmer/Builders/Builders.WebApp.fs index b18f715ff..9f883e2c8 100644 --- a/src/Farmer/Builders/Builders.WebApp.fs +++ b/src/Farmer/Builders/Builders.WebApp.fs @@ -3,6 +3,7 @@ module rec Farmer.Builders.WebApp open Farmer open Farmer.Arm +open Farmer.Builders open Farmer.WebApp open Farmer.Arm.KeyVault.Vaults open Sites @@ -205,7 +206,9 @@ type CommonWebConfig = WorkerProcess : Bitness option ZipDeployPath : (string*ZipDeploy.ZipDeploySlot) option HealthCheckPath: string option - IpSecurityRestrictions: IpSecurityRestriction list } + IpSecurityRestrictions: IpSecurityRestriction list + RouteViaSubnet : SubnetReference option + PrivateEndpoints: (SubnetReference * string option) Set } type WebAppConfig = { CommonWebConfig: CommonWebConfig @@ -227,7 +230,6 @@ type WebAppConfig = DockerAcrCredentials : {| RegistryName : string; Password : SecureParameter |} option AutomaticLoggingExtension : bool SiteExtensions : ExtensionName Set - PrivateEndpoints: (LinkedResource * string option) Set CustomDomain : DomainConfig DockerPort: int option } member this.Name = this.CommonWebConfig.Name @@ -454,7 +456,7 @@ type WebAppConfig = ZipDeployPath = this.CommonWebConfig.ZipDeployPath |> Option.map (fun (path,slot) -> path, ZipDeploy.ZipDeployTarget.WebApp, slot ) HealthCheckPath = this.CommonWebConfig.HealthCheckPath IpSecurityRestrictions = this.CommonWebConfig.IpSecurityRestrictions - } + LinkToSubnet = this.CommonWebConfig.RouteViaSubnet } match keyVault with | Some keyVault -> @@ -571,7 +573,13 @@ type WebAppConfig = SslState = SslDisabled } | NoDomain -> () - yield! (PrivateEndpoint.create location this.ResourceId ["sites"] this.PrivateEndpoints) + match this.CommonWebConfig.RouteViaSubnet with + | None -> () + | Some subnetRef -> + { Site = site + Subnet = subnetRef.ResourceId + Dependencies = subnetRef.Dependency |> Option.toList } + yield! (PrivateEndpoint.create location this.ResourceId ["sites"] this.CommonWebConfig.PrivateEndpoints) ] type WebAppBuilder() = @@ -594,7 +602,9 @@ type WebAppBuilder() = WorkerProcess = None ZipDeployPath = None HealthCheckPath = None - IpSecurityRestrictions = [] } + IpSecurityRestrictions = [] + RouteViaSubnet = None + PrivateEndpoints = Set.empty } Sku = Sku.F1 WorkerSize = Small WorkerCount = 1 @@ -613,7 +623,6 @@ type WebAppBuilder() = DockerAcrCredentials = None AutomaticLoggingExtension = true SiteExtensions = Set.empty - PrivateEndpoints = Set.empty CustomDomain = NoDomain DockerPort = None } member _.Run(state:WebAppConfig) = @@ -715,7 +724,7 @@ type WebAppBuilder() = [] member _.DockerPort(state: WebAppConfig, dockerPort:int) = { state with DockerPort = Some dockerPort } - interface IPrivateEndpoints with member _.Add state endpoints = { state with PrivateEndpoints = state.PrivateEndpoints |> Set.union endpoints} + interface IPrivateEndpoints with member _.Add state endpoints = {state with CommonWebConfig = { state.CommonWebConfig with PrivateEndpoints = state.CommonWebConfig.PrivateEndpoints |> Set.union endpoints}} interface ITaggable with member _.Add state tags = { state with Tags = state.Tags |> Map.merge tags } interface IDependable with member _.Add state newDeps = { state with Dependencies = state.Dependencies + newDeps } interface IServicePlanApp with @@ -732,7 +741,7 @@ type EndpointBuilder with /// An interface for shared capabilities between builders that work with Service Plan-style apps. /// In other words, Web Apps or Functions. -type IServicePlanApp<'T> = +type IServicePlanApp<'T> = abstract member Get : 'T -> CommonWebConfig abstract member Wrap : 'T -> CommonWebConfig -> 'T @@ -942,3 +951,17 @@ module Extensions = [] member this.DenyIp(state:'T, name, ip) = this.Map state (fun x -> { x with IpSecurityRestrictions = IpSecurityRestriction.Create name ip Deny :: x.IpSecurityRestrictions }) + /// Integrate this app with a virtual network subnet + [] + member this.RouteViaSubnet(state:'T, subnet:SubnetReference option) = + match subnet with + | Some subnetId -> + if subnetId.ResourceId.Type.Type <> Arm.Network.subnets.Type + then raiseFarmer $"given resource was not of type '{Arm.Network.subnets.Type}'." + | None -> () + this.Map state (fun x -> {x with RouteViaSubnet = subnet}) + member this.RouteViaSubnet(state:'T, subnetRef) = this.RouteViaSubnet (state, Some subnetRef) + member this.RouteViaSubnet(state:'T, subnetId:LinkedResource) = this.RouteViaSubnet (state, SubnetReference.create subnetId) + member this.RouteViaSubnet(state:'T, subnet:SubnetConfig) = this.RouteViaSubnet (state, SubnetReference.create subnet) + member this.RouteViaSubnet(state:'T, (vnet:VirtualNetworkConfig, subnetName)) = this.RouteViaSubnet (state, SubnetReference.create (vnet,subnetName)) + member this.RouteViaSubnet(state:'T, (vnetId:LinkedResource, subnetName)) = this.RouteViaSubnet (state, SubnetReference.create (vnetId,subnetName)) diff --git a/src/Farmer/Builders/Extensions.fs b/src/Farmer/Builders/Extensions.fs index e7ad44b37..5f893d9b7 100644 --- a/src/Farmer/Builders/Extensions.fs +++ b/src/Farmer/Builders/Extensions.fs @@ -2,13 +2,14 @@ namespace Farmer.Builders open Farmer open System +open Farmer.Arm type ITaggable<'TConfig> = abstract member Add : 'TConfig -> list -> 'TConfig type IDependable<'TConfig> = abstract member Add : 'TConfig -> ResourceId Set -> 'TConfig type IPrivateEndpoints<'TConfig> = - abstract member Add : 'TConfig -> (LinkedResource * String option) Set -> 'TConfig + abstract member Add : 'TConfig -> (SubnetReference * String option) Set -> 'TConfig [] module Extensions = diff --git a/src/Farmer/Farmer.fsproj b/src/Farmer/Farmer.fsproj index ea5834103..97bc1ccf4 100644 --- a/src/Farmer/Farmer.fsproj +++ b/src/Farmer/Farmer.fsproj @@ -104,7 +104,6 @@ - diff --git a/src/Tests/test-data/diagnostics.json b/src/Tests/test-data/diagnostics.json index 45fb76f82..8b30f5590 100644 --- a/src/Tests/test-data/diagnostics.json +++ b/src/Tests/test-data/diagnostics.json @@ -45,7 +45,7 @@ "type": "Microsoft.Web/sites/siteextensions" }, { - "apiVersion": "2020-06-01", + "apiVersion": "2021-03-01", "dependsOn": [ "[resourceId('Microsoft.Web/serverfarms', 'isaacdiagsuperweb-farm')]" ], diff --git a/src/Tests/test-data/lots-of-resources.json b/src/Tests/test-data/lots-of-resources.json index 061382fbf..0c35a4ec8 100644 --- a/src/Tests/test-data/lots-of-resources.json +++ b/src/Tests/test-data/lots-of-resources.json @@ -140,7 +140,7 @@ "type": "Microsoft.Web/sites/siteextensions" }, { - "apiVersion": "2020-06-01", + "apiVersion": "2021-03-01", "dependsOn": [ "[resourceId('Microsoft.Insights/components', 'farmerwebapp1979-ai')]", "[resourceId('Microsoft.Web/serverfarms', 'farmerwebapp1979-farm')]" @@ -252,7 +252,7 @@ "type": "Microsoft.Insights/components" }, { - "apiVersion": "2020-06-01", + "apiVersion": "2021-03-01", "dependsOn": [ "[resourceId('Microsoft.Insights/components', 'farmerfuncs1979-ai')]", "[resourceId('Microsoft.Storage/storageAccounts', 'farmerfuncs1979storage')]", @@ -790,7 +790,7 @@ "type": "Microsoft.Storage/storageAccounts" }, { - "apiVersion": "2020-06-01", + "apiVersion": "2021-03-01", "dependsOn": [ "[resourceId('Microsoft.Storage/storageAccounts', 'dockerfuncstorage')]", "[resourceId('Microsoft.Web/serverfarms', 'docker-func-farm')]" From 062fb5cd442b112915398ba6da3236bb5ce015f7 Mon Sep 17 00:00:00 2001 From: Richard Sanderson-Pope Date: Wed, 9 Mar 2022 12:28:03 +0000 Subject: [PATCH 41/73] Added tests --- src/Farmer/Builders/Builders.WebApp.fs | 14 +++++++------- src/Tests/WebApp.fs | 26 ++++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 7 deletions(-) diff --git a/src/Farmer/Builders/Builders.WebApp.fs b/src/Farmer/Builders/Builders.WebApp.fs index 9f883e2c8..986751d61 100644 --- a/src/Farmer/Builders/Builders.WebApp.fs +++ b/src/Farmer/Builders/Builders.WebApp.fs @@ -952,16 +952,16 @@ module Extensions = member this.DenyIp(state:'T, name, ip) = this.Map state (fun x -> { x with IpSecurityRestrictions = IpSecurityRestriction.Create name ip Deny :: x.IpSecurityRestrictions }) /// Integrate this app with a virtual network subnet - [] - member this.RouteViaSubnet(state:'T, subnet:SubnetReference option) = + [] + member this.RouteViaVNet(state:'T, subnet:SubnetReference option) = match subnet with | Some subnetId -> if subnetId.ResourceId.Type.Type <> Arm.Network.subnets.Type then raiseFarmer $"given resource was not of type '{Arm.Network.subnets.Type}'." | None -> () this.Map state (fun x -> {x with RouteViaSubnet = subnet}) - member this.RouteViaSubnet(state:'T, subnetRef) = this.RouteViaSubnet (state, Some subnetRef) - member this.RouteViaSubnet(state:'T, subnetId:LinkedResource) = this.RouteViaSubnet (state, SubnetReference.create subnetId) - member this.RouteViaSubnet(state:'T, subnet:SubnetConfig) = this.RouteViaSubnet (state, SubnetReference.create subnet) - member this.RouteViaSubnet(state:'T, (vnet:VirtualNetworkConfig, subnetName)) = this.RouteViaSubnet (state, SubnetReference.create (vnet,subnetName)) - member this.RouteViaSubnet(state:'T, (vnetId:LinkedResource, subnetName)) = this.RouteViaSubnet (state, SubnetReference.create (vnetId,subnetName)) + member this.RouteViaVNet(state:'T, subnetRef) = this.RouteViaVNet (state, Some subnetRef) + member this.RouteViaVNet(state:'T, subnetId:LinkedResource) = this.RouteViaVNet (state, SubnetReference.create subnetId) + member this.RouteViaVNet(state:'T, subnet:SubnetConfig) = this.RouteViaVNet (state, SubnetReference.create subnet) + member this.RouteViaVNet(state:'T, (vnet:VirtualNetworkConfig, subnetName)) = this.RouteViaVNet (state, SubnetReference.create (vnet,subnetName)) + member this.RouteViaVNet(state:'T, (vnetId:LinkedResource, subnetName)) = this.RouteViaVNet (state, SubnetReference.create (vnetId,subnetName)) diff --git a/src/Tests/WebApp.fs b/src/Tests/WebApp.fs index 8c46b836c..d4b83ad99 100644 --- a/src/Tests/WebApp.fs +++ b/src/Tests/WebApp.fs @@ -767,4 +767,30 @@ let tests = testList "Web App Tests" [ Expect.isNone defaultWa.DockerPort "Docker port should not be set" } + test "Can integrate unmanaged vnet" { + let subnetId = Arm.Network.subnets.resourceId (ResourceName "my-vnet", ResourceName "my-subnet") + let wa = webApp { name "testApp"; route_via_vnet (Unmanaged subnetId) } + + let resources = wa |> getResources + let site = resources |> getResource |> List.head + let vnet = Expect.wantSome site.LinkToSubnet "LinkToSubnet was not set" + Expect.equal vnet (Direct (Unmanaged subnetId)) "LinkToSubnet was incorrect" + + let vnetConnections = resources |> getResource + Expect.hasLength vnetConnections 1 "incorrect number of Vnet connections" + } + + test "Can integrate managed vnet" { + let vnetConfig = vnet { name "my-vnet" } + let wa = webApp { name "testApp"; route_via_vnet (vnetConfig, ResourceName "my-subnet") } + + let resources = wa |> getResources + let site = resources |> getResource |> List.head + let vnet = Expect.wantSome site.LinkToSubnet "LinkToSubnet was not set" + Expect.equal vnet (ViaManagedVNet ( (Arm.Network.virtualNetworks.resourceId "my-vnet"), ResourceName "my-subnet" )) "LinkToSubnet was incorrect" + + let vnetConnections = resources |> getResource + Expect.hasLength vnetConnections 1 "incorrect number of Vnet connections" + } + ] From b60cc2a0e74b1df724fb3ba190e7e9f26b868d83 Mon Sep 17 00:00:00 2001 From: Richard Sanderson-Pope Date: Wed, 9 Mar 2022 12:31:33 +0000 Subject: [PATCH 42/73] docs --- RELEASE_NOTES.md | 2 ++ docs/content/api-overview/resources/functions.md | 2 +- docs/content/api-overview/resources/web-app.md | 1 + 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 13ee74909..a6612a7a6 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,5 +1,7 @@ Release Notes ============= +## 1.6.28 +- WebApps/Functions: Add support for vnet integration ## 1.6.27 * Functions: Make connection_string available for Azure Functions in addition to WebApps. diff --git a/docs/content/api-overview/resources/functions.md b/docs/content/api-overview/resources/functions.md index 69983a05f..d983e1041 100644 --- a/docs/content/api-overview/resources/functions.md +++ b/docs/content/api-overview/resources/functions.md @@ -50,7 +50,7 @@ The Functions builder is used to create Azure Functions accounts. It abstracts t | health_check_path | Sets the path to your functions health check endpoint, which Azure load balancers will ping to determine which instances are healthy.| | add_allowed_ip_restriction | Adds an 'allow' rule for an ip | | add_denied_ip_restriction | Adds an 'deny' rule for an ip | - +| route_via_vnet | Enable the VNET integration feature in azure where all outbound traffic from the function with be sent via the specified subnet. | #### Post-deployment Builder Keywords The Functions builder contains special commands that are executed *after* the ARM deployment is completed. diff --git a/docs/content/api-overview/resources/web-app.md b/docs/content/api-overview/resources/web-app.md index 499d404b5..434d0b636 100644 --- a/docs/content/api-overview/resources/web-app.md +++ b/docs/content/api-overview/resources/web-app.md @@ -60,6 +60,7 @@ The Web App builder is used to create Azure App Service accounts. It abstracts t | Web App | add_allowed_ip_restriction | Adds an 'allow' rule for an ip | | Web App | add_denied_ip_restriction | Adds an 'deny' rule for an ip | | Web App | docker_port | Adds `WEBSITES_PORT` setting to map custom docker port to app service port 80 | +| Web App | route_via_vnet | Enable the VNET integration feature in azure where all outbound traffic from the web app with be sent via the specified subnet. | | Service Plan | service_plan_name | Sets the name of the service plan. If not set, uses the name of the web app postfixed with "-plan". | | Service Plan | runtime_stack | Sets the runtime stack. | | Service Plan | operating_system | Sets the operating system. If Linux, App Insights configuration settings will be omitted as they are not supported by Azure App Service. | From 2099a5dc3e0e500d9a13c69a60aa864c191d028e Mon Sep 17 00:00:00 2001 From: Richard Sanderson-Pope Date: Wed, 9 Mar 2022 14:50:49 +0000 Subject: [PATCH 43/73] cleanup --- src/Farmer/Arm/Network.fs | 17 +++++---------- src/Farmer/Builders/Builders.Functions.fs | 2 ++ src/Tests/Functions.fs | 26 +++++++++++++++++++++++ 3 files changed, 33 insertions(+), 12 deletions(-) diff --git a/src/Farmer/Arm/Network.fs b/src/Farmer/Arm/Network.fs index c5cd662b3..b92a83a4a 100644 --- a/src/Farmer/Arm/Network.fs +++ b/src/Farmer/Arm/Network.fs @@ -35,24 +35,17 @@ type SubnetReference = | ViaManagedVNet (vnetId,_) | Direct (Managed vnetId) -> Some vnetId | _ -> None - static member private validateVNetId vnetId = - if vnetId.Type.Type <> virtualNetworks.Type then - raiseFarmer $"given resource was not of type '{virtualNetworks.Type}'." - static member private validateSubnetId subnetId = - if subnetId.Type.Type <> subnets.Type then - raiseFarmer $"given resource was not of type '{subnets.Type}'." - static member private deriveSubnetId (vnetId:ResourceId) subnetName = - SubnetReference.validateVNetId vnetId - { vnetId with Type = subnets; Segments = [subnetName] } static member create(vnetRef:LinkedResource, subnetName:ResourceName) = - SubnetReference.validateVNetId vnetRef.ResourceId + if vnetRef.ResourceId.Type.Type <> virtualNetworks.Type then + raiseFarmer $"given resource was not of type '{virtualNetworks.Type}'." match vnetRef with | Managed vnetId -> ViaManagedVNet (vnetId, subnetName) | Unmanaged vnetId -> - Direct (Unmanaged (SubnetReference.deriveSubnetId vnetId subnetName) ) + Direct (Unmanaged { vnetId with Type = subnets; Segments = [subnetName] } ) static member create(subnetRef:LinkedResource) = - SubnetReference.validateSubnetId subnetRef.ResourceId + if subnetRef.ResourceId.Type.Type <> subnets.Type then + raiseFarmer $"given resource was not of type '{subnets.Type}'." Direct subnetRef static member create(subnetId:ResourceId) = () diff --git a/src/Farmer/Builders/Builders.Functions.fs b/src/Farmer/Builders/Builders.Functions.fs index 0b3c11d5e..c1910219b 100644 --- a/src/Farmer/Builders/Builders.Functions.fs +++ b/src/Farmer/Builders/Builders.Functions.fs @@ -11,6 +11,7 @@ open Farmer.Arm.Storage open Farmer.Arm.KeyVault open Farmer.Arm.KeyVault.Vaults open System +open Farmer.Arm type FunctionsRuntime = DotNet | DotNetIsolated | Node | Java | Python type VersionedFunctionsRuntime = FunctionsRuntime * string option @@ -326,6 +327,7 @@ type FunctionsConfig = { Site = site Subnet = subnetRef.ResourceId Dependencies = subnetRef.Dependency |> Option.toList } + yield! (PrivateEndpoint.create location this.ResourceId ["sites"] this.CommonWebConfig.PrivateEndpoints) if Map.isEmpty this.CommonWebConfig.Slots then site diff --git a/src/Tests/Functions.fs b/src/Tests/Functions.fs index fce471e01..e5e0254cf 100644 --- a/src/Tests/Functions.fs +++ b/src/Tests/Functions.fs @@ -414,4 +414,30 @@ let tests = testList "Functions tests" [ Expect.equal slot.IpSecurityRestrictions [ expectedSlotRestriction ] "Slot should have correct allowed ip security restriction" Expect.equal site.IpSecurityRestrictions [ expectedSiteRestriction ] "Site should have correct allowed ip security restriction" } + test "Can integrate unmanaged vnet" { + let subnetId = Arm.Network.subnets.resourceId (ResourceName "my-vnet", ResourceName "my-subnet") + let wa = functions { name "testApp"; route_via_vnet (Unmanaged subnetId) } + + let resources = wa |> getResources + let site = resources |> getResource |> List.head + let vnet = Expect.wantSome site.LinkToSubnet "LinkToSubnet was not set" + Expect.equal vnet (Direct (Unmanaged subnetId)) "LinkToSubnet was incorrect" + + let vnetConnections = resources |> getResource + Expect.hasLength vnetConnections 1 "incorrect number of Vnet connections" + } + + test "Can integrate managed vnet" { + let vnetConfig = vnet { name "my-vnet" } + let wa = functions { name "testApp"; route_via_vnet (vnetConfig, ResourceName "my-subnet") } + + let resources = wa |> getResources + let site = resources |> getResource |> List.head + let vnet = Expect.wantSome site.LinkToSubnet "LinkToSubnet was not set" + Expect.equal vnet (ViaManagedVNet ( (Arm.Network.virtualNetworks.resourceId "my-vnet"), ResourceName "my-subnet" )) "LinkToSubnet was incorrect" + + let vnetConnections = resources |> getResource + Expect.hasLength vnetConnections 1 "incorrect number of Vnet connections" + } + ] \ No newline at end of file From 3e2592b03e74c064e8154d821c27aae1317b4e2a Mon Sep 17 00:00:00 2001 From: Prashant Pathak Date: Fri, 11 Mar 2022 14:15:23 +0000 Subject: [PATCH 44/73] IP with CIDR: Add test and release notes --- RELEASE_NOTES.md | 5 ++++- src/Tests/Common.fs | 5 +++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index ed1eb9321..3979a9e8b 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,11 +1,14 @@ Release Notes ============= +## 1.6.30 +* WebApps/Functions: Allow adding IP restriction string with CIDR + ## 1.6.29 * CLI: include `--only-show-error` option when executing Azure CLI commands. ## 1.6.28 -* ServicePlan/WebApp: Support for enabling ZoneDedundant +* ServicePlan/WebApp: Support for enabling ZoneRedundant ## 1.6.27 * Functions: Make `connection_string` available for Azure Functions in addition to WebApps. diff --git a/src/Tests/Common.fs b/src/Tests/Common.fs index 7ebd44212..68185c79b 100644 --- a/src/Tests/Common.fs +++ b/src/Tests/Common.fs @@ -53,6 +53,11 @@ let tests = testList "Common" [ Expect.isFalse (outerCidr |> IPAddressCidr.contains innerCidr) "" } + test "IPAddressCidr default prefix is 32" { + let cidr = IPAddressCidr.parse "192.168.1.0" + Expect.equal cidr.Prefix 32 "" + } + test "Docker image tag generation" { let officialNginx = Containers.DockerImage.PublicImage ("nginx", None) Expect.equal officialNginx.ImageTag "nginx:latest" "Official image generated with incorrect tag" From 3bd6686b9eff56ab1865def65c24e1e930011e6f Mon Sep 17 00:00:00 2001 From: isaac Date: Sat, 12 Mar 2022 17:39:02 +0100 Subject: [PATCH 45/73] App Insights now supports Log Analytics --- src/Farmer/Arm/Insights.fs | 46 ++++++++++++----- src/Farmer/Builders/Builders.AppInsights.fs | 31 ++++++++++-- src/Farmer/Builders/Builders.Functions.fs | 10 ++-- src/Farmer/Builders/Builders.WebApp.fs | 56 +++++++++++---------- src/Farmer/Types.fs | 4 +- src/Tests/AppInsights.fs | 26 ++++++++++ src/Tests/test-data/lots-of-resources.json | 2 + 7 files changed, 124 insertions(+), 51 deletions(-) diff --git a/src/Farmer/Arm/Insights.fs b/src/Farmer/Arm/Insights.fs index 0ee64a418..dd61a1ad6 100644 --- a/src/Farmer/Arm/Insights.fs +++ b/src/Farmer/Arm/Insights.fs @@ -4,6 +4,16 @@ module Farmer.Arm.Insights open Farmer let components = ResourceType("Microsoft.Insights/components", "2014-04-01") +let componentsWorkspace = ResourceType("Microsoft.Insights/components", "2020-02-02") + +/// The type of AI instance to create. +type ComponentsType = + | Classic + | Workspace of ResourceId + member this.ComponentsType = + match this with + | Classic -> components + | Workspace _ -> componentsWorkspace type Components = { Name : ResourceName @@ -11,7 +21,9 @@ type Components = LinkedWebsite : ResourceName option DisableIpMasking : bool SamplingPercentage : int - Tags: Map } + Type : ComponentsType + Tags: Map + Dependencies : ResourceId Set } interface IArmResource with member this.ResourceId = components.resourceId this.Name member this.JsonModel = @@ -19,16 +31,24 @@ type Components = match this.LinkedWebsite with | Some linkedWebsite -> this.Tags.Add($"[concat('hidden-link:', resourceGroup().id, '/providers/Microsoft.Web/sites/', '{linkedWebsite.Value}')]", "Resource") | None -> this.Tags - - {| components.Create(this.Name, this.Location, tags = tags) with - kind = "web" - properties = - {| name = this.Name.Value - Application_Type = "web" - ApplicationId = - match this.LinkedWebsite with - | Some linkedWebsite -> linkedWebsite.Value - | None -> null - DisableIpMasking = this.DisableIpMasking - SamplingPercentage = this.SamplingPercentage |} + {| this.Type.ComponentsType.Create(this.Name, this.Location, this.Dependencies, tags) with + kind = "web" + properties = + {| + name = this.Name.Value + Application_Type = "web" + ApplicationId = + match this.LinkedWebsite with + | Some linkedWebsite -> linkedWebsite.Value + | None -> null + DisableIpMasking = this.DisableIpMasking + SamplingPercentage = this.SamplingPercentage + IngestionMode = + match this.Type with + | Workspace _ -> "LogAnalytics" + | Classic -> null + WorkspaceResourceId = + match this.Type with + | Workspace resourceId -> resourceId.Eval() + | Classic -> null |} |} \ No newline at end of file diff --git a/src/Farmer/Builders/Builders.AppInsights.fs b/src/Farmer/Builders/Builders.AppInsights.fs index ddd7d87a1..35fdc5835 100644 --- a/src/Farmer/Builders/Builders.AppInsights.fs +++ b/src/Farmer/Builders/Builders.AppInsights.fs @@ -3,23 +3,27 @@ module Farmer.Builders.AppInsights open Farmer open Farmer.Arm.Insights +open Farmer.Arm.LogAnalytics type AppInsights = static member getInstrumentationKey (resourceId:ResourceId) = ArmExpression - .reference(components, resourceId) + .reference(resourceId) .Map(fun r -> r + ".InstrumentationKey") .WithOwner(resourceId) - static member getInstrumentationKey (name:ResourceName, ?resourceGroup) = - AppInsights.getInstrumentationKey(ResourceId.create (components, name, ?group = resourceGroup)) + static member getInstrumentationKey (name:ResourceName, ?resourceGroup, ?componentsType) = + let componentsType = componentsType |> Option.defaultValue components + AppInsights.getInstrumentationKey(ResourceId.create (componentsType, name, ?group = resourceGroup)) type AppInsightsConfig = { Name : ResourceName DisableIpMasking : bool SamplingPercentage : int + Type : ComponentsType + Dependencies : ResourceId Set Tags : Map } /// Gets the ARM expression path to the instrumentation key of this App Insights instance. - member this.InstrumentationKey = AppInsights.getInstrumentationKey this.Name + member this.InstrumentationKey = AppInsights.getInstrumentationKey(this.Name, componentsType = this.Type.ComponentsType) interface IBuilder with member this.ResourceId = components.resourceId this.Name member this.BuildResources location = [ @@ -28,6 +32,8 @@ type AppInsightsConfig = LinkedWebsite = None DisableIpMasking = this.DisableIpMasking SamplingPercentage = this.SamplingPercentage + Dependencies = this.Dependencies + Type = this.Type Tags = this.Tags } ] @@ -36,7 +42,10 @@ type AppInsightsBuilder() = { Name = ResourceName.Empty DisableIpMasking = false SamplingPercentage = 100 - Tags = Map.empty } + Tags = Map.empty + Dependencies = Set.empty + Type = Classic } + [] /// Sets the name of the App Insights instance. member _.Name(state:AppInsightsConfig, name) = { state with Name = ResourceName name } @@ -49,10 +58,22 @@ type AppInsightsBuilder() = /// Sets the name of the App Insights instance. member _.SamplingPercentage(state:AppInsightsConfig, samplingPercentage) = { state with SamplingPercentage = samplingPercentage } + /// Links this AI instance to a Log Analytics workspace, using the newer 2020-02-02-preview App Insights version. + [] + member _.Workspace(state:AppInsightsConfig, workspace:ResourceId) = + { state with + Type = Workspace workspace + Dependencies = state.Dependencies.Add workspace + } + member this.Workspace(state:AppInsightsConfig, workspace:WorkspaceConfig) = + this.Workspace(state, workspaces.resourceId workspace.Name) + member _.Run (state:AppInsightsConfig) = if state.SamplingPercentage > 100 then raiseFarmer "Sampling Percentage cannot be higher than 100%" elif state.SamplingPercentage <= 0 then raiseFarmer "Sampling Percentage cannot be lower than or equal to 0%" state + interface ITaggable with member _.Add state tags = { state with Tags = state.Tags |> Map.merge tags } + interface IDependable with member _.Add state resources = { state with Dependencies = state.Dependencies + resources } let appInsights = AppInsightsBuilder() \ No newline at end of file diff --git a/src/Farmer/Builders/Builders.Functions.fs b/src/Farmer/Builders/Builders.Functions.fs index f77d82ef4..c3fd8fd8a 100644 --- a/src/Farmer/Builders/Builders.Functions.fs +++ b/src/Farmer/Builders/Builders.Functions.fs @@ -15,7 +15,7 @@ open System type FunctionsRuntime = DotNet | DotNetIsolated | Node | Java | Python type VersionedFunctionsRuntime = FunctionsRuntime * string option type FunctionsRuntime with - // These values are defined on FunctionsRuntime to reduce the need for users to be aware of the distinction + // These values are defined on FunctionsRuntime to reduce the need for users to be aware of the distinction // between FunctionsRuntime and VersionedFunctionsRuntime as well as to provide parity with WebApp runtime static member DotNetCore31 = DotNet, Some "3.1" static member DotNet50 = DotNet, Some "5.0" @@ -246,12 +246,12 @@ type FunctionsConfig = HTTP20Enabled = None ClientAffinityEnabled = None WebSocketsEnabled = None - LinuxFxVersion = + LinuxFxVersion = match this.CommonWebConfig.OperatingSystem with | Windows -> None | Linux -> match this.VersionedRuntime with - | DotNet, Some version -> + | DotNet, Some version -> match Double.TryParse(version) with | true, versionNo when versionNo < 4.0 -> Some $"DOTNETCORE|{version}" | _ -> Some $"DOTNET|{version}" @@ -310,6 +310,8 @@ type FunctionsConfig = Location = location DisableIpMasking = false SamplingPercentage = 100 + Dependencies = Set.empty + Type = Classic LinkedWebsite = match this.CommonWebConfig.OperatingSystem with | Windows -> Some this.Name.ResourceName @@ -346,7 +348,7 @@ type FunctionsBuilder() = Slots = Map.empty WorkerProcess = None ZipDeployPath = None - HealthCheckPath = None + HealthCheckPath = None IpSecurityRestrictions = [] } StorageAccount = derived (fun config -> let storage = config.Name.ResourceName.Map (sprintf "%sstorage") |> sanitiseStorage |> ResourceName diff --git a/src/Farmer/Builders/Builders.WebApp.fs b/src/Farmer/Builders/Builders.WebApp.fs index 42ddfe4f4..cbf433bc4 100644 --- a/src/Farmer/Builders/Builders.WebApp.fs +++ b/src/Farmer/Builders/Builders.WebApp.fs @@ -160,27 +160,27 @@ type SlotBuilder() = member this.AddConnectionStrings(state, connectionStrings:string list) :SlotConfig = connectionStrings |> List.fold (fun state key -> this.AddConnectionString(state, key)) state - + /// Add Allowed ip for ip security restrictions - [] - member _.AllowIp(state, name, cidr:IPAddressCidr) : SlotConfig = + [] + member _.AllowIp(state, name, cidr:IPAddressCidr) : SlotConfig = { state with IpSecurityRestrictions = state.IpSecurityRestrictions @ [IpSecurityRestriction.Create name cidr Allow] } - member this.AllowIp(state, name, ip:Net.IPAddress) : SlotConfig = + member this.AllowIp(state, name, ip:Net.IPAddress) : SlotConfig = let cidr = { Address = ip; Prefix = 32 } this.AllowIp(state, name, cidr) - member this.AllowIp(state, name, ip:string) : SlotConfig = + member this.AllowIp(state, name, ip:string) : SlotConfig = let cidr = IPAddressCidr.parse ip this.AllowIp(state, name, cidr) /// Add Denied ip for ip security restrictions - [] - member _.DenyIp(state, name, cidr:IPAddressCidr) : SlotConfig = + [] + member _.DenyIp(state, name, cidr:IPAddressCidr) : SlotConfig = { state with IpSecurityRestrictions = state.IpSecurityRestrictions @ [IpSecurityRestriction.Create name cidr Deny] } - member this.DenyIp(state, name, ip:Net.IPAddress) : SlotConfig = + member this.DenyIp(state, name, ip:Net.IPAddress) : SlotConfig = let cidr = { Address = ip; Prefix = 32 } this.DenyIp(state, name, cidr) - member this.DenyIp(state, name, ip:string) : SlotConfig = + member this.DenyIp(state, name, ip:string) : SlotConfig = let cidr = IPAddressCidr.parse ip - this.DenyIp(state, name, cidr) + this.DenyIp(state, name, cidr) interface ITaggable with member _.Add state tags = { state with Tags = state.Tags |> Map.merge tags } interface IDependable with member _.Add state newDeps = { state with Dependencies = state.Dependencies + newDeps } @@ -228,7 +228,7 @@ type WebAppConfig = AutomaticLoggingExtension : bool SiteExtensions : ExtensionName Set PrivateEndpoints: (LinkedResource * string option) Set - CustomDomains : Map + CustomDomains : Map DockerPort: int option ZoneRedundant : FeatureFlag option } member this.Name = this.CommonWebConfig.Name @@ -311,9 +311,9 @@ type WebAppConfig = | Linux, Some _ | _ , None -> () - + yield! this.DockerPort |> Option.mapList AppSettings.WebsitesPort - + if this.DockerCi then "DOCKER_ENABLE_CI", "true" ] @@ -480,6 +480,8 @@ type WebAppConfig = Location = location DisableIpMasking = false SamplingPercentage = 100 + Type = Classic + Dependencies = Set.empty LinkedWebsite = match this.CommonWebConfig.OperatingSystem with | Windows -> Some this.Name.ResourceName @@ -499,7 +501,7 @@ type WebAppConfig = MaximumElasticWorkerCount = this.MaximumElasticWorkerCount OperatingSystem = this.CommonWebConfig.OperatingSystem ZoneRedundant = this.ZoneRedundant - Tags = this.Tags} + Tags = this.Tags } | _ -> () @@ -514,13 +516,13 @@ type WebAppConfig = { site with AppSettings = None; ConnectionStrings = None } // Don't deploy production slot settings as they could cause an app restart for (_,slot) in this.CommonWebConfig.Slots |> Map.toSeq do slot.ToSite site - + // Host Name Bindings must be deployed sequentially to avoid an error, as the site cannot be modified concurrently. // To do so we add a dependency to the previous binding. let mutable previousHostNameBinding = None for customDomain in this.CustomDomains |> Map.toSeq |> Seq.map snd do - let dependsOn = - match previousHostNameBinding with + let dependsOn = + match previousHostNameBinding with | Some previous -> Set.singleton previous | None -> Set.empty @@ -544,16 +546,16 @@ type WebAppConfig = hostNameBinding // Get the resource group which contains the app service plan - let aspRgName = + let aspRgName = match this.CommonWebConfig.ServicePlan with | LinkedResource linked -> linked.ResourceId.ResourceGroup | _ -> None // Create a nested resource group deployment for the certificate - this isn't strictly necessary when the app & app service plan are in the same resource group // however, when they are in different resource groups this is required to make the deployment succeed (there is an ARM bug which causes a Not Found / Conflict otherwise) // To keep the code simple, I opted to always nest the certificate deployment. - TheRSP 2021-12-14 - let certRg = resourceGroup { + let certRg = resourceGroup { name (aspRgName |> Option.defaultValue "[resourceGroup().name]") - add_resource + add_resource { cert with SiteId = Unmanaged cert.SiteId.ResourceId ServicePlanId = Unmanaged cert.ServicePlanId.ResourceId } @@ -838,7 +840,7 @@ module Extensions = let current = this.Get state connectionStrings |> List.fold (fun (state:CommonWebConfig) (key, value:ArmExpression) -> - { state with ConnectionStrings = state.ConnectionStrings.Add(key, (ExpressionSetting value, Custom)) }) current + { state with ConnectionStrings = state.ConnectionStrings.Add(key, (ExpressionSetting value, Custom)) }) current |> this.Wrap state /// Sets an app setting of the web app in the form "key" "value". [] @@ -956,16 +958,16 @@ module Extensions = /// Specifies the path Azure load balancers will ping to check for unhealthy instances. member this.HealthCheckPath(state:'T, healthCheckPath:string) = this.Map state (fun x -> {x with HealthCheckPath = Some(healthCheckPath)}) /// Add Allowed ip for ip security restrictions - [] - member this.AllowIp(state:'T, name, ip:IPAddressCidr) = + [] + member this.AllowIp(state:'T, name, ip:IPAddressCidr) = this.Map state (fun x -> { x with IpSecurityRestrictions = IpSecurityRestriction.Create name ip Allow :: x.IpSecurityRestrictions }) - member this.AllowIp(state:'T, name, ip:string) = + member this.AllowIp(state:'T, name, ip:string) = let ip = IPAddressCidr.parse ip this.Map state (fun x -> { x with IpSecurityRestrictions = IpSecurityRestriction.Create name ip Allow :: x.IpSecurityRestrictions }) /// Add Denied ip for ip security restrictions - [] - member this.DenyIp(state:'T, name, ip) = + [] + member this.DenyIp(state:'T, name, ip) = this.Map state (fun x -> { x with IpSecurityRestrictions = IpSecurityRestriction.Create name ip Deny :: x.IpSecurityRestrictions }) - member this.DenyIp(state:'T, name, ip:string) = + member this.DenyIp(state:'T, name, ip:string) = let ip = IPAddressCidr.parse ip this.Map state (fun x -> { x with IpSecurityRestrictions = IpSecurityRestriction.Create name ip Deny :: x.IpSecurityRestrictions }) diff --git a/src/Farmer/Types.fs b/src/Farmer/Types.fs index f46bdc679..2350f4779 100644 --- a/src/Farmer/Types.fs +++ b/src/Farmer/Types.fs @@ -244,8 +244,8 @@ type CertificateOptions = type DomainConfig = | SecureDomain of domain:string * cert:CertificateOptions | InsecureDomain of domain:string - member this.DomainName = - match this with + member this.DomainName = + match this with | SecureDomain (domainName,_) | InsecureDomain (domainName) -> domainName diff --git a/src/Tests/AppInsights.fs b/src/Tests/AppInsights.fs index 28772bcfe..1c883985b 100644 --- a/src/Tests/AppInsights.fs +++ b/src/Tests/AppInsights.fs @@ -3,6 +3,8 @@ module AppInsights open Expecto open Farmer open Farmer.Builders.AppInsights +open Farmer.Builders.LogAnalytics +open Newtonsoft.Json.Linq let tests = testList "AppInsights" [ test "Creates keys on an AI instance correctly" { @@ -11,8 +13,32 @@ let tests = testList "AppInsights" [ Expect.equal ai.InstrumentationKey.Value ("reference(resourceId('Microsoft.Insights/components', 'foo'), '2014-04-01').InstrumentationKey") "Incorrect Value" } + test "Creates with classic version by default" { + let deployment = arm { add_resource (appInsights { name "foo" }) } + let json = deployment.Template |> Writer.toJson |> JObject.Parse + let version = json.SelectToken("resources[?(@.name=='foo')].apiVersion").ToString() + Expect.equal version "2014-04-01" "Incorrect API version" + } + test "Create generated keys correctly" { let generatedKey = AppInsights.getInstrumentationKey(ResourceId.create(Arm.Insights.components, ResourceName "foo", "group")) Expect.equal generatedKey.Value "reference(resourceId('group', 'Microsoft.Insights/components', 'foo'), '2014-04-01').InstrumentationKey" "Incorrect generated key" } + + test "Creates LA-enabled workspace" { + let workspace = logAnalytics { name "la" } + let ai = appInsights { name "ai"; log_analytics_workspace workspace } + let deployment = arm { + add_resources [ workspace; ai ] + } + let json = deployment.Template |> Writer.toJson |> JObject.Parse + let version = json.SelectToken("resources[?(@.name=='ai')].apiVersion").ToString() + let resourceId = json.SelectToken("resources[?(@.name=='ai')].properties.WorkspaceResourceId").ToString() + let dependencies = json.SelectToken("resources[?(@.name=='ai')].dependsOn").Children() |> Seq.map string |> Seq.toArray + + Expect.equal resourceId "[resourceId('Microsoft.OperationalInsights/workspaces', 'la')]" "Incorrect workspace id" + Expect.equal version "2020-02-02-preview" "Incorrect API version" + Expect.equal ai.InstrumentationKey.Value ("reference(resourceId('Microsoft.Insights/components', 'ai'), '2020-02-02-preview').InstrumentationKey") "Incorrect Instrumentation Key reference" + Expect.sequenceEqual dependencies [ "[resourceId('Microsoft.OperationalInsights/workspaces', 'la')]" ] "Incorrect dependencies" + } ] \ No newline at end of file diff --git a/src/Tests/test-data/lots-of-resources.json b/src/Tests/test-data/lots-of-resources.json index 061382fbf..1c7556329 100644 --- a/src/Tests/test-data/lots-of-resources.json +++ b/src/Tests/test-data/lots-of-resources.json @@ -97,6 +97,7 @@ }, { "apiVersion": "2014-04-01", + "dependsOn": [], "kind": "web", "location": "northeurope", "name": "farmerwebapp1979-ai", @@ -236,6 +237,7 @@ }, { "apiVersion": "2014-04-01", + "dependsOn": [], "kind": "web", "location": "northeurope", "name": "farmerfuncs1979-ai", From 4754169db427493daf7c8b30045c7465d82fe39a Mon Sep 17 00:00:00 2001 From: Jon Roberts Date: Sat, 12 Mar 2022 11:23:32 -0600 Subject: [PATCH 46/73] add VM Priority and Spot Instance --- src/Farmer/Arm/Compute.fs | 25 ++++++++++++++++++------- src/Farmer/Builders/Builders.Vm.fs | 13 +++++++++++++ src/Farmer/Common.fs | 14 ++++++++++++++ 3 files changed, 45 insertions(+), 7 deletions(-) diff --git a/src/Farmer/Arm/Compute.fs b/src/Farmer/Arm/Compute.fs index 229dafade..7c159b39c 100644 --- a/src/Farmer/Arm/Compute.fs +++ b/src/Farmer/Arm/Compute.fs @@ -7,7 +7,7 @@ open Farmer.Vm open System open System.Text -let virtualMachines = ResourceType ("Microsoft.Compute/virtualMachines", "2018-10-01") +let virtualMachines = ResourceType ("Microsoft.Compute/virtualMachines", "2019-03-01") let extensions = ResourceType ("Microsoft.Compute/virtualMachines/extensions", "2019-12-01") type CustomScriptExtension = @@ -70,6 +70,7 @@ type VirtualMachine = Location : Location StorageAccount : ResourceName option Size : VMSize + Priority : Priority Credentials : {| Username : string; Password : SecureParameter |} CustomData : string option DisablePasswordAuthentication: bool option @@ -93,12 +94,9 @@ type VirtualMachine = networkInterfaces.resourceId this.NetworkInterfaceName yield! this.StorageAccount |> Option.mapList storageAccounts.resourceId ] - {| virtualMachines.Create(this.Name, this.Location, dependsOn, this.Tags) with - identity = - if this.Identity = ManagedIdentity.Empty then Unchecked.defaultof<_> - else this.Identity.ToArmJson - properties = - {| hardwareProfile = {| vmSize = this.Size.ArmValue |} + let properties = + {| priority = this.Priority.ArmValue + hardwareProfile = {| vmSize = this.Size.ArmValue |} osProfile = {| computerName = this.Name.Value adminUsername = this.Credentials.Username @@ -161,4 +159,17 @@ type VirtualMachine = | None -> box {| bootDiagnostics = {| enabled = false |} |} |} + + {| virtualMachines.Create(this.Name, this.Location, dependsOn, this.Tags) with + identity = + if this.Identity = ManagedIdentity.Empty then Unchecked.defaultof<_> + else this.Identity.ToArmJson + properties = + match this.Priority with + | Low | Regular -> box properties + | Spot (evictionPolicy, maxPrice) -> + {| properties with + evictionPolicy = evictionPolicy.ArmValue + billingProfile = {| maxPrice = maxPrice |} + |} |} \ No newline at end of file diff --git a/src/Farmer/Builders/Builders.Vm.fs b/src/Farmer/Builders/Builders.Vm.fs index c2b75b0f5..3ada2350d 100644 --- a/src/Farmer/Builders/Builders.Vm.fs +++ b/src/Farmer/Builders/Builders.Vm.fs @@ -18,6 +18,8 @@ type VmConfig = { Name : ResourceName DiagnosticsStorageAccount : ResourceRef option + Priority: Priority + Username : string option PasswordParameter: string option Image : ImageDefinition @@ -67,6 +69,7 @@ type VmConfig = |> Option.map(fun r -> r.resourceId(this).Name) NetworkInterfaceName = this.NicName.Name Size = this.Size + Priority = this.Priority Credentials = match this.Username with | Some username -> @@ -189,6 +192,7 @@ type VirtualMachineBuilder() = member _.Yield _ = { Name = ResourceName.Empty DiagnosticsStorageAccount = None + Priority = Regular Size = Basic_A0 Username = None PasswordParameter = None @@ -258,6 +262,15 @@ type VirtualMachineBuilder() = /// Adds a data disk to the VM with a specific size and type. [] member _.AddDisk(state:VmConfig, size, diskType) = { state with DataDisks = { Size = size; DiskType = diskType } :: state.DataDisks } + /// Sets priority of VMm. Overrides spot_instance. + [] + member _.Priority(state:VmConfig, priority) = { state with Priority = priority } + /// Converts VM into a spot instance. Overides priority. + [] + member _.Spot(state:VmConfig, (evictionPolicy, maxPrice)) : VmConfig = { state with Priority = Spot (evictionPolicy, maxPrice) } + //member _.Spot(state:VmConfig, spotSettings: EvictionPolicy * decimal) : VmConfig = { state with Priority = Spot spotSettings } + member this.Spot(state:VmConfig, evictionPolicy:EvictionPolicy) : VmConfig = this.Spot(state,(evictionPolicy, -1m)) + member this.Spot(state:VmConfig, maxPrice) : VmConfig = this.Spot(state,(Deallocate, maxPrice)) /// Adds a SSD data disk to the VM with a specific size. [] member this.AddSsd(state:VmConfig, size) = this.AddDisk(state, size, StandardSSD_LRS) diff --git a/src/Farmer/Common.fs b/src/Farmer/Common.fs index 7e54eb8d3..fdff43b25 100644 --- a/src/Farmer/Common.fs +++ b/src/Farmer/Common.fs @@ -309,6 +309,20 @@ module Vm = /// Represents a disk in a VM. type DiskInfo = { Size : int; DiskType : DiskType } + type EvictionPolicy = + | Deallocate + | Delete + member this.ArmValue = match this with x -> x.ToString() + type BillingProfile = + { MaxPrice: decimal } + type Priority = + | Low + | Regular + | Spot of evictionPolicy:EvictionPolicy * maxPrice:decimal + member this.ArmValue = + match this with + | Spot _ -> "Spot" + | x -> x.ToString() module internal Validation = // ANDs two validation rules From 8d2053b7693e59c8d0e23fce193b965b4439236a Mon Sep 17 00:00:00 2001 From: Jon Roberts Date: Sat, 12 Mar 2022 11:24:23 -0600 Subject: [PATCH 47/73] update tests for vm prioirity --- src/Tests/test-data/lots-of-resources.json | 3 ++- src/Tests/test-data/vm.json | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Tests/test-data/lots-of-resources.json b/src/Tests/test-data/lots-of-resources.json index 061382fbf..a10379814 100644 --- a/src/Tests/test-data/lots-of-resources.json +++ b/src/Tests/test-data/lots-of-resources.json @@ -629,7 +629,7 @@ "type": "Microsoft.DocumentDb/databaseAccounts/mongodbDatabases" }, { - "apiVersion": "2018-10-01", + "apiVersion": "2019-03-01", "dependsOn": [ "[resourceId('Microsoft.Network/networkInterfaces', 'farmervm-nic')]" ], @@ -656,6 +656,7 @@ "adminUsername": "farmer-admin", "computerName": "farmervm" }, + "priority": "Regular", "storageProfile": { "dataDisks": [ { diff --git a/src/Tests/test-data/vm.json b/src/Tests/test-data/vm.json index 6c4f00977..046bb19ce 100644 --- a/src/Tests/test-data/vm.json +++ b/src/Tests/test-data/vm.json @@ -9,7 +9,7 @@ }, "resources": [ { - "apiVersion": "2018-10-01", + "apiVersion": "2019-03-01", "dependsOn": [ "[resourceId('Microsoft.Network/networkInterfaces', 'isaacsVM-nic')]", "[resourceId('Microsoft.Storage/storageAccounts', 'isaacsvmstorage')]" @@ -38,6 +38,7 @@ "adminUsername": "isaac", "computerName": "isaacsVM" }, + "priority": "Regular", "storageProfile": { "dataDisks": [ { From 6fea7c39b8a40339538e7a921896ea1903374902 Mon Sep 17 00:00:00 2001 From: Jon Roberts Date: Sat, 12 Mar 2022 11:24:45 -0600 Subject: [PATCH 48/73] add documentation for vm priority and spot instance --- docs/content/api-overview/resources/virtual-machine.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/content/api-overview/resources/virtual-machine.md b/docs/content/api-overview/resources/virtual-machine.md index 07bfd1b85..f704ffdbb 100644 --- a/docs/content/api-overview/resources/virtual-machine.md +++ b/docs/content/api-overview/resources/virtual-machine.md @@ -24,6 +24,8 @@ In addition, every VM you create will add a SecureString parameter to the ARM te |diagnostics_support|Turns on diagnostics support using an automatically created created storage account.| |diagnostics_support_external|Turns on diagnostics support using an existing storage account.| |vm_size|Sets the size of the VM.| +|priority|Sets the VM Priority. Overrides `spot_instance`.| +|spot_instance|Makes the VM a spot instance. Overrides `priority`| |username|Sets the admin username of the VM (note: the password is supplied as a securestring parameter to the generated ARM template).| |password_parameter|Sets the name of the parameter which contains the admin password for this VM. defaults to "password-for-"| |operating_system|Sets the operating system of the VM. A set of samples is provided in the `CommonImages` module.| From de90ac8e5beb147cbf8966f1fa24ce14d4fde3e1 Mon Sep 17 00:00:00 2001 From: Jon Roberts Date: Sat, 12 Mar 2022 11:25:05 -0600 Subject: [PATCH 49/73] add sample for vm spot instance --- Farmer.sln | 1 + samples/scripts/vm-spot-instance.fsx | 26 ++++++++++++++++++++++++++ 2 files changed, 27 insertions(+) create mode 100644 samples/scripts/vm-spot-instance.fsx diff --git a/Farmer.sln b/Farmer.sln index bfe6f246d..1add8239d 100644 --- a/Farmer.sln +++ b/Farmer.sln @@ -45,6 +45,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "scripts", "scripts", "{C399 samples\scripts\storage.fsx = samples\scripts\storage.fsx samples\scripts\template.json = samples\scripts\template.json samples\scripts\vm.fsx = samples\scripts\vm.fsx + samples\scripts\vm-spot-instance.fsx = samples\scripts\vm-spot-instance.fsx samples\scripts\vnet-gateway.fsx = samples\scripts\vnet-gateway.fsx samples\scripts\vnet-hub-and-spoke.fsx = samples\scripts\vnet-hub-and-spoke.fsx samples\scripts\vnet.fsx = samples\scripts\vnet.fsx diff --git a/samples/scripts/vm-spot-instance.fsx b/samples/scripts/vm-spot-instance.fsx new file mode 100644 index 000000000..9d3b6d9ef --- /dev/null +++ b/samples/scripts/vm-spot-instance.fsx @@ -0,0 +1,26 @@ +#r "nuget:Farmer" + +open Farmer +open Farmer.Builders +open Farmer.Vm + +let myVm = vm { + name "isaacsVM" + username "isaac" + spot_instance Deallocate + vm_size Standard_A2 + operating_system WindowsServer_2012Datacenter + os_disk 128 StandardSSD_LRS + add_ssd_disk 128 + add_slow_disk 512 + diagnostics_support + system_identity +} + +let deployment = arm { + location Location.NorthEurope + add_resource myVm +} + +deployment +|> Deploy.execute "my-resource-group-name" Deploy.NoParameters \ No newline at end of file From c45f736432bb4fadb1fc1f78020b213249cb1970 Mon Sep 17 00:00:00 2001 From: Jon Roberts Date: Sat, 12 Mar 2022 11:28:41 -0600 Subject: [PATCH 50/73] Update release notes to include VM Priority and Spot Instance --- RELEASE_NOTES.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 3979a9e8b..2d9eff841 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,6 +1,9 @@ Release Notes ============= +## 1.6.30 +* VMs: Allow setting Priority and Spot Instance Settings + ## 1.6.30 * WebApps/Functions: Allow adding IP restriction string with CIDR From 1363c910aeeaba7427e37bff646f5af358457bec Mon Sep 17 00:00:00 2001 From: Jon Roberts Date: Sat, 12 Mar 2022 11:33:35 -0600 Subject: [PATCH 51/73] update release notes to 1.6.31 correctly --- RELEASE_NOTES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 2d9eff841..0073232c7 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,7 +1,7 @@ Release Notes ============= -## 1.6.30 +## 1.6.31 * VMs: Allow setting Priority and Spot Instance Settings ## 1.6.30 From 540a71ebce695acd13acafb0623191212bfd134d Mon Sep 17 00:00:00 2001 From: Jon Roberts Date: Sat, 12 Mar 2022 11:36:28 -0600 Subject: [PATCH 52/73] cleanup comments --- src/Farmer/Builders/Builders.Vm.fs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Farmer/Builders/Builders.Vm.fs b/src/Farmer/Builders/Builders.Vm.fs index 3ada2350d..a3450bdf4 100644 --- a/src/Farmer/Builders/Builders.Vm.fs +++ b/src/Farmer/Builders/Builders.Vm.fs @@ -265,10 +265,9 @@ type VirtualMachineBuilder() = /// Sets priority of VMm. Overrides spot_instance. [] member _.Priority(state:VmConfig, priority) = { state with Priority = priority } - /// Converts VM into a spot instance. Overides priority. + /// Makes VM a spot instance. Overrides priority. [] member _.Spot(state:VmConfig, (evictionPolicy, maxPrice)) : VmConfig = { state with Priority = Spot (evictionPolicy, maxPrice) } - //member _.Spot(state:VmConfig, spotSettings: EvictionPolicy * decimal) : VmConfig = { state with Priority = Spot spotSettings } member this.Spot(state:VmConfig, evictionPolicy:EvictionPolicy) : VmConfig = this.Spot(state,(evictionPolicy, -1m)) member this.Spot(state:VmConfig, maxPrice) : VmConfig = this.Spot(state,(Deallocate, maxPrice)) /// Adds a SSD data disk to the VM with a specific size. From 019a1ee3dde1b03166884c66b497e6061d01aa65 Mon Sep 17 00:00:00 2001 From: isaac Date: Sun, 13 Mar 2022 22:16:51 +0100 Subject: [PATCH 53/73] Add docs & samples. --- RELEASE_NOTES.md | 2 ++ .../api-overview/resources/app-insights.md | 4 +++ samples/scripts/appinsights-loganalytics.fsx | 26 +++++++++++++++++++ 3 files changed, 32 insertions(+) create mode 100644 samples/scripts/appinsights-loganalytics.fsx diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 3979a9e8b..db10e5e6c 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,5 +1,7 @@ Release Notes ============= +## vNext +* Application Insights: Support for Workspace-enabled instances. ## 1.6.30 * WebApps/Functions: Allow adding IP restriction string with CIDR diff --git a/docs/content/api-overview/resources/app-insights.md b/docs/content/api-overview/resources/app-insights.md index 61ef6d4dd..949e2a6b9 100644 --- a/docs/content/api-overview/resources/app-insights.md +++ b/docs/content/api-overview/resources/app-insights.md @@ -10,6 +10,8 @@ The App Insights builder is used to create Application Insights accounts. Use th * Application Insights (`Microsoft.Insights/components`) +> This builder supports both "Classic" (standalone) and "Workspace Enabled" (Log Analytics-backed) instances of App Insights. See the `log_analytics_workspace` keyword to see how to create the latter type of instance. + #### Builder Keywords | Keyword | Purpose | @@ -17,6 +19,7 @@ The App Insights builder is used to create Application Insights accounts. Use th | name | Sets the name of the App Insights instance. | | disable_ip_masking | Disable IP masking. | | sampling_percentage | Define sampling percentage (0-100) | +| log_analytics_workspace | Use a Log Analytics workspace as the backing store for this AI instance. You can supply either a Farmer-generate Log Analytics`WorkspaceConfig` instance that exists in the same resource group, or a fully-qualified Resource ID path to that instance. This will also switch the AI instance over to creating a "workspace enabled" AI instance. | #### Configuration Members @@ -32,5 +35,6 @@ open Farmer.Builders let ai = appInsights { name "myAI" + log_analytics_workspace myWorkspace // use to activate workspace-enabled AI instances. } ``` \ No newline at end of file diff --git a/samples/scripts/appinsights-loganalytics.fsx b/samples/scripts/appinsights-loganalytics.fsx new file mode 100644 index 000000000..16b3bc6f6 --- /dev/null +++ b/samples/scripts/appinsights-loganalytics.fsx @@ -0,0 +1,26 @@ +#r @"nuget:Farmer" + +open Farmer +open Farmer.Builders + +let workspace = logAnalytics { + name "loganalytics-workspace" +} + +let myAppInsights = appInsights { + name "appInsights" + log_analytics_workspace workspace +} + +let myFunctions = functions { + name "functions-app" + link_to_app_insights myAppInsights.Name +} + +let template = arm { + location Location.NorthEurope + add_resources [ workspace; myAppInsights; myFunctions ] +} + +template +|> Deploy.execute "deleteme" Deploy.NoParameters \ No newline at end of file From 522b3cf3bb0787e0eccaf44a02fb351289e0a968 Mon Sep 17 00:00:00 2001 From: isaac Date: Sun, 13 Mar 2022 22:35:20 +0100 Subject: [PATCH 54/73] Small rename --- src/Farmer/Arm/Insights.fs | 19 +++++++++++-------- src/Farmer/Builders/Builders.AppInsights.fs | 16 ++++++++-------- src/Farmer/Builders/Builders.Functions.fs | 2 +- src/Farmer/Builders/Builders.WebApp.fs | 2 +- 4 files changed, 21 insertions(+), 18 deletions(-) diff --git a/src/Farmer/Arm/Insights.fs b/src/Farmer/Arm/Insights.fs index dd61a1ad6..3a48ce2fb 100644 --- a/src/Farmer/Arm/Insights.fs +++ b/src/Farmer/Arm/Insights.fs @@ -3,14 +3,17 @@ module Farmer.Arm.Insights open Farmer -let components = ResourceType("Microsoft.Insights/components", "2014-04-01") -let componentsWorkspace = ResourceType("Microsoft.Insights/components", "2020-02-02") +let private createComponents version = ResourceType("Microsoft.Insights/components", version) +/// Classic AI instance +let components = createComponents "2014-04-01" +/// Workspace-enabled AI instance +let componentsWorkspace = createComponents "2020-02-02" /// The type of AI instance to create. -type ComponentsType = +type InstanceKind = | Classic | Workspace of ResourceId - member this.ComponentsType = + member this.ResourceType = match this with | Classic -> components | Workspace _ -> componentsWorkspace @@ -21,7 +24,7 @@ type Components = LinkedWebsite : ResourceName option DisableIpMasking : bool SamplingPercentage : int - Type : ComponentsType + InstanceKind : InstanceKind Tags: Map Dependencies : ResourceId Set } interface IArmResource with @@ -31,7 +34,7 @@ type Components = match this.LinkedWebsite with | Some linkedWebsite -> this.Tags.Add($"[concat('hidden-link:', resourceGroup().id, '/providers/Microsoft.Web/sites/', '{linkedWebsite.Value}')]", "Resource") | None -> this.Tags - {| this.Type.ComponentsType.Create(this.Name, this.Location, this.Dependencies, tags) with + {| this.InstanceKind.ResourceType.Create(this.Name, this.Location, this.Dependencies, tags) with kind = "web" properties = {| @@ -44,11 +47,11 @@ type Components = DisableIpMasking = this.DisableIpMasking SamplingPercentage = this.SamplingPercentage IngestionMode = - match this.Type with + match this.InstanceKind with | Workspace _ -> "LogAnalytics" | Classic -> null WorkspaceResourceId = - match this.Type with + match this.InstanceKind with | Workspace resourceId -> resourceId.Eval() | Classic -> null |} |} \ No newline at end of file diff --git a/src/Farmer/Builders/Builders.AppInsights.fs b/src/Farmer/Builders/Builders.AppInsights.fs index 35fdc5835..fd5ddf62d 100644 --- a/src/Farmer/Builders/Builders.AppInsights.fs +++ b/src/Farmer/Builders/Builders.AppInsights.fs @@ -11,19 +11,19 @@ type AppInsights = .reference(resourceId) .Map(fun r -> r + ".InstrumentationKey") .WithOwner(resourceId) - static member getInstrumentationKey (name:ResourceName, ?resourceGroup, ?componentsType) = - let componentsType = componentsType |> Option.defaultValue components - AppInsights.getInstrumentationKey(ResourceId.create (componentsType, name, ?group = resourceGroup)) + static member getInstrumentationKey (name:ResourceName, ?resourceGroup, ?resourceType) = + let resourceType = resourceType |> Option.defaultValue components + AppInsights.getInstrumentationKey(ResourceId.create (resourceType, name, ?group = resourceGroup)) type AppInsightsConfig = { Name : ResourceName DisableIpMasking : bool SamplingPercentage : int - Type : ComponentsType + InstanceKind : InstanceKind Dependencies : ResourceId Set Tags : Map } /// Gets the ARM expression path to the instrumentation key of this App Insights instance. - member this.InstrumentationKey = AppInsights.getInstrumentationKey(this.Name, componentsType = this.Type.ComponentsType) + member this.InstrumentationKey = AppInsights.getInstrumentationKey(this.Name, resourceType = this.InstanceKind.ResourceType) interface IBuilder with member this.ResourceId = components.resourceId this.Name member this.BuildResources location = [ @@ -33,7 +33,7 @@ type AppInsightsConfig = DisableIpMasking = this.DisableIpMasking SamplingPercentage = this.SamplingPercentage Dependencies = this.Dependencies - Type = this.Type + InstanceKind = this.InstanceKind Tags = this.Tags } ] @@ -44,7 +44,7 @@ type AppInsightsBuilder() = SamplingPercentage = 100 Tags = Map.empty Dependencies = Set.empty - Type = Classic } + InstanceKind = Classic } [] /// Sets the name of the App Insights instance. @@ -62,7 +62,7 @@ type AppInsightsBuilder() = [] member _.Workspace(state:AppInsightsConfig, workspace:ResourceId) = { state with - Type = Workspace workspace + InstanceKind = Workspace workspace Dependencies = state.Dependencies.Add workspace } member this.Workspace(state:AppInsightsConfig, workspace:WorkspaceConfig) = diff --git a/src/Farmer/Builders/Builders.Functions.fs b/src/Farmer/Builders/Builders.Functions.fs index c3fd8fd8a..923881af6 100644 --- a/src/Farmer/Builders/Builders.Functions.fs +++ b/src/Farmer/Builders/Builders.Functions.fs @@ -311,7 +311,7 @@ type FunctionsConfig = DisableIpMasking = false SamplingPercentage = 100 Dependencies = Set.empty - Type = Classic + InstanceKind = Classic LinkedWebsite = match this.CommonWebConfig.OperatingSystem with | Windows -> Some this.Name.ResourceName diff --git a/src/Farmer/Builders/Builders.WebApp.fs b/src/Farmer/Builders/Builders.WebApp.fs index cbf433bc4..579e141d2 100644 --- a/src/Farmer/Builders/Builders.WebApp.fs +++ b/src/Farmer/Builders/Builders.WebApp.fs @@ -480,7 +480,7 @@ type WebAppConfig = Location = location DisableIpMasking = false SamplingPercentage = 100 - Type = Classic + InstanceKind = Classic Dependencies = Set.empty LinkedWebsite = match this.CommonWebConfig.OperatingSystem with From a9f32a43f5f25e71746e452c4e4876d335161432 Mon Sep 17 00:00:00 2001 From: isaac Date: Mon, 14 Mar 2022 22:53:39 +0100 Subject: [PATCH 55/73] Fix test --- src/Tests/AppInsights.fs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Tests/AppInsights.fs b/src/Tests/AppInsights.fs index 1c883985b..75e0db5a1 100644 --- a/src/Tests/AppInsights.fs +++ b/src/Tests/AppInsights.fs @@ -37,8 +37,8 @@ let tests = testList "AppInsights" [ let dependencies = json.SelectToken("resources[?(@.name=='ai')].dependsOn").Children() |> Seq.map string |> Seq.toArray Expect.equal resourceId "[resourceId('Microsoft.OperationalInsights/workspaces', 'la')]" "Incorrect workspace id" - Expect.equal version "2020-02-02-preview" "Incorrect API version" - Expect.equal ai.InstrumentationKey.Value ("reference(resourceId('Microsoft.Insights/components', 'ai'), '2020-02-02-preview').InstrumentationKey") "Incorrect Instrumentation Key reference" + Expect.equal version "2020-02-02" "Incorrect API version" + Expect.equal ai.InstrumentationKey.Value ("reference(resourceId('Microsoft.Insights/components', 'ai'), '2020-02-02').InstrumentationKey") "Incorrect Instrumentation Key reference" Expect.sequenceEqual dependencies [ "[resourceId('Microsoft.OperationalInsights/workspaces', 'la')]" ] "Incorrect dependencies" } ] \ No newline at end of file From 6578a0f008577b3beb06342056cae8c258f15ed3 Mon Sep 17 00:00:00 2001 From: Richard Sanderson-Pope Date: Tue, 15 Mar 2022 08:09:35 +0000 Subject: [PATCH 56/73] rename route_via_vnet -> vnet --- src/Farmer/Builders/Builders.WebApp.fs | 2 +- src/Tests/Functions.fs | 4 ++-- src/Tests/WebApp.fs | 8 ++++---- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Farmer/Builders/Builders.WebApp.fs b/src/Farmer/Builders/Builders.WebApp.fs index 2a93d4e18..2875429dc 100644 --- a/src/Farmer/Builders/Builders.WebApp.fs +++ b/src/Farmer/Builders/Builders.WebApp.fs @@ -975,7 +975,7 @@ module Extensions = member this.DenyIp(state:'T, name, ip) = this.Map state (fun x -> { x with IpSecurityRestrictions = IpSecurityRestriction.Create name ip Deny :: x.IpSecurityRestrictions }) /// Integrate this app with a virtual network subnet - [] + [] member this.RouteViaVNet(state:'T, subnet:SubnetReference option) = match subnet with | Some subnetId -> diff --git a/src/Tests/Functions.fs b/src/Tests/Functions.fs index e5e0254cf..aeb0be6bb 100644 --- a/src/Tests/Functions.fs +++ b/src/Tests/Functions.fs @@ -416,7 +416,7 @@ let tests = testList "Functions tests" [ } test "Can integrate unmanaged vnet" { let subnetId = Arm.Network.subnets.resourceId (ResourceName "my-vnet", ResourceName "my-subnet") - let wa = functions { name "testApp"; route_via_vnet (Unmanaged subnetId) } + let wa = functions { name "testApp"; vnet (Unmanaged subnetId) } let resources = wa |> getResources let site = resources |> getResource |> List.head @@ -429,7 +429,7 @@ let tests = testList "Functions tests" [ test "Can integrate managed vnet" { let vnetConfig = vnet { name "my-vnet" } - let wa = functions { name "testApp"; route_via_vnet (vnetConfig, ResourceName "my-subnet") } + let wa = functions { name "testApp"; vnet (vnetConfig, ResourceName "my-subnet") } let resources = wa |> getResources let site = resources |> getResource |> List.head diff --git a/src/Tests/WebApp.fs b/src/Tests/WebApp.fs index c6e0820e9..ceae4540f 100644 --- a/src/Tests/WebApp.fs +++ b/src/Tests/WebApp.fs @@ -814,9 +814,9 @@ let tests = testList "Web App Tests" [ Expect.equal sf.ZoneRedundant (Some Enabled) "ZoneRedundant should be enabled" } - test "Can integrate unmanaged vnet" { + test "Can integrate with unmanaged vnet" { let subnetId = Arm.Network.subnets.resourceId (ResourceName "my-vnet", ResourceName "my-subnet") - let wa = webApp { name "testApp"; route_via_vnet (Unmanaged subnetId) } + let wa = webApp { name "testApp"; vnet (Unmanaged subnetId) } let resources = wa |> getResources let site = resources |> getResource |> List.head @@ -827,9 +827,9 @@ let tests = testList "Web App Tests" [ Expect.hasLength vnetConnections 1 "incorrect number of Vnet connections" } - test "Can integrate managed vnet" { + test "Can integrate with managed vnet" { let vnetConfig = vnet { name "my-vnet" } - let wa = webApp { name "testApp"; route_via_vnet (vnetConfig, ResourceName "my-subnet") } + let wa = webApp { name "testApp"; vnet (vnetConfig, ResourceName "my-subnet") } let resources = wa |> getResources let site = resources |> getResource |> List.head From 6407710b8c3d56c792b953832da822876c97a33f Mon Sep 17 00:00:00 2001 From: Richard Sanderson-Pope Date: Tue, 15 Mar 2022 08:31:06 +0000 Subject: [PATCH 57/73] Added validation --- src/Farmer/Builders/Builders.Functions.fs | 6 +++--- src/Farmer/Builders/Builders.WebApp.fs | 20 +++++++++++++++----- src/Tests/WebApp.fs | 4 ++-- 3 files changed, 20 insertions(+), 10 deletions(-) diff --git a/src/Farmer/Builders/Builders.Functions.fs b/src/Farmer/Builders/Builders.Functions.fs index c1910219b..f9de547d1 100644 --- a/src/Farmer/Builders/Builders.Functions.fs +++ b/src/Farmer/Builders/Builders.Functions.fs @@ -276,7 +276,7 @@ type FunctionsConfig = WorkerProcess = this.CommonWebConfig.WorkerProcess HealthCheckPath = this.CommonWebConfig.HealthCheckPath IpSecurityRestrictions = this.CommonWebConfig.IpSecurityRestrictions - LinkToSubnet = this.CommonWebConfig.RouteViaSubnet } + LinkToSubnet = this.CommonWebConfig.IntegratedSubnet } match this.CommonWebConfig.ServicePlan with | DeployableResource this.Name.ResourceName resourceId -> @@ -321,7 +321,7 @@ type FunctionsConfig = | None -> () - match this.CommonWebConfig.RouteViaSubnet with + match this.CommonWebConfig.IntegratedSubnet with | None -> () | Some subnetRef -> { Site = site @@ -358,7 +358,7 @@ type FunctionsBuilder() = ZipDeployPath = None HealthCheckPath = None IpSecurityRestrictions = [] - RouteViaSubnet = None + IntegratedSubnet = None PrivateEndpoints = Set.empty} StorageAccount = derived (fun config -> let storage = config.Name.ResourceName.Map (sprintf "%sstorage") |> sanitiseStorage |> ResourceName diff --git a/src/Farmer/Builders/Builders.WebApp.fs b/src/Farmer/Builders/Builders.WebApp.fs index 2875429dc..8257e0790 100644 --- a/src/Farmer/Builders/Builders.WebApp.fs +++ b/src/Farmer/Builders/Builders.WebApp.fs @@ -207,7 +207,7 @@ type CommonWebConfig = ZipDeployPath : (string*ZipDeploy.ZipDeploySlot) option HealthCheckPath: string option IpSecurityRestrictions: IpSecurityRestriction list - RouteViaSubnet : SubnetReference option + IntegratedSubnet : SubnetReference option PrivateEndpoints: (SubnetReference * string option) Set } type WebAppConfig = @@ -458,7 +458,7 @@ type WebAppConfig = ZipDeployPath = this.CommonWebConfig.ZipDeployPath |> Option.map (fun (path,slot) -> path, ZipDeploy.ZipDeployTarget.WebApp, slot ) HealthCheckPath = this.CommonWebConfig.HealthCheckPath IpSecurityRestrictions = this.CommonWebConfig.IpSecurityRestrictions - LinkToSubnet = this.CommonWebConfig.RouteViaSubnet } + LinkToSubnet = this.CommonWebConfig.IntegratedSubnet } match keyVault with | Some keyVault -> @@ -586,7 +586,7 @@ type WebAppConfig = } :> IBuilder).BuildResources location | _ -> () - match this.CommonWebConfig.RouteViaSubnet with + match this.CommonWebConfig.IntegratedSubnet with | None -> () | Some subnetRef -> { Site = site @@ -616,7 +616,7 @@ type WebAppBuilder() = ZipDeployPath = None HealthCheckPath = None IpSecurityRestrictions = [] - RouteViaSubnet = None + IntegratedSubnet = None PrivateEndpoints = Set.empty } Sku = Sku.F1 WorkerSize = Small @@ -642,6 +642,16 @@ type WebAppBuilder() = ZoneRedundant = None } member _.Run(state:WebAppConfig) = if state.Name.ResourceName = ResourceName.Empty then raiseFarmer "No Web App name has been set." + match state.CommonWebConfig with + | { ServicePlan = LinkedResource _ } -> () // can't validate as validation dependent on linked resource + | { IntegratedSubnet = None } -> () // no VNet to validate + | _ -> + match state.Sku with + | Standard _ -> () + | Premium _ | PremiumV2 _| PremiumV3 _ -> () + | ElasticPremium _ -> () + | Isolated _ -> () + | other -> raiseFarmer $"Web apps with SKU '%A{other}' do not support vnet integration." { state with SiteExtensions = match state with @@ -982,7 +992,7 @@ module Extensions = if subnetId.ResourceId.Type.Type <> Arm.Network.subnets.Type then raiseFarmer $"given resource was not of type '{Arm.Network.subnets.Type}'." | None -> () - this.Map state (fun x -> {x with RouteViaSubnet = subnet}) + this.Map state (fun x -> {x with IntegratedSubnet = subnet}) member this.RouteViaVNet(state:'T, subnetRef) = this.RouteViaVNet (state, Some subnetRef) member this.RouteViaVNet(state:'T, subnetId:LinkedResource) = this.RouteViaVNet (state, SubnetReference.create subnetId) member this.RouteViaVNet(state:'T, subnet:SubnetConfig) = this.RouteViaVNet (state, SubnetReference.create subnet) diff --git a/src/Tests/WebApp.fs b/src/Tests/WebApp.fs index ceae4540f..58f39ebc4 100644 --- a/src/Tests/WebApp.fs +++ b/src/Tests/WebApp.fs @@ -816,7 +816,7 @@ let tests = testList "Web App Tests" [ } test "Can integrate with unmanaged vnet" { let subnetId = Arm.Network.subnets.resourceId (ResourceName "my-vnet", ResourceName "my-subnet") - let wa = webApp { name "testApp"; vnet (Unmanaged subnetId) } + let wa = webApp { name "testApp"; sku WebApp.Sku.S1; vnet (Unmanaged subnetId) } let resources = wa |> getResources let site = resources |> getResource |> List.head @@ -829,7 +829,7 @@ let tests = testList "Web App Tests" [ test "Can integrate with managed vnet" { let vnetConfig = vnet { name "my-vnet" } - let wa = webApp { name "testApp"; vnet (vnetConfig, ResourceName "my-subnet") } + let wa = webApp { name "testApp"; sku WebApp.Sku.S1; vnet (vnetConfig, ResourceName "my-subnet") } let resources = wa |> getResources let site = resources |> getResource |> List.head From f9d8cd85dbdd9ecc2b306483a1f61c0e9b984b0a Mon Sep 17 00:00:00 2001 From: Richard Sanderson-Pope Date: Tue, 15 Mar 2022 08:36:21 +0000 Subject: [PATCH 58/73] Updated release notes --- RELEASE_NOTES.md | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 497ff593b..e09812a21 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,13 +1,10 @@ Release Notes ============= -## 1.6.28 -- WebApps/Functions: Add support for vnet integration - -## vNext -* WebApps/Functions: Specify connection string types ## 1.6.30 * WebApps/Functions: Allow adding IP restriction string with CIDR +* WebApps/Functions: Specify connection string types +* WebApps/Functions: Add support for vnet integration ## 1.6.29 * CLI: include `--only-show-error` option when executing Azure CLI commands. From 77c2b5b9a8ccbec2c4058e398b7a28d40cface7a Mon Sep 17 00:00:00 2001 From: isaac Date: Tue, 15 Mar 2022 14:19:08 +0100 Subject: [PATCH 59/73] Update release notes and simplify tests. --- RELEASE_NOTES.md | 4 +--- src/Tests/AppInsights.fs | 11 +++++------ 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index db10e5e6c..a6f1790b3 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,10 +1,8 @@ Release Notes ============= -## vNext -* Application Insights: Support for Workspace-enabled instances. - ## 1.6.30 * WebApps/Functions: Allow adding IP restriction string with CIDR +* Application Insights: Support for Workspace-enabled instances. ## 1.6.29 * CLI: include `--only-show-error` option when executing Azure CLI commands. diff --git a/src/Tests/AppInsights.fs b/src/Tests/AppInsights.fs index 75e0db5a1..6fa51462f 100644 --- a/src/Tests/AppInsights.fs +++ b/src/Tests/AppInsights.fs @@ -31,14 +31,13 @@ let tests = testList "AppInsights" [ let deployment = arm { add_resources [ workspace; ai ] } + let json = deployment.Template |> Writer.toJson |> JObject.Parse - let version = json.SelectToken("resources[?(@.name=='ai')].apiVersion").ToString() - let resourceId = json.SelectToken("resources[?(@.name=='ai')].properties.WorkspaceResourceId").ToString() - let dependencies = json.SelectToken("resources[?(@.name=='ai')].dependsOn").Children() |> Seq.map string |> Seq.toArray + let select query = json.SelectToken(query).ToString() - Expect.equal resourceId "[resourceId('Microsoft.OperationalInsights/workspaces', 'la')]" "Incorrect workspace id" - Expect.equal version "2020-02-02" "Incorrect API version" + Expect.equal (select "resources[?(@.name=='ai')].properties.WorkspaceResourceId") "[resourceId('Microsoft.OperationalInsights/workspaces', 'la')]" "Incorrect workspace id" + Expect.equal (select "resources[?(@.name=='ai')].apiVersion") "2020-02-02" "Incorrect API version" Expect.equal ai.InstrumentationKey.Value ("reference(resourceId('Microsoft.Insights/components', 'ai'), '2020-02-02').InstrumentationKey") "Incorrect Instrumentation Key reference" - Expect.sequenceEqual dependencies [ "[resourceId('Microsoft.OperationalInsights/workspaces', 'la')]" ] "Incorrect dependencies" + Expect.sequenceEqual (json.SelectToken("resources[?(@.name=='ai')].dependsOn").Children() |> Seq.map string |> Seq.toArray) [ "[resourceId('Microsoft.OperationalInsights/workspaces', 'la')]" ] "Incorrect dependencies" } ] \ No newline at end of file From 9058d2323578b0429365b687b91ba0228e39da17 Mon Sep 17 00:00:00 2001 From: Jon Roberts Date: Thu, 17 Mar 2022 09:45:09 -0500 Subject: [PATCH 60/73] only allow one spot_instance or prioirity setting in VM --- .../api-overview/resources/virtual-machine.md | 6 +++--- src/Farmer/Builders/Builders.Vm.fs | 16 +++++++++++----- src/Farmer/Common.fs | 5 +++-- src/Tests/VirtualMachine.fs | 16 ++++++++++++++++ 4 files changed, 33 insertions(+), 10 deletions(-) diff --git a/docs/content/api-overview/resources/virtual-machine.md b/docs/content/api-overview/resources/virtual-machine.md index f704ffdbb..b143e837e 100644 --- a/docs/content/api-overview/resources/virtual-machine.md +++ b/docs/content/api-overview/resources/virtual-machine.md @@ -1,6 +1,6 @@ --- title: "Virtual Machine" -date: 2020-02-05T08:53:46+01:00 +date: 2022-03-17T09:33:27+05:00 chapter: false weight: 21 --- @@ -24,8 +24,8 @@ In addition, every VM you create will add a SecureString parameter to the ARM te |diagnostics_support|Turns on diagnostics support using an automatically created created storage account.| |diagnostics_support_external|Turns on diagnostics support using an existing storage account.| |vm_size|Sets the size of the VM.| -|priority|Sets the VM Priority. Overrides `spot_instance`.| -|spot_instance|Makes the VM a spot instance. Overrides `priority`| +|priority|Sets the VM Priority. Only one `spot_instance` or `priority` setting is allowed per VM.| +|spot_instance|Makes the VM a spot instance. Only one `spot_instance` or `priority` setting is allowed per VM.| |username|Sets the admin username of the VM (note: the password is supplied as a securestring parameter to the generated ARM template).| |password_parameter|Sets the name of the parameter which contains the admin password for this VM. defaults to "password-for-"| |operating_system|Sets the operating system of the VM. A set of samples is provided in the `CommonImages` module.| diff --git a/src/Farmer/Builders/Builders.Vm.fs b/src/Farmer/Builders/Builders.Vm.fs index a3450bdf4..09a84f30a 100644 --- a/src/Farmer/Builders/Builders.Vm.fs +++ b/src/Farmer/Builders/Builders.Vm.fs @@ -18,7 +18,7 @@ type VmConfig = { Name : ResourceName DiagnosticsStorageAccount : ResourceRef option - Priority: Priority + Priority: Priority option Username : string option PasswordParameter: string option @@ -69,7 +69,7 @@ type VmConfig = |> Option.map(fun r -> r.resourceId(this).Name) NetworkInterfaceName = this.NicName.Name Size = this.Size - Priority = this.Priority + Priority = this.Priority |> Option.defaultValue Regular Credentials = match this.Username with | Some username -> @@ -192,7 +192,7 @@ type VirtualMachineBuilder() = member _.Yield _ = { Name = ResourceName.Empty DiagnosticsStorageAccount = None - Priority = Regular + Priority = None Size = Basic_A0 Username = None PasswordParameter = None @@ -264,10 +264,16 @@ type VirtualMachineBuilder() = member _.AddDisk(state:VmConfig, size, diskType) = { state with DataDisks = { Size = size; DiskType = diskType } :: state.DataDisks } /// Sets priority of VMm. Overrides spot_instance. [] - member _.Priority(state:VmConfig, priority) = { state with Priority = priority } + member _.Priority(state:VmConfig, priority) = + match state.Priority with + | Some priority -> raiseFarmer $"Priority is already set to {priority}. Only one priority or spot_instance setting per VM is allowed" + | None -> { state with Priority = Some priority } /// Makes VM a spot instance. Overrides priority. [] - member _.Spot(state:VmConfig, (evictionPolicy, maxPrice)) : VmConfig = { state with Priority = Spot (evictionPolicy, maxPrice) } + member _.Spot(state:VmConfig, (evictionPolicy, maxPrice)) : VmConfig = + match state.Priority with + | Some priority -> raiseFarmer $"Priority is already set to {priority}. Only one priority or spot_instance setting per VM is allowed" + | None -> { state with Priority = (evictionPolicy, maxPrice) |> Spot |> Some } member this.Spot(state:VmConfig, evictionPolicy:EvictionPolicy) : VmConfig = this.Spot(state,(evictionPolicy, -1m)) member this.Spot(state:VmConfig, maxPrice) : VmConfig = this.Spot(state,(Deallocate, maxPrice)) /// Adds a SSD data disk to the VM with a specific size. diff --git a/src/Farmer/Common.fs b/src/Farmer/Common.fs index fdff43b25..34e6da7c6 100644 --- a/src/Farmer/Common.fs +++ b/src/Farmer/Common.fs @@ -312,7 +312,7 @@ module Vm = type EvictionPolicy = | Deallocate | Delete - member this.ArmValue = match this with x -> x.ToString() + member this.ArmValue = match this with | Deallocate -> "Deallocate" | Delete -> "Delete" type BillingProfile = { MaxPrice: decimal } type Priority = @@ -321,8 +321,9 @@ module Vm = | Spot of evictionPolicy:EvictionPolicy * maxPrice:decimal member this.ArmValue = match this with + | Low -> "Low" + | Regular -> "Regular" | Spot _ -> "Spot" - | x -> x.ToString() module internal Validation = // ANDs two validation rules diff --git a/src/Tests/VirtualMachine.fs b/src/Tests/VirtualMachine.fs index 644007f8b..fcabbb4ed 100644 --- a/src/Tests/VirtualMachine.fs +++ b/src/Tests/VirtualMachine.fs @@ -240,4 +240,20 @@ let tests = testList "Virtual Machine" [ Expect.equal (string extensionResource.["properties"].["type"]) "AADSSHLoginForLinux" $"Missing or incorrect extension type." Expect.equal (string extensionResource.["properties"].["typeHandlerVersion"]) "1.0" $"Missing or incorrect extension typeHandlerVersion." } + + test "throws an error if you set priority more than once" { + let createVm () = arm { add_resource (vm { name "foo"; username "foo"; priority Regular; priority Regular }) } |> ignore + Expect.throws createVm "priority set more than once" + } + + test "throws an error if you set spot_instance more than once" { + let createVm () = arm { add_resource (vm { name "foo"; username "foo"; spot_instance Deallocate; spot_instance Deallocate }) } |> ignore + Expect.throws createVm "priority and spot_instance both set" + } + + test "throws an error if you specify priority and spot_instance" { + let createVm () = arm { add_resource (vm { name "foo"; username "foo"; priority Regular; spot_instance Deallocate }) } |> ignore + Expect.throws createVm "spot_instance set more than once" + } + ] \ No newline at end of file From cef4e042cd52b25dbdcc4da7294e7d21daf6c195 Mon Sep 17 00:00:00 2001 From: Jon Roberts Date: Thu, 17 Mar 2022 12:30:38 -0500 Subject: [PATCH 61/73] correct some text on tests --- src/Tests/VirtualMachine.fs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Tests/VirtualMachine.fs b/src/Tests/VirtualMachine.fs index fcabbb4ed..6fea32dbd 100644 --- a/src/Tests/VirtualMachine.fs +++ b/src/Tests/VirtualMachine.fs @@ -248,12 +248,12 @@ let tests = testList "Virtual Machine" [ test "throws an error if you set spot_instance more than once" { let createVm () = arm { add_resource (vm { name "foo"; username "foo"; spot_instance Deallocate; spot_instance Deallocate }) } |> ignore - Expect.throws createVm "priority and spot_instance both set" + Expect.throws createVm "spot_instance set more than once" } - test "throws an error if you specify priority and spot_instance" { + test "throws an error if you set priority and spot_instance" { let createVm () = arm { add_resource (vm { name "foo"; username "foo"; priority Regular; spot_instance Deallocate }) } |> ignore - Expect.throws createVm "spot_instance set more than once" + Expect.throws createVm "priority and spot_instance both set" } ] \ No newline at end of file From c9bc1c8dffa375ed037b33822e2162d39a3b1baa Mon Sep 17 00:00:00 2001 From: Jon Roberts Date: Thu, 17 Mar 2022 12:36:53 -0500 Subject: [PATCH 62/73] Add docs about what spot_instance actually does. --- docs/content/api-overview/resources/virtual-machine.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/content/api-overview/resources/virtual-machine.md b/docs/content/api-overview/resources/virtual-machine.md index b143e837e..a84a5a467 100644 --- a/docs/content/api-overview/resources/virtual-machine.md +++ b/docs/content/api-overview/resources/virtual-machine.md @@ -25,7 +25,7 @@ In addition, every VM you create will add a SecureString parameter to the ARM te |diagnostics_support_external|Turns on diagnostics support using an existing storage account.| |vm_size|Sets the size of the VM.| |priority|Sets the VM Priority. Only one `spot_instance` or `priority` setting is allowed per VM.| -|spot_instance|Makes the VM a spot instance. Only one `spot_instance` or `priority` setting is allowed per VM.| +|spot_instance|Makes the VM a spot instance. Shorthand for `priority (Spot (, )`. Only one `spot_instance` or `priority` setting is allowed per VM.| |username|Sets the admin username of the VM (note: the password is supplied as a securestring parameter to the generated ARM template).| |password_parameter|Sets the name of the parameter which contains the admin password for this VM. defaults to "password-for-"| |operating_system|Sets the operating system of the VM. A set of samples is provided in the `CommonImages` module.| From c680a99cf21b05908baaf714ae065860c1791709 Mon Sep 17 00:00:00 2001 From: Jon Roberts Date: Thu, 17 Mar 2022 12:37:38 -0500 Subject: [PATCH 63/73] Update release notes changes to match tag on PR --- RELEASE_NOTES.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 13ea12716..a788f6ecb 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,13 +1,11 @@ Release Notes ============= -## vNext -* VMs: Priority and Spot Instance Settings - ## 1.6.30 * WebApps/Functions: Specify connection string types * WebApps/Functions: Allow adding IP restriction string with CIDR * Application Insights: Support for Workspace-enabled instances. +* VMs: Priority and Spot Instance Settings ## 1.6.29 * CLI: include `--only-show-error` option when executing Azure CLI commands. From bcdf85345718a3467dced1bc38694e47cefb0562 Mon Sep 17 00:00:00 2001 From: Dave Curylo Date: Thu, 17 Mar 2022 17:23:33 -0400 Subject: [PATCH 64/73] 1.6.30 release --- src/Farmer/Farmer.fsproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Farmer/Farmer.fsproj b/src/Farmer/Farmer.fsproj index 48d328499..106f0fb5a 100644 --- a/src/Farmer/Farmer.fsproj +++ b/src/Farmer/Farmer.fsproj @@ -2,7 +2,7 @@ Farmer - 1.6.29 + 1.6.30 Farmer makes authoring ARM templates easy! Copyright 2019-2022 Compositional IT Ltd. Compositional IT From 7917662eadf78ef0c58b985c393f59075415279b Mon Sep 17 00:00:00 2001 From: Richard Sanderson-Pope Date: Fri, 18 Mar 2022 07:57:48 +0000 Subject: [PATCH 65/73] update to resemble to vm builder instead of AKS builder --- src/Farmer/Builders/Builders.WebApp.fs | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/src/Farmer/Builders/Builders.WebApp.fs b/src/Farmer/Builders/Builders.WebApp.fs index 49cdd10f6..70c0ddb25 100644 --- a/src/Farmer/Builders/Builders.WebApp.fs +++ b/src/Farmer/Builders/Builders.WebApp.fs @@ -994,16 +994,26 @@ module Extensions = let ip = IPAddressCidr.parse ip this.Map state (fun x -> { x with IpSecurityRestrictions = IpSecurityRestriction.Create name ip Deny :: x.IpSecurityRestrictions }) /// Integrate this app with a virtual network subnet - [] - member this.RouteViaVNet(state:'T, subnet:SubnetReference option) = + [] + member this.LinkToVNet(state:'T, subnet:SubnetReference option) = match subnet with | Some subnetId -> if subnetId.ResourceId.Type.Type <> Arm.Network.subnets.Type then raiseFarmer $"given resource was not of type '{Arm.Network.subnets.Type}'." | None -> () this.Map state (fun x -> {x with IntegratedSubnet = subnet}) - member this.RouteViaVNet(state:'T, subnetRef) = this.RouteViaVNet (state, Some subnetRef) - member this.RouteViaVNet(state:'T, subnetId:LinkedResource) = this.RouteViaVNet (state, SubnetReference.create subnetId) - member this.RouteViaVNet(state:'T, subnet:SubnetConfig) = this.RouteViaVNet (state, SubnetReference.create subnet) - member this.RouteViaVNet(state:'T, (vnet:VirtualNetworkConfig, subnetName)) = this.RouteViaVNet (state, SubnetReference.create (vnet,subnetName)) - member this.RouteViaVNet(state:'T, (vnetId:LinkedResource, subnetName)) = this.RouteViaVNet (state, SubnetReference.create (vnetId,subnetName)) + member this.LinkToVNet(state:'T, subnetRef) = this.LinkToVNet (state, Some subnetRef) + member this.LinkToVNet(state:'T, subnet:SubnetConfig) = this.LinkToVNet (state, SubnetReference.create subnet) + member this.LinkToVNet(state:'T, (vnet:VirtualNetworkConfig, subnetName)) = this.LinkToVNet (state, SubnetReference.create (vnet,subnetName)) + member this.LinkToVNet(state:'T, (vnetId:ResourceId, subnetName)) = this.LinkToVNet (state, SubnetReference.create (Managed vnetId,subnetName)) + [] + member this.LinkToUnmanagedVNet(state:'T, subnet:SubnetReference option) = + match subnet with + | Some subnetId -> + if subnetId.ResourceId.Type.Type <> Arm.Network.subnets.Type + then raiseFarmer $"given resource was not of type '{Arm.Network.subnets.Type}'." + | None -> () + this.Map state (fun x -> {x with IntegratedSubnet = subnet}) + member this.LinkToUnmanagedVNet(state:'T, subnetRef) = this.LinkToVNet (state, Some subnetRef) + member this.LinkToUnmanagedVNet(state:'T, subnetId:ResourceId) = this.LinkToUnmanagedVNet (state, SubnetReference.create (Unmanaged subnetId)) + member this.LinkToUnmanagedVNet(state:'T, (vnetId:ResourceId, subnetName)) = this.LinkToUnmanagedVNet (state, SubnetReference.create (Unmanaged vnetId,subnetName)) From b012a44ec6f45138aa04e1a82248eecbe025f0e5 Mon Sep 17 00:00:00 2001 From: Richard Sanderson-Pope Date: Fri, 18 Mar 2022 08:47:31 +0000 Subject: [PATCH 66/73] - update docs - add further LinkTo*Vnet overloads - update tests --- .../api-overview/resources/functions.md | 3 ++- .../content/api-overview/resources/web-app.md | 3 ++- src/Farmer/Arm/Network.fs | 6 ++---- src/Farmer/Builders/Builders.WebApp.fs | 19 +++++++------------ src/Tests/Functions.fs | 8 +++----- src/Tests/WebApp.fs | 4 ++-- 6 files changed, 18 insertions(+), 25 deletions(-) diff --git a/docs/content/api-overview/resources/functions.md b/docs/content/api-overview/resources/functions.md index d983e1041..5f038ad03 100644 --- a/docs/content/api-overview/resources/functions.md +++ b/docs/content/api-overview/resources/functions.md @@ -50,7 +50,8 @@ The Functions builder is used to create Azure Functions accounts. It abstracts t | health_check_path | Sets the path to your functions health check endpoint, which Azure load balancers will ping to determine which instances are healthy.| | add_allowed_ip_restriction | Adds an 'allow' rule for an ip | | add_denied_ip_restriction | Adds an 'deny' rule for an ip | -| route_via_vnet | Enable the VNET integration feature in azure where all outbound traffic from the function with be sent via the specified subnet. | +| link_to_vnet | Enable the VNET integration feature in azure where all outbound traffic from the function with be sent via the specified subnet. Use this operator when the given VNET is in the same deployment | +| link_to_unmanaged_vnet | Enable the VNET integration feature in azure where all outbound traffic from the function with be sent via the specified subnet. Use this operator when the given VNET is *not* in the same deployment | #### Post-deployment Builder Keywords The Functions builder contains special commands that are executed *after* the ARM deployment is completed. diff --git a/docs/content/api-overview/resources/web-app.md b/docs/content/api-overview/resources/web-app.md index c3abcdbbf..879e73a45 100644 --- a/docs/content/api-overview/resources/web-app.md +++ b/docs/content/api-overview/resources/web-app.md @@ -60,7 +60,8 @@ The Web App builder is used to create Azure App Service accounts. It abstracts t | Web App | add_allowed_ip_restriction | Adds an 'allow' rule for an ip | | Web App | add_denied_ip_restriction | Adds an 'deny' rule for an ip | | Web App | docker_port | Adds `WEBSITES_PORT` setting to map custom docker port to app service port 80 | -| Web App | route_via_vnet | Enable the VNET integration feature in azure where all outbound traffic from the web app with be sent via the specified subnet. | +| Web App | link_to_vnet | Enable the VNET integration feature in azure where all outbound traffic from the web app with be sent via the specified subnet. Use this operator when the given VNET is in the same deployment | +| Web App | link_to_unmanaged_vnet | Enable the VNET integration feature in azure where all outbound traffic from the web app with be sent via the specified subnet. Use this operator when the given VNET is *not* in the same deployment | | Service Plan | service_plan_name | Sets the name of the service plan. If not set, uses the name of the web app postfixed with "-plan". | | Service Plan | runtime_stack | Sets the runtime stack. | | Service Plan | operating_system | Sets the operating system. If Linux, App Insights configuration settings will be omitted as they are not supported by Azure App Service. | diff --git a/src/Farmer/Arm/Network.fs b/src/Farmer/Arm/Network.fs index b92a83a4a..799ad5d06 100644 --- a/src/Farmer/Arm/Network.fs +++ b/src/Farmer/Arm/Network.fs @@ -32,8 +32,8 @@ type SubnetReference = | Direct subnet -> subnet.ResourceId member this.Dependency = match this with - | ViaManagedVNet (vnetId,_) - | Direct (Managed vnetId) -> Some vnetId + | ViaManagedVNet (id,_) + | Direct (Managed id) -> Some id | _ -> None static member create(vnetRef:LinkedResource, subnetName:ResourceName) = if vnetRef.ResourceId.Type.Type <> virtualNetworks.Type then @@ -48,8 +48,6 @@ type SubnetReference = raiseFarmer $"given resource was not of type '{subnets.Type}'." Direct subnetRef - static member create(subnetId:ResourceId) = () - type PublicIpAddress = { Name : ResourceName Location : Location diff --git a/src/Farmer/Builders/Builders.WebApp.fs b/src/Farmer/Builders/Builders.WebApp.fs index f83f8c90b..7cc46f3ce 100644 --- a/src/Farmer/Builders/Builders.WebApp.fs +++ b/src/Farmer/Builders/Builders.WebApp.fs @@ -1005,17 +1005,12 @@ module Extensions = | None -> () this.Map state (fun x -> {x with IntegratedSubnet = subnet}) member this.LinkToVNet(state:'T, subnetRef) = this.LinkToVNet (state, Some subnetRef) + member this.LinkToVNet(state:'T, subnetId:ResourceId) = this.LinkToVNet (state, SubnetReference.create (Managed subnetId)) + member this.LinkToVNet(state:'T, (vnetId, subnetName):ResourceId*ResourceName) = this.LinkToVNet (state, SubnetReference.create (Managed vnetId,subnetName)) member this.LinkToVNet(state:'T, subnet:SubnetConfig) = this.LinkToVNet (state, SubnetReference.create subnet) - member this.LinkToVNet(state:'T, (vnet:VirtualNetworkConfig, subnetName)) = this.LinkToVNet (state, SubnetReference.create (vnet,subnetName)) - member this.LinkToVNet(state:'T, (vnetId:ResourceId, subnetName)) = this.LinkToVNet (state, SubnetReference.create (Managed vnetId,subnetName)) + member this.LinkToVNet(state:'T, (vnet, subnetName):VirtualNetworkConfig*ResourceName) = this.LinkToVNet (state, SubnetReference.create (vnet,subnetName)) [] - member this.LinkToUnmanagedVNet(state:'T, subnet:SubnetReference option) = - match subnet with - | Some subnetId -> - if subnetId.ResourceId.Type.Type <> Arm.Network.subnets.Type - then raiseFarmer $"given resource was not of type '{Arm.Network.subnets.Type}'." - | None -> () - this.Map state (fun x -> {x with IntegratedSubnet = subnet}) - member this.LinkToUnmanagedVNet(state:'T, subnetRef) = this.LinkToVNet (state, Some subnetRef) - member this.LinkToUnmanagedVNet(state:'T, subnetId:ResourceId) = this.LinkToUnmanagedVNet (state, SubnetReference.create (Unmanaged subnetId)) - member this.LinkToUnmanagedVNet(state:'T, (vnetId:ResourceId, subnetName)) = this.LinkToUnmanagedVNet (state, SubnetReference.create (Unmanaged vnetId,subnetName)) + member this.LinkToUnmanagedVNet(state:'T, subnetId:ResourceId) = this.LinkToVNet (state, SubnetReference.create (Unmanaged subnetId)) + member this.LinkToUnmanagedVNet(state:'T, (vnetId, subnetName):ResourceId*ResourceName) = this.LinkToVNet (state, SubnetReference.create (Unmanaged vnetId,subnetName)) + member this.LinkToUnmanagedVNet(state:'T, subnet:SubnetConfig) = this.LinkToUnmanagedVNet (state, (subnet:>IBuilder).ResourceId) + member this.LinkToUnmanagedVNet(state:'T, (vnet, subnetName):VirtualNetworkConfig*ResourceName) = this.LinkToUnmanagedVNet (state, vnet.SubnetIds[subnetName.Value]) diff --git a/src/Tests/Functions.fs b/src/Tests/Functions.fs index aeb0be6bb..d627e9c72 100644 --- a/src/Tests/Functions.fs +++ b/src/Tests/Functions.fs @@ -416,7 +416,7 @@ let tests = testList "Functions tests" [ } test "Can integrate unmanaged vnet" { let subnetId = Arm.Network.subnets.resourceId (ResourceName "my-vnet", ResourceName "my-subnet") - let wa = functions { name "testApp"; vnet (Unmanaged subnetId) } + let wa = functions { name "testApp"; link_to_unmanaged_vnet subnetId } let resources = wa |> getResources let site = resources |> getResource |> List.head @@ -425,11 +425,10 @@ let tests = testList "Functions tests" [ let vnetConnections = resources |> getResource Expect.hasLength vnetConnections 1 "incorrect number of Vnet connections" - } - + } test "Can integrate managed vnet" { let vnetConfig = vnet { name "my-vnet" } - let wa = functions { name "testApp"; vnet (vnetConfig, ResourceName "my-subnet") } + let wa = functions { name "testApp"; link_to_vnet (vnetConfig, ResourceName "my-subnet") } let resources = wa |> getResources let site = resources |> getResource |> List.head @@ -439,5 +438,4 @@ let tests = testList "Functions tests" [ let vnetConnections = resources |> getResource Expect.hasLength vnetConnections 1 "incorrect number of Vnet connections" } - ] \ No newline at end of file diff --git a/src/Tests/WebApp.fs b/src/Tests/WebApp.fs index 4ee753788..d8a8d9e8f 100644 --- a/src/Tests/WebApp.fs +++ b/src/Tests/WebApp.fs @@ -817,7 +817,7 @@ let tests = testList "Web App Tests" [ } test "Can integrate with unmanaged vnet" { let subnetId = Arm.Network.subnets.resourceId (ResourceName "my-vnet", ResourceName "my-subnet") - let wa = webApp { name "testApp"; sku WebApp.Sku.S1; vnet (Unmanaged subnetId) } + let wa = webApp { name "testApp"; sku WebApp.Sku.S1; link_to_unmanaged_vnet subnetId } let resources = wa |> getResources let site = resources |> getResource |> List.head @@ -830,7 +830,7 @@ let tests = testList "Web App Tests" [ test "Can integrate with managed vnet" { let vnetConfig = vnet { name "my-vnet" } - let wa = webApp { name "testApp"; sku WebApp.Sku.S1; vnet (vnetConfig, ResourceName "my-subnet") } + let wa = webApp { name "testApp"; sku WebApp.Sku.S1; link_to_vnet (vnetConfig, ResourceName "my-subnet") } let resources = wa |> getResources let site = resources |> getResource |> List.head From fd24cebe1e4bf1e7efc0dba83c21d074cbfce440 Mon Sep 17 00:00:00 2001 From: Richard Sanderson-Pope Date: Fri, 18 Mar 2022 09:42:49 +0000 Subject: [PATCH 67/73] Add validation for functions --- src/Farmer/Builders/Builders.Functions.fs | 4 ++- src/Farmer/Builders/Builders.WebApp.fs | 34 +++++++++++++---------- 2 files changed, 23 insertions(+), 15 deletions(-) diff --git a/src/Farmer/Builders/Builders.Functions.fs b/src/Farmer/Builders/Builders.Functions.fs index 1c2382fd1..676081551 100644 --- a/src/Farmer/Builders/Builders.Functions.fs +++ b/src/Farmer/Builders/Builders.Functions.fs @@ -282,7 +282,7 @@ type FunctionsConfig = | DeployableResource this.Name.ResourceName resourceId -> { Name = resourceId.Name Location = location - Sku = Sku.Y1 + Sku = this.CommonWebConfig.Sku WorkerSize = Serverless WorkerCount = 0 MaximumElasticWorkerCount = None @@ -355,6 +355,7 @@ type FunctionsBuilder() = SecretStore = AppService ServicePlan = derived (fun name -> serverFarms.resourceId (name-"farm")) Settings = Map.empty + Sku = Sku.Y1 Slots = Map.empty WorkerProcess = None ZipDeployPath = None @@ -372,6 +373,7 @@ type FunctionsBuilder() = Tags = Map.empty } member _.Run (state:FunctionsConfig) = if state.Name.ResourceName = ResourceName.Empty then raiseFarmer "No Functions instance name has been set." + state.CommonWebConfig.Validate() state /// Do not create an automatic storage account; instead, link to a storage account that is created outside of this Functions instance. [] diff --git a/src/Farmer/Builders/Builders.WebApp.fs b/src/Farmer/Builders/Builders.WebApp.fs index 7cc46f3ce..6562e4a0f 100644 --- a/src/Farmer/Builders/Builders.WebApp.fs +++ b/src/Farmer/Builders/Builders.WebApp.fs @@ -202,6 +202,7 @@ type CommonWebConfig = SecretStore : SecretStore ServicePlan : ResourceRef Settings : Map + Sku : Sku Slots : Map WorkerProcess : Bitness option ZipDeployPath : (string*ZipDeploy.ZipDeploySlot) option @@ -209,6 +210,21 @@ type CommonWebConfig = IpSecurityRestrictions: IpSecurityRestriction list IntegratedSubnet : SubnetReference option PrivateEndpoints: (SubnetReference * string option) Set } + member this.Validate () = + match this with + | { ServicePlan = LinkedResource _ } -> () // can't validate as validation dependent on linked resource + | { IntegratedSubnet = None } -> () // no VNet to validate + | _ -> + match this.Sku with + | Standard _ -> () + | Premium _ | PremiumV2 _| PremiumV3 _ -> () + | ElasticPremium _ -> () + | Isolated _ -> () + | Shared as other -> raiseFarmer $"Sites deployed to service plans with SKU '%A{other}' do not support vnet integration." + | Free as other -> raiseFarmer $"Sites deployed to service plans with SKU '%A{other}' do not support vnet integration." + | Basic _ as other -> raiseFarmer $"Sites deployed to service plans with SKU '%A{other}' do not support vnet integration." + | Dynamic as other -> raiseFarmer $"Sites deployed to service plans with SKU '%A{other}' do not support vnet integration." + type WebAppConfig = { CommonWebConfig: CommonWebConfig @@ -217,7 +233,6 @@ type WebAppConfig = WebSocketsEnabled: bool option Dependencies : ResourceId Set Tags : Map - Sku : Sku WorkerSize : WorkerSize WorkerCount : int MaximumElasticWorkerCount : int option @@ -498,7 +513,7 @@ type WebAppConfig = | DeployableResource this.Name.ResourceName resourceId -> { Name = resourceId.Name Location = location - Sku = this.Sku + Sku = this.CommonWebConfig.Sku WorkerSize = this.WorkerSize WorkerCount = this.WorkerCount MaximumElasticWorkerCount = this.MaximumElasticWorkerCount @@ -613,6 +628,7 @@ type WebAppBuilder() = SecretStore = AppService ServicePlan = derived (fun name -> serverFarms.resourceId (name-"farm")) Settings = Map.empty + Sku = Sku.F1 Slots = Map.empty WorkerProcess = None ZipDeployPath = None @@ -620,7 +636,6 @@ type WebAppBuilder() = IpSecurityRestrictions = [] IntegratedSubnet = None PrivateEndpoints = Set.empty } - Sku = Sku.F1 WorkerSize = Small WorkerCount = 1 MaximumElasticWorkerCount = None @@ -644,16 +659,7 @@ type WebAppBuilder() = ZoneRedundant = None } member _.Run(state:WebAppConfig) = if state.Name.ResourceName = ResourceName.Empty then raiseFarmer "No Web App name has been set." - match state.CommonWebConfig with - | { ServicePlan = LinkedResource _ } -> () // can't validate as validation dependent on linked resource - | { IntegratedSubnet = None } -> () // no VNet to validate - | _ -> - match state.Sku with - | Standard _ -> () - | Premium _ | PremiumV2 _| PremiumV3 _ -> () - | ElasticPremium _ -> () - | Isolated _ -> () - | other -> raiseFarmer $"Web apps with SKU '%A{other}' do not support vnet integration." + state.CommonWebConfig.Validate() { state with SiteExtensions = match state with @@ -679,7 +685,7 @@ type WebAppBuilder() = } [] - member _.Sku(state:WebAppConfig, sku) = { state with Sku = sku } + member _.Sku(state:WebAppConfig, sku) = { state with CommonWebConfig = {state.CommonWebConfig with Sku = sku }} /// Sets the size of the service plan worker. [] member _.WorkerSize(state:WebAppConfig, workerSize) = { state with WorkerSize = workerSize } From 3ac349872f5185708cd4092579a69fdb640887ee Mon Sep 17 00:00:00 2001 From: solere Date: Tue, 22 Mar 2022 00:44:17 +0000 Subject: [PATCH 68/73] AzureFirewall supports AvailabilityZones --- src/Farmer/Arm/AzureFirewall.fs | 2 ++ src/Farmer/Builders/Builders.AzureFirewall.fs | 8 +++++++- src/Tests/JsonRegression.fs | 2 ++ src/Tests/test-data/azure-firewall.json | 6 +++++- 4 files changed, 16 insertions(+), 2 deletions(-) diff --git a/src/Farmer/Arm/AzureFirewall.fs b/src/Farmer/Arm/AzureFirewall.fs index 4f5b8cfb5..464dbba06 100644 --- a/src/Farmer/Arm/AzureFirewall.fs +++ b/src/Farmer/Arm/AzureFirewall.fs @@ -40,6 +40,7 @@ type AzureFirewall = FirewallPolicy : ResourceId option VirtualHub : ResourceId option HubIPAddresses : HubIPAddresses option + AvailabilityZones : string list Sku : Sku } interface IArmResource with member this.ResourceId = azureFirewalls.resourceId this.Name @@ -50,5 +51,6 @@ type AzureFirewall = virtualHub = this.VirtualHub |> Option.map (fun resId -> box {| id = resId.ArmExpression.Eval() |}) |> Option.defaultValue null firewallPolicy = this.FirewallPolicy |> Option.map (fun resId -> box {| id = resId.ArmExpression.Eval() |}) |> Option.defaultValue null hubIPAddresses = this.HubIPAddresses |> Option.map (fun x -> box x.JsonModel) |> Option.defaultValue null |} + zones = if this.AvailabilityZones.IsEmpty then null else this.AvailabilityZones |> box |} diff --git a/src/Farmer/Builders/Builders.AzureFirewall.fs b/src/Farmer/Builders/Builders.AzureFirewall.fs index d3f4b6d9f..7793313d2 100644 --- a/src/Farmer/Builders/Builders.AzureFirewall.fs +++ b/src/Farmer/Builders/Builders.AzureFirewall.fs @@ -22,6 +22,7 @@ type AzureFirewallConfig = VirtualHub : LinkedResource option HubIPAddressSpace : HubIPAddressSpace option Sku : Sku + AvailabilityZones : string list Dependencies : ResourceId Set } interface IBuilder with member this.ResourceId = azureFirewalls.resourceId this.Name @@ -40,7 +41,8 @@ type AzureFirewallConfig = FirewallPolicy = this.FirewallPolicy |> Option.map (fun x -> match x with | Managed resId | Unmanaged resId -> resId) VirtualHub = this.VirtualHub |> Option.map (fun x -> match x with | Managed resId | Unmanaged resId -> resId) HubIPAddresses = this.HubIPAddressSpace |> Option.map (fun x -> x.Arm) - Sku = this.Sku } + Sku = this.Sku + AvailabilityZones = this.AvailabilityZones } ] type AzureFirewallBuilder() = @@ -50,6 +52,7 @@ type AzureFirewallBuilder() = FirewallPolicy = None VirtualHub = None HubIPAddressSpace = None + AvailabilityZones = List.empty Dependencies = Set.empty } /// The name of the firewall. [] member _.Name(state:AzureFirewallConfig, name) = { state with Name = ResourceName name } @@ -82,6 +85,9 @@ type AzureFirewallBuilder() = [] member _.PublicIpReservationCount(state:AzureFirewallConfig, count) = { state with HubIPAddressSpace = Some (HubIPAddressSpace.PublicCount count) } + [] + member _.AvailabilityZones(state:AzureFirewallConfig, zones) = + { state with AvailabilityZones = zones } member _.Run(state:AzureFirewallConfig) = let stateIBuilder = state :> IBuilder match state.Sku.Name with diff --git a/src/Tests/JsonRegression.fs b/src/Tests/JsonRegression.fs index 9dd6e99b1..b14c104b7 100644 --- a/src/Tests/JsonRegression.fs +++ b/src/Tests/JsonRegression.fs @@ -16,6 +16,7 @@ let tests = let path = __SOURCE_DIRECTORY__ + "/test-data/" + jsonFile let expected = File.ReadAllText path let actual = deployment.Template |> Writer.toJson + let filename = Writer.toFile (path + "out") "deployment" actual Expect.equal (actual.Trim()) (expected.Trim()) (sprintf "ARM template generation has changed! Either fix the writer, or update the contents of the generated file (%s)" path) let compareResourcesToJson (resources:IBuilder list) jsonFile = @@ -300,6 +301,7 @@ let tests = sku SkuName.AZFW_Hub SkuTier.Standard public_ip_reservation_count 2 link_to_vhub vhub + availability_zones ["1";"2"] depends_on [(vhub :>IBuilder).ResourceId] } compareResourcesToJson [ firewall; vhub; vwan ] "azure-firewall.json" diff --git a/src/Tests/test-data/azure-firewall.json b/src/Tests/test-data/azure-firewall.json index cbce84e13..d174fc116 100644 --- a/src/Tests/test-data/azure-firewall.json +++ b/src/Tests/test-data/azure-firewall.json @@ -26,7 +26,11 @@ "id": "[resourceId('Microsoft.Network/virtualHubs', 'farmer_vhub')]" } }, - "type": "Microsoft.Network/azureFirewalls" + "type": "Microsoft.Network/azureFirewalls", + "zones": [ + "1", + "2" + ] }, { "apiVersion": "2020-07-01", From e8ae29e60f54dc78b7690ca09d8db01806c304cd Mon Sep 17 00:00:00 2001 From: solere Date: Tue, 22 Mar 2022 00:52:25 +0000 Subject: [PATCH 69/73] Release notes --- RELEASE_NOTES.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index a788f6ecb..74915747d 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,6 +1,8 @@ Release Notes ============= +## 1.6.31 +* AzureFirewall: Supports availability zones ## 1.6.30 * WebApps/Functions: Specify connection string types * WebApps/Functions: Allow adding IP restriction string with CIDR From 5f3ada69bd94dbf2c7afc441eacd3cf94137abff Mon Sep 17 00:00:00 2001 From: Richard Sanderson-Pope Date: Tue, 22 Mar 2022 12:32:24 +0000 Subject: [PATCH 70/73] Fix test failure due to increased validation --- src/Tests/Functions.fs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Tests/Functions.fs b/src/Tests/Functions.fs index d627e9c72..fd04ec33f 100644 --- a/src/Tests/Functions.fs +++ b/src/Tests/Functions.fs @@ -416,7 +416,8 @@ let tests = testList "Functions tests" [ } test "Can integrate unmanaged vnet" { let subnetId = Arm.Network.subnets.resourceId (ResourceName "my-vnet", ResourceName "my-subnet") - let wa = functions { name "testApp"; link_to_unmanaged_vnet subnetId } + let asp = serverFarms.resourceId "my-asp" + let wa = functions { name "testApp"; link_to_unmanaged_service_plan asp; link_to_unmanaged_vnet subnetId } let resources = wa |> getResources let site = resources |> getResource |> List.head @@ -428,7 +429,8 @@ let tests = testList "Functions tests" [ } test "Can integrate managed vnet" { let vnetConfig = vnet { name "my-vnet" } - let wa = functions { name "testApp"; link_to_vnet (vnetConfig, ResourceName "my-subnet") } + let asp = serverFarms.resourceId "my-asp" + let wa = functions { name "testApp"; link_to_unmanaged_service_plan asp; link_to_vnet (vnetConfig, ResourceName "my-subnet") } let resources = wa |> getResources let site = resources |> getResource |> List.head From 5e3458177eebc868f466d21b5a5a362d248eeaab Mon Sep 17 00:00:00 2001 From: Dave Curylo Date: Tue, 22 Mar 2022 08:38:07 -0400 Subject: [PATCH 71/73] Adds JSON selection test for zonal AzFirewall --- src/Tests/AzureFirewall.fs | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/Tests/AzureFirewall.fs b/src/Tests/AzureFirewall.fs index 9bdf44a11..77513aba7 100644 --- a/src/Tests/AzureFirewall.fs +++ b/src/Tests/AzureFirewall.fs @@ -5,6 +5,7 @@ open Expecto open Farmer open Farmer.Arm.AzureFirewall open Farmer.Builders +open Newtonsoft.Json.Linq let tests = testList "Azure Firewall" [ test "Link to builder" { @@ -29,7 +30,34 @@ let tests = testList "Azure Firewall" [ link_to_vhub vhub link_to_firewall_policy existingFwPolicy } + let deployment = arm { add_resource fw } + let jobj = deployment.Template |> Writer.toJson |> JObject.Parse + let zones = jobj.SelectToken "resources[?(@.name=='azfw')].zones" Expect.equal fw.FirewallPolicy.Value.ResourceId (azureFirewallPolicies.resourceId "existing-firewall-policy") "Expected to be linked to existing FW policy" Expect.isEmpty fw.Dependencies "Expected not to depend on any resources since it links to an existing policy" + Expect.isNull zones "Should not have a value for 'zones'" + } + test "Zonal firewall" { + let vwan = vwan { + name "my-vwan" + standard_vwan + } + let vhub = vhub { + name "my-vhub" + address_prefix (IPAddressCidr.parse "100.73.255.0/24") + link_to_vwan vwan + } + let fw = azureFirewall { + name "azfw" + sku AzureFirewall.SkuName.AZFW_Hub AzureFirewall.SkuTier.Standard + public_ip_reservation_count 2 + link_to_vhub vhub + availability_zones ["2"; "3"] + } + let deployment = arm { add_resource fw } + let jobj = deployment.Template |> Writer.toJson |> JObject.Parse + let zones = jobj.SelectToken "resources[?(@.name=='azfw')].zones" :?> JArray + Expect.hasLength zones 2 "Unexpected number of zones" + Expect.containsAll zones [ JValue "2"; JValue "3" ] "Incorrect zones." } ] From 5737fc1d90430edfb7bf076fd512c81cf3359f71 Mon Sep 17 00:00:00 2001 From: solere Date: Mon, 21 Mar 2022 21:54:03 +0000 Subject: [PATCH 72/73] fix resource id for resource group config --- src/Farmer/Builders/Builders.ResourceGroup.fs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Farmer/Builders/Builders.ResourceGroup.fs b/src/Farmer/Builders/Builders.ResourceGroup.fs index 8236f7d2e..a4c00fd98 100644 --- a/src/Farmer/Builders/Builders.ResourceGroup.fs +++ b/src/Farmer/Builders/Builders.ResourceGroup.fs @@ -48,7 +48,7 @@ type ResourceGroupConfig = | None -> $"deployment-{deploymentIndex()}" - member this.ResourceId = {resourceGroupDeployment.resourceId (this.DeploymentName.GetValue this.GenerateDeploymentName) with ResourceGroup = this.TargetResourceGroup} + member this.ResourceId = {resourceGroupDeployment.resourceId (this.DeploymentName.GetValue this.GenerateDeploymentName) with ResourceGroup = this.TargetResourceGroup; Subscription = this.SubscriptionId |> Option.map string} member private this.ContentDeployment = if this.Parameters.IsEmpty && this.Outputs.IsEmpty && this.Resources.IsEmpty then None // this resource group has no content so there's nothing to deploy From a2bce2c85cdf4670c6baaadc6c90b695a8fe0361 Mon Sep 17 00:00:00 2001 From: Nargiz Gasimova <3729486+nargiz@users.noreply.github.com> Date: Thu, 24 Mar 2022 17:06:48 +0000 Subject: [PATCH 73/73] improvements for slot settings --- .../content/api-overview/resources/web-app.md | 4 +- src/Farmer/Builders/Builders.Functions.fs | 11 +- src/Farmer/Builders/Builders.WebApp.fs | 24 +- src/Tests/Functions.fs | 18 +- src/Tests/WebApp.fs | 23 +- .../aks-with-acr.jsonout/deployment.json | 108 +++ .../azure-firewall.jsonout/deployment.json | 67 ++ .../diagnostics.jsonout/deployment.json | 167 ++++ .../event-grid.jsonout/deployment.json | 121 +++ .../load-balancer.jsonout/deployment.json | 153 ++++ .../lots-of-resources.jsonout/deployment.json | 860 ++++++++++++++++++ .../service-bus.jsonout/deployment.json | 100 ++ .../virtual-wan.jsonout/deployment.json | 20 + .../test-data/vm.jsonout/deployment.json | 160 ++++ 14 files changed, 1809 insertions(+), 27 deletions(-) create mode 100644 src/Tests/test-data/aks-with-acr.jsonout/deployment.json create mode 100644 src/Tests/test-data/azure-firewall.jsonout/deployment.json create mode 100644 src/Tests/test-data/diagnostics.jsonout/deployment.json create mode 100644 src/Tests/test-data/event-grid.jsonout/deployment.json create mode 100644 src/Tests/test-data/load-balancer.jsonout/deployment.json create mode 100644 src/Tests/test-data/lots-of-resources.jsonout/deployment.json create mode 100644 src/Tests/test-data/service-bus.jsonout/deployment.json create mode 100644 src/Tests/test-data/virtual-wan.jsonout/deployment.json create mode 100644 src/Tests/test-data/vm.jsonout/deployment.json diff --git a/docs/content/api-overview/resources/web-app.md b/docs/content/api-overview/resources/web-app.md index f73367b70..5f8a60c15 100644 --- a/docs/content/api-overview/resources/web-app.md +++ b/docs/content/api-overview/resources/web-app.md @@ -55,8 +55,8 @@ The Web App builder is used to create Azure App Service accounts. It abstracts t | Web App | add_private_endpoints | Adds private endpoints for this Webapp to the given subnets | | Web App | add_slot | Adds a deployment slot to the app | | Web App | add_slots | Adds multiple deployment slots to the app | -| Web App | slot_setting | Sets a deployment slot setting of the web app in the form "key" "value". | -| Web App | slot_settings | Sets a list of deployment slot setting of the web app as tuples in the form of ("key", "value"). | +| Web App | add_slot_setting | Sets a deployment slot setting of the web app in the form "key" "value". | +| Web App | add_slot_settings | Sets a list of deployment slot setting of the web app as tuples in the form of ("key", "value"). | | Web App | health_check_path | Sets the path to your functions health check endpoint, which Azure load balancers will ping to determine which instances are healthy.| | Web App | custom_domain | Adds a custom domain to the app. By default this will produce an AppService-managed SSL certificate for your domain as well. Through the overloads of this operator, you can provide a custom certificate thumbprint or choose not to use SSL. You can use this operator multiple times to add multiple custom domains. | | Web App | add_allowed_ip_restriction | Adds an 'allow' rule for an ip | diff --git a/src/Farmer/Builders/Builders.Functions.fs b/src/Farmer/Builders/Builders.Functions.fs index 39c9a47fe..54ddd7c6d 100644 --- a/src/Farmer/Builders/Builders.Functions.fs +++ b/src/Farmer/Builders/Builders.Functions.fs @@ -338,11 +338,12 @@ type FunctionsConfig = for (_, slot) in this.CommonWebConfig.Slots |> Map.toSeq do slot.ToSite site - if this.CommonWebConfig.SlotSettingNames <> Set.empty then - { - SiteName = this.Name.ResourceName; - SlotSettingNames = this.CommonWebConfig.SlotSettingNames; - } + match this.CommonWebConfig.SlotSettingNames with + | x when Set.empty <> x -> + { SiteName = this.Name.ResourceName + SlotSettingNames = this.CommonWebConfig.SlotSettingNames } + | _ -> + () ] type FunctionsBuilder() = diff --git a/src/Farmer/Builders/Builders.WebApp.fs b/src/Farmer/Builders/Builders.WebApp.fs index afc8fdc31..2dbe704a5 100644 --- a/src/Farmer/Builders/Builders.WebApp.fs +++ b/src/Farmer/Builders/Builders.WebApp.fs @@ -612,11 +612,12 @@ type WebAppConfig = Dependencies = subnetRef.Dependency |> Option.toList } yield! (PrivateEndpoint.create location this.ResourceId ["sites"] this.CommonWebConfig.PrivateEndpoints) - if this.CommonWebConfig.SlotSettingNames <> Set.empty then - { - SiteName = this.Name.ResourceName; - SlotSettingNames = this.CommonWebConfig.SlotSettingNames; - } + match this.CommonWebConfig.SlotSettingNames with + | x when Set.empty <> x -> + { SiteName = this.Name.ResourceName + SlotSettingNames = this.CommonWebConfig.SlotSettingNames } + | _ -> + () ] type WebAppBuilder() = @@ -1029,14 +1030,19 @@ module Extensions = member this.LinkToUnmanagedVNet(state:'T, subnet:SubnetConfig) = this.LinkToUnmanagedVNet (state, (subnet:>IBuilder).ResourceId) member this.LinkToUnmanagedVNet(state:'T, (vnet, subnetName):VirtualNetworkConfig*ResourceName) = this.LinkToUnmanagedVNet (state, vnet.SubnetIds[subnetName.Value]) /// Adds slot settings - [] + [] member this.AddSlotSetting (state:'T, key, value) = let current = this.Get state - { current with Settings = current.Settings.Add(key, LiteralSetting value); SlotSettingNames =current.SlotSettingNames.Add(key) } + { current with + Settings = current.Settings.Add(key, LiteralSetting value) + SlotSettingNames = current.SlotSettingNames.Add(key) } |> this.Wrap state - [] + [] member this.AddSlotSettings(state:'T, settings: (string*string) list) = let current = this.Get state settings - |> List.fold (fun (state:CommonWebConfig) (key, value: string) -> { state with Settings = state.Settings.Add(key, LiteralSetting value); SlotSettingNames = state.SlotSettingNames.Add(key) }) current + |> List.fold (fun (state:CommonWebConfig) (key, value: string) -> + { state with + Settings = state.Settings.Add(key, LiteralSetting value) + SlotSettingNames = state.SlotSettingNames.Add(key) }) current |> this.Wrap state \ No newline at end of file diff --git a/src/Tests/Functions.fs b/src/Tests/Functions.fs index c52e3d08d..51a97d51a 100644 --- a/src/Tests/Functions.fs +++ b/src/Tests/Functions.fs @@ -442,10 +442,12 @@ let tests = testList "Functions tests" [ } test "Supports slot settings" { - let functionsApp = functions { name "test"; slot_settings [ "sticky_config", "sticky_config_value"; "another_sticky_config", "another_sticky_config_value" ]} + let slot = appSlot { name "warm-up" } + let functionsApp = functions { name "test"; add_slot slot; add_slot_settings [ "sticky_config", "sticky_config_value"; "another_sticky_config", "another_sticky_config_value" ]} let scn = functionsApp |> getResources |> getResource |> List.head let ws = functionsApp |> getResources |> getResource |> List.head + let slots = functionsApp |> getResources|> getResource |> List.filter (fun x -> x.ResourceType = Arm.Web.slots) let template = arm{ add_resource functionsApp} let jobj = template.Template |> Writer.toJson |> Newtonsoft.Json.Linq.JObject.Parse @@ -462,7 +464,10 @@ let tests = testList "Functions tests" [ "sticky_config", LiteralSetting "sticky_config_value" "another_sticky_config", LiteralSetting "another_sticky_config_value" ] - let settings = Expect.wantSome ws.AppSettings "AppSettings should be set" + Expect.isNone ((ws).AppSettings) "Function should not have any settings" + Expect.hasLength slots 1 "Function should have a slot" + + let settings = Expect.wantSome slots.[0].AppSettings "AppSettings should be set on the slot" Expect.containsAll settings expectedSettings "App settings should contain the slot settings" Expect.containsAll scn.SlotSettingNames ["sticky_config"; "another_sticky_config"] "Slot config names should be set" Expect.equal scn.SiteName (ResourceName "test") "Parent name should be set" @@ -472,10 +477,12 @@ let tests = testList "Functions tests" [ } test "Supports slot setting" { - let functionsApp = functions { name "test"; slot_setting "sticky_config" "sticky_config_value" } + let slot = appSlot { name "warm-up" } + let functionsApp = functions { name "test"; add_slot slot; add_slot_setting "sticky_config" "sticky_config_value" } let scn = functionsApp |> getResources |> getResource |> List.head let ws = functionsApp |> getResources |> getResource |> List.head + let slots = functionsApp |> getResources|> getResource |> List.filter (fun x -> x.ResourceType = Arm.Web.slots) let template = arm{ add_resource functionsApp} let jobj = template.Template |> Writer.toJson |> Newtonsoft.Json.Linq.JObject.Parse @@ -490,8 +497,11 @@ let tests = testList "Functions tests" [ let expectedSettings = Map [ "sticky_config", LiteralSetting "sticky_config_value" ] + + Expect.isNone ((ws).AppSettings) "Function should not have any settings" + Expect.hasLength slots 1 "Function should have a slot" - let settings = Expect.wantSome ws.AppSettings "AppSettings should be set" + let settings = Expect.wantSome slots.[0].AppSettings "AppSettings should be set on the slot" Expect.containsAll settings expectedSettings "App settings should contain the slot setting" Expect.containsAll scn.SlotSettingNames ["sticky_config"] "Slot config name should be set" Expect.equal scn.SiteName (ResourceName "test") "Parent name should be set" diff --git a/src/Tests/WebApp.fs b/src/Tests/WebApp.fs index 16d3ef4a9..ee5d04eed 100644 --- a/src/Tests/WebApp.fs +++ b/src/Tests/WebApp.fs @@ -841,10 +841,12 @@ let tests = testList "Web App Tests" [ Expect.hasLength vnetConnections 1 "incorrect number of Vnet connections" } test "Supports slot settings" { - let webApp = webApp { name "test"; slot_settings [ "sticky_config", "sticky_config_value"; "another_sticky_config", "another_sticky_config_value" ]} + let slot = appSlot { name "warm-up" } + let webApp = webApp { name "test"; add_slot slot; add_slot_settings [ "sticky_config", "sticky_config_value"; "another_sticky_config", "another_sticky_config_value" ]} let scn = webApp |> getResources |> getResource |> List.head let ws = webApp |> getResources |> getResource |> List.head + let slots = webApp |> getResources|> getResource |> List.filter (fun x -> x.ResourceType = Arm.Web.slots) let template = arm{ add_resource webApp} let jobj = template.Template |> Writer.toJson |> Newtonsoft.Json.Linq.JObject.Parse @@ -860,8 +862,11 @@ let tests = testList "Web App Tests" [ let expectedSettings = Map [ "sticky_config", LiteralSetting "sticky_config_value" "another_sticky_config", LiteralSetting "another_sticky_config_value" ] - - let settings = Expect.wantSome ws.AppSettings "AppSettings should be set" + + Expect.isNone ((ws).AppSettings) "App service should not have any settings" + Expect.hasLength slots 1 "Should have a slot" + + let settings = Expect.wantSome slots.[0].AppSettings "AppSettings should be set on the slot" Expect.containsAll settings expectedSettings "App settings should contain the slot settings" Expect.containsAll scn.SlotSettingNames ["sticky_config"; "another_sticky_config"] "Slot config names should be set" Expect.equal scn.SiteName (ResourceName "test") "Parent name should be set" @@ -871,10 +876,12 @@ let tests = testList "Web App Tests" [ } test "Supports slot setting" { - let webApp = webApp { name "test"; slot_setting "sticky_config" "sticky_config_value" } + let slot = appSlot { name "warm-up" } + let webApp = webApp { name "test"; add_slot slot; add_slot_setting "sticky_config" "sticky_config_value" } let scn = webApp |> getResources |> getResource |> List.head - let ws = webApp |> getResources |> getResource |> List.head + let ws = webApp |> getResources |> getResource |> List.head + let slots = webApp |> getResources|> getResource |> List.filter (fun x -> x.ResourceType = Arm.Web.slots) let template = arm{ add_resource webApp} let jobj = template.Template |> Writer.toJson |> Newtonsoft.Json.Linq.JObject.Parse @@ -890,12 +897,14 @@ let tests = testList "Web App Tests" [ let expectedSettings = Map [ "sticky_config", LiteralSetting "sticky_config_value" ] - let settings = Expect.wantSome ws.AppSettings "AppSettings should be set" + Expect.isNone ((ws).AppSettings) "App service should not have any settings" + Expect.hasLength slots 1 "App should have a slot" + + let settings = Expect.wantSome slots[0].AppSettings "AppSettings should be set on a slot" Expect.containsAll settings expectedSettings "App settings should contain the slot setting" Expect.containsAll scn.SlotSettingNames ["sticky_config"] "Slot config name should be set" Expect.equal scn.SiteName (ResourceName "test") "Parent name should be set" Expect.containsAll appSettingNames [ "sticky_config" ] "Slot config name should be present in template" Expect.containsAll dependencies [ $"[resourceId('Microsoft.Web/sites', '{webApp.Name.ResourceName.Value}')]"] "Slot config names resource should depend on web site" - } ] diff --git a/src/Tests/test-data/aks-with-acr.jsonout/deployment.json b/src/Tests/test-data/aks-with-acr.jsonout/deployment.json new file mode 100644 index 000000000..6aaa8ab46 --- /dev/null +++ b/src/Tests/test-data/aks-with-acr.jsonout/deployment.json @@ -0,0 +1,108 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "outputs": {}, + "parameters": {}, + "resources": [ + { + "apiVersion": "2018-11-30", + "dependsOn": [], + "location": "eastus", + "name": "kubeletIdentity", + "tags": {}, + "type": "Microsoft.ManagedIdentity/userAssignedIdentities" + }, + { + "apiVersion": "2018-11-30", + "dependsOn": [], + "location": "eastus", + "name": "clusterIdentity", + "tags": {}, + "type": "Microsoft.ManagedIdentity/userAssignedIdentities" + }, + { + "apiVersion": "2019-05-01", + "location": "eastus", + "name": "farmercontainerregistry1234", + "properties": { + "adminUserEnabled": false + }, + "sku": { + "name": "Basic" + }, + "tags": {}, + "type": "Microsoft.ContainerRegistry/registries" + }, + { + "apiVersion": "2021-03-01", + "dependsOn": [ + "[resourceId('Microsoft.ContainerRegistry/registries', 'farmercontainerregistry1234')]", + "[resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', 'clusterIdentity')]", + "[resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', 'kubeletIdentity')]", + "[guid(concat(resourceGroup().id, 'clusterIdentity', 'f1a07417-d97a-45cb-824c-7a7467783830'))]", + "[guid(concat(resourceGroup().id, 'kubeletIdentity', '7f951dda-4ed3-4680-a7ca-43fe172d538d'))]" + ], + "identity": { + "type": "UserAssigned", + "userAssignedIdentities": { + "[resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', 'clusterIdentity')]": {} + } + }, + "location": "eastus", + "name": "aks-cluster", + "properties": { + "agentPoolProfiles": [ + { + "count": 3, + "mode": "System", + "name": "nodepool1", + "osDiskSizeGB": 0, + "osType": "Linux", + "vmSize": "Standard_DS2_v2" + } + ], + "dnsPrefix": "aks-cluster-223d2976", + "enableRBAC": false, + "identityProfile": { + "kubeletIdentity": { + "clientId": "[reference(resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', 'kubeletIdentity'), '2018-11-30').clientId]", + "objectId": "[reference(resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', 'kubeletIdentity'), '2018-11-30').principalId]", + "resourceId": "[resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', 'kubeletIdentity')]" + } + }, + "servicePrincipalProfile": { + "clientId": "msi" + } + }, + "type": "Microsoft.ContainerService/managedClusters" + }, + { + "apiVersion": "2021-04-01-preview", + "dependsOn": [ + "[resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', 'clusterIdentity')]" + ], + "name": "[guid(concat(resourceGroup().id, 'clusterIdentity', 'f1a07417-d97a-45cb-824c-7a7467783830'))]", + "properties": { + "principalId": "[reference(resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', 'clusterIdentity')).principalId]", + "principalType": "ServicePrincipal", + "roleDefinitionId": "[concat('/subscriptions/', subscription().subscriptionId, '/providers/Microsoft.Authorization/roleDefinitions/', 'f1a07417-d97a-45cb-824c-7a7467783830')]" + }, + "type": "Microsoft.Authorization/roleAssignments" + }, + { + "apiVersion": "2021-04-01-preview", + "dependsOn": [ + "[resourceId('Microsoft.ContainerRegistry/registries', 'farmercontainerregistry1234')]", + "[resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', 'kubeletIdentity')]" + ], + "name": "[guid(concat(resourceGroup().id, 'kubeletIdentity', '7f951dda-4ed3-4680-a7ca-43fe172d538d'))]", + "properties": { + "principalId": "[reference(resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', 'kubeletIdentity')).principalId]", + "principalType": "ServicePrincipal", + "roleDefinitionId": "[concat('/subscriptions/', subscription().subscriptionId, '/providers/Microsoft.Authorization/roleDefinitions/', '7f951dda-4ed3-4680-a7ca-43fe172d538d')]" + }, + "scope": "[resourceId('Microsoft.ContainerRegistry/registries', 'farmercontainerregistry1234')]", + "type": "Microsoft.Authorization/roleAssignments" + } + ] +} \ No newline at end of file diff --git a/src/Tests/test-data/azure-firewall.jsonout/deployment.json b/src/Tests/test-data/azure-firewall.jsonout/deployment.json new file mode 100644 index 000000000..d174fc116 --- /dev/null +++ b/src/Tests/test-data/azure-firewall.jsonout/deployment.json @@ -0,0 +1,67 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "outputs": {}, + "parameters": {}, + "resources": [ + { + "apiVersion": "2020-07-01", + "dependsOn": [ + "[resourceId('Microsoft.Network/virtualHubs', 'farmer_vhub')]" + ], + "location": "northeurope", + "name": "farmer_firewall", + "properties": { + "hubIPAddresses": { + "publicIPs": { + "addresses": [], + "count": 2 + } + }, + "sku": { + "name": "AZFW_Hub", + "tier": "Standard" + }, + "virtualHub": { + "id": "[resourceId('Microsoft.Network/virtualHubs', 'farmer_vhub')]" + } + }, + "type": "Microsoft.Network/azureFirewalls", + "zones": [ + "1", + "2" + ] + }, + { + "apiVersion": "2020-07-01", + "dependsOn": [ + "[resourceId('Microsoft.Network/virtualWans', 'farmer-vwan')]" + ], + "location": "northeurope", + "name": "farmer_vhub", + "properties": { + "addressPrefix": "100.73.255.0/24", + "routeTable": { + "routes": [] + }, + "sku": "Standard", + "virtualWan": { + "id": "[resourceId('Microsoft.Network/virtualWans', 'farmer-vwan')]" + } + }, + "type": "Microsoft.Network/virtualHubs" + }, + { + "apiVersion": "2020-07-01", + "location": "northeurope", + "name": "farmer-vwan", + "properties": { + "allowBranchToBranchTraffic": true, + "disableVpnEncryption": true, + "office365LocalBreakoutCategory": "None", + "type": "Standard" + }, + "type": "Microsoft.Network/virtualWans" + } + ] +} \ No newline at end of file diff --git a/src/Tests/test-data/diagnostics.jsonout/deployment.json b/src/Tests/test-data/diagnostics.jsonout/deployment.json new file mode 100644 index 000000000..8b30f5590 --- /dev/null +++ b/src/Tests/test-data/diagnostics.jsonout/deployment.json @@ -0,0 +1,167 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "outputs": {}, + "parameters": {}, + "resources": [ + { + "apiVersion": "2019-06-01", + "dependsOn": [], + "kind": "StorageV2", + "location": "northeurope", + "name": "isaacsuperdata", + "properties": {}, + "sku": { + "name": "Standard_LRS" + }, + "tags": {}, + "type": "Microsoft.Storage/storageAccounts" + }, + { + "apiVersion": "2018-02-01", + "location": "northeurope", + "name": "isaacdiagsuperweb-farm", + "properties": { + "name": "isaacdiagsuperweb-farm", + "perSiteScaling": false, + "reserved": false + }, + "sku": { + "capacity": 1, + "name": "F1", + "size": "0", + "tier": "Free" + }, + "tags": {}, + "type": "Microsoft.Web/serverfarms" + }, + { + "apiVersion": "2020-06-01", + "dependsOn": [ + "[resourceId('Microsoft.Web/sites', 'isaacdiagsuperweb')]" + ], + "location": "northeurope", + "name": "isaacdiagsuperweb/Microsoft.AspNetCore.AzureAppServices.SiteExtension", + "type": "Microsoft.Web/sites/siteextensions" + }, + { + "apiVersion": "2021-03-01", + "dependsOn": [ + "[resourceId('Microsoft.Web/serverfarms', 'isaacdiagsuperweb-farm')]" + ], + "kind": "app", + "location": "northeurope", + "name": "isaacdiagsuperweb", + "properties": { + "httpsOnly": false, + "serverFarmId": "[resourceId('Microsoft.Web/serverfarms', 'isaacdiagsuperweb-farm')]", + "siteConfig": { + "alwaysOn": false, + "appSettings": [], + "connectionStrings": [], + "metadata": [ + { + "name": "CURRENT_STACK", + "value": "dotnetcore" + } + ] + } + }, + "tags": {}, + "type": "Microsoft.Web/sites" + }, + { + "apiVersion": "2017-04-01", + "location": "northeurope", + "name": "isaacsuperhub-ns", + "properties": {}, + "sku": { + "capacity": 1, + "name": "Standard", + "tier": "Standard" + }, + "tags": {}, + "type": "Microsoft.EventHub/namespaces" + }, + { + "apiVersion": "2017-04-01", + "dependsOn": [ + "[resourceId('Microsoft.EventHub/namespaces', 'isaacsuperhub-ns')]" + ], + "location": "northeurope", + "name": "isaacsuperhub-ns/isaacsuperhub", + "properties": { + "partitionCount": 1, + "status": "Active" + }, + "tags": {}, + "type": "Microsoft.EventHub/namespaces/eventhubs" + }, + { + "apiVersion": "2017-04-01", + "dependsOn": [ + "[resourceId('Microsoft.EventHub/namespaces', 'isaacsuperhub-ns')]", + "[resourceId('Microsoft.EventHub/namespaces/eventhubs', 'isaacsuperhub-ns', 'isaacsuperhub')]" + ], + "location": "northeurope", + "name": "isaacsuperhub-ns/isaacsuperhub/$Default", + "type": "Microsoft.EventHub/namespaces/eventhubs/consumergroups" + }, + { + "apiVersion": "2020-03-01-preview", + "location": "northeurope", + "name": "isaacsuperlogs", + "properties": { + "sku": { + "name": "PerGB2018" + } + }, + "tags": {}, + "type": "Microsoft.OperationalInsights/workspaces" + }, + { + "apiVersion": "2017-05-01-preview", + "dependsOn": [ + "[resourceId('Microsoft.EventHub/namespaces', 'isaacsuperhub-ns')]", + "[resourceId('Microsoft.OperationalInsights/workspaces', 'isaacsuperlogs')]", + "[resourceId('Microsoft.Storage/storageAccounts', 'isaacsuperdata')]", + "[resourceId('Microsoft.Web/sites', 'isaacdiagsuperweb')]" + ], + "location": "northeurope", + "name": "isaacdiagsuperweb/Microsoft.Insights/myDiagnosticSetting", + "properties": { + "LogAnalyticsDestinationType": "Dedicated", + "eventHubAuthorizationRuleId": "[resourceId('Microsoft.EventHub/namespaces/AuthorizationRules', 'isaacsuperhub-ns', 'RootManageSharedAccessKey')]", + "eventHubName": "isaacsuperhub", + "logs": [ + { + "category": "AppServiceAntivirusScanAuditLogs", + "enabled": true + }, + { + "category": "AppServiceAppLogs", + "enabled": true + }, + { + "category": "AppServiceHTTPLogs", + "enabled": true + }, + { + "category": "AppServicePlatformLogs", + "enabled": true + } + ], + "metrics": [ + { + "category": "AllMetrics", + "enabled": true + } + ], + "storageAccountId": "[resourceId('Microsoft.Storage/storageAccounts', 'isaacsuperdata')]", + "workspaceId": "[resourceId('Microsoft.OperationalInsights/workspaces', 'isaacsuperlogs')]" + }, + "tags": {}, + "type": "Microsoft.Web/sites/providers/diagnosticSettings" + } + ] +} \ No newline at end of file diff --git a/src/Tests/test-data/event-grid.jsonout/deployment.json b/src/Tests/test-data/event-grid.jsonout/deployment.json new file mode 100644 index 000000000..831c9af5a --- /dev/null +++ b/src/Tests/test-data/event-grid.jsonout/deployment.json @@ -0,0 +1,121 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "outputs": {}, + "parameters": {}, + "resources": [ + { + "apiVersion": "2019-06-01", + "dependsOn": [], + "kind": "StorageV2", + "location": "northeurope", + "name": "isaacgriddevprac", + "properties": {}, + "sku": { + "name": "Standard_LRS" + }, + "tags": {}, + "type": "Microsoft.Storage/storageAccounts" + }, + { + "apiVersion": "2018-03-01-preview", + "dependsOn": [ + "[resourceId('Microsoft.Storage/storageAccounts', 'isaacgriddevprac')]" + ], + "name": "isaacgriddevprac/default/data", + "properties": { + "publicAccess": "None" + }, + "type": "Microsoft.Storage/storageAccounts/blobServices/containers" + }, + { + "apiVersion": "2019-06-01", + "dependsOn": [ + "[resourceId('Microsoft.Storage/storageAccounts', 'isaacgriddevprac')]" + ], + "name": "isaacgriddevprac/default/todo", + "type": "Microsoft.Storage/storageAccounts/queueServices/queues" + }, + { + "apiVersion": "2017-04-01", + "dependsOn": [], + "location": "northeurope", + "name": "farmereventpubservicebusns", + "sku": { + "name": "Basic", + "tier": "Basic" + }, + "tags": {}, + "type": "Microsoft.ServiceBus/namespaces" + }, + { + "apiVersion": "2017-04-01", + "dependsOn": [ + "[resourceId('Microsoft.ServiceBus/namespaces', 'farmereventpubservicebusns')]" + ], + "name": "farmereventpubservicebusns/events", + "properties": {}, + "type": "Microsoft.ServiceBus/namespaces/queues" + }, + { + "apiVersion": "2020-04-01-preview", + "dependsOn": [ + "[resourceId('Microsoft.Storage/storageAccounts', 'isaacgriddevprac')]" + ], + "location": "northeurope", + "name": "newblobscreated", + "properties": { + "source": "[resourceId('Microsoft.Storage/storageAccounts', 'isaacgriddevprac')]", + "topicType": "Microsoft.Storage.StorageAccounts" + }, + "tags": {}, + "type": "Microsoft.EventGrid/systemTopics" + }, + { + "apiVersion": "2020-04-01-preview", + "dependsOn": [ + "[resourceId('Microsoft.EventGrid/systemTopics', 'newblobscreated')]", + "[resourceId('Microsoft.ServiceBus/namespaces/queues', 'farmereventpubservicebusns', 'events')]" + ], + "name": "newblobscreated/events-farmereventpubservicebusns-servicebus-queue", + "properties": { + "destination": { + "endpointType": "ServiceBusQueue", + "properties": { + "queueName": "events", + "resourceId": "[resourceId('Microsoft.ServiceBus/namespaces/queues', 'farmereventpubservicebusns', 'events')]" + } + }, + "filter": { + "includedEventTypes": [ + "Microsoft.Storage.BlobCreated" + ] + } + }, + "type": "Microsoft.EventGrid/systemTopics/eventSubscriptions" + }, + { + "apiVersion": "2020-04-01-preview", + "dependsOn": [ + "[resourceId('Microsoft.EventGrid/systemTopics', 'newblobscreated')]", + "[resourceId('Microsoft.Storage/storageAccounts/queueServices/queues', 'isaacgriddevprac', 'default', 'todo')]" + ], + "name": "newblobscreated/isaacgriddevprac-todo-queue", + "properties": { + "destination": { + "endpointType": "StorageQueue", + "properties": { + "queueName": "todo", + "resourceId": "[resourceId('Microsoft.Storage/storageAccounts', 'isaacgriddevprac')]" + } + }, + "filter": { + "includedEventTypes": [ + "Microsoft.Storage.BlobCreated" + ] + } + }, + "type": "Microsoft.EventGrid/systemTopics/eventSubscriptions" + } + ] +} \ No newline at end of file diff --git a/src/Tests/test-data/load-balancer.jsonout/deployment.json b/src/Tests/test-data/load-balancer.jsonout/deployment.json new file mode 100644 index 000000000..af253d636 --- /dev/null +++ b/src/Tests/test-data/load-balancer.jsonout/deployment.json @@ -0,0 +1,153 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "outputs": {}, + "parameters": {}, + "resources": [ + { + "apiVersion": "2020-07-01", + "dependsOn": [], + "location": "northeurope", + "name": "my-vnet", + "properties": { + "addressSpace": { + "addressPrefixes": [ + "10.0.1.0/24" + ] + }, + "subnets": [ + { + "name": "my-services", + "properties": { + "addressPrefix": "10.0.1.0/24", + "delegations": [ + { + "name": "Microsoft.ContainerInstance/containerGroups", + "properties": { + "serviceName": "Microsoft.ContainerInstance/containerGroups" + } + } + ] + } + } + ] + }, + "tags": {}, + "type": "Microsoft.Network/virtualNetworks" + }, + { + "apiVersion": "2020-11-01", + "dependsOn": [ + "[resourceId('Microsoft.Network/publicIPAddresses', 'lb-pip')]" + ], + "location": "northeurope", + "name": "lb", + "properties": { + "backendAddressPools": [ + { + "name": "lb-backend" + } + ], + "frontendIpConfigurations": [ + { + "name": "lb-frontend", + "properties": { + "privateIPAllocationMethod": "Dynamic", + "publicIPAddress": { + "id": "[resourceId('Microsoft.Network/publicIPAddresses', 'lb-pip')]" + } + } + } + ], + "inboundNatPools": [], + "inboundNatRules": [], + "loadBalancingRules": [ + { + "name": "rule1", + "properties": { + "backendAddressPool": { + "id": "[resourceId('Microsoft.Network/loadBalancers/backendAddressPools', 'lb', 'lb-backend')]" + }, + "backendPort": 8080, + "disableOutboundSnat": true, + "enableTcpReset": false, + "frontendIPConfiguration": { + "id": "[resourceId('Microsoft.Network/loadBalancers/frontendIPConfigurations', 'lb', 'lb-frontend')]" + }, + "frontendPort": 80, + "idleTimeoutInMinutes": 4, + "loadDistribution": "Default", + "probe": { + "id": "[resourceId('Microsoft.Network/loadBalancers/probes', 'lb', 'httpGet')]" + }, + "protocol": "Tcp" + } + } + ], + "outboundNatRules": [], + "probes": [ + { + "name": "httpGet", + "properties": { + "intervalInSeconds": 15, + "numberOfProbes": 2, + "port": 8080, + "protocol": "Http", + "requestPath": "/" + } + } + ] + }, + "sku": { + "name": "Standard", + "tier": "Regional" + }, + "tags": {}, + "type": "Microsoft.Network/loadBalancers" + }, + { + "apiVersion": "2020-11-01", + "dependsOn": [ + "[resourceId('Microsoft.Network/loadBalancers', 'lb')]", + "[resourceId('Microsoft.Network/virtualNetworks', 'my-vnet')]" + ], + "name": "lb/lb-backend", + "properties": { + "loadBalancerBackendAddresses": [ + { + "name": "addr0", + "properties": { + "ipAddress": "10.0.1.4", + "virtualNetwork": { + "id": "[resourceId('Microsoft.Network/virtualNetworks', 'my-vnet')]" + } + } + }, + { + "name": "addr1", + "properties": { + "ipAddress": "10.0.1.5", + "virtualNetwork": { + "id": "[resourceId('Microsoft.Network/virtualNetworks', 'my-vnet')]" + } + } + } + ] + }, + "type": "Microsoft.Network/loadBalancers/backendAddressPools" + }, + { + "apiVersion": "2018-11-01", + "location": "northeurope", + "name": "lb-pip", + "properties": { + "publicIPAllocationMethod": "Static" + }, + "sku": { + "name": "Standard" + }, + "tags": {}, + "type": "Microsoft.Network/publicIPAddresses" + } + ] +} \ No newline at end of file diff --git a/src/Tests/test-data/lots-of-resources.jsonout/deployment.json b/src/Tests/test-data/lots-of-resources.jsonout/deployment.json new file mode 100644 index 000000000..dfff0f497 --- /dev/null +++ b/src/Tests/test-data/lots-of-resources.jsonout/deployment.json @@ -0,0 +1,860 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "outputs": {}, + "parameters": { + "password-for-farmersql1979": { + "type": "securestring" + }, + "password-for-farmervm": { + "type": "securestring" + } + }, + "resources": [ + { + "apiVersion": "2019-06-01-preview", + "location": "northeurope", + "name": "farmersql1979", + "properties": { + "administratorLogin": "farmersqladmin", + "administratorLoginPassword": "[parameters('password-for-farmersql1979')]", + "version": "12.0" + }, + "tags": { + "displayName": "farmersql1979" + }, + "type": "Microsoft.Sql/servers" + }, + { + "apiVersion": "2019-06-01-preview", + "dependsOn": [ + "[resourceId('Microsoft.Sql/servers', 'farmersql1979')]", + "[resourceId('Microsoft.Sql/servers/elasticPools', 'farmersql1979', 'farmersql1979-pool')]" + ], + "location": "northeurope", + "name": "farmersql1979/farmertestdb", + "properties": { + "collation": "SQL_Latin1_General_CP1_CI_AS", + "elasticPoolId": "[resourceId('Microsoft.Sql/servers/elasticPools', 'farmersql1979', 'farmersql1979-pool')]" + }, + "tags": { + "displayName": "farmertestdb" + }, + "type": "Microsoft.Sql/servers/databases" + }, + { + "apiVersion": "2014-04-01-preview", + "comments": "Transparent Data Encryption", + "dependsOn": [ + "[resourceId('Microsoft.Sql/servers/databases', 'farmersql1979', 'farmertestdb')]" + ], + "name": "farmersql1979/farmertestdb/current", + "properties": { + "status": "Enabled" + }, + "type": "Microsoft.Sql/servers/databases/transparentDataEncryption" + }, + { + "apiVersion": "2014-04-01", + "dependsOn": [ + "[resourceId('Microsoft.Sql/servers', 'farmersql1979')]" + ], + "location": "northeurope", + "name": "farmersql1979/allow-azure-services", + "properties": { + "endIpAddress": "0.0.0.0", + "startIpAddress": "0.0.0.0" + }, + "type": "Microsoft.Sql/servers/firewallrules" + }, + { + "apiVersion": "2017-10-01-preview", + "dependsOn": [ + "[resourceId('Microsoft.Sql/servers', 'farmersql1979')]" + ], + "location": "northeurope", + "name": "farmersql1979/farmersql1979-pool", + "properties": {}, + "sku": { + "name": "BasicPool", + "size": "50", + "tier": "Basic" + }, + "type": "Microsoft.Sql/servers/elasticPools" + }, + { + "apiVersion": "2019-06-01", + "dependsOn": [], + "kind": "StorageV2", + "location": "northeurope", + "name": "farmerstorage1979", + "properties": {}, + "sku": { + "name": "Standard_LRS" + }, + "tags": {}, + "type": "Microsoft.Storage/storageAccounts" + }, + { + "apiVersion": "2014-04-01", + "dependsOn": [], + "kind": "web", + "location": "northeurope", + "name": "farmerwebapp1979-ai", + "properties": { + "ApplicationId": "farmerwebapp1979", + "Application_Type": "web", + "DisableIpMasking": false, + "SamplingPercentage": 100, + "name": "farmerwebapp1979-ai" + }, + "tags": { + "[concat('hidden-link:', resourceGroup().id, '/providers/Microsoft.Web/sites/', 'farmerwebapp1979')]": "Resource" + }, + "type": "Microsoft.Insights/components" + }, + { + "apiVersion": "2018-02-01", + "location": "northeurope", + "name": "farmerwebapp1979-farm", + "properties": { + "name": "farmerwebapp1979-farm", + "perSiteScaling": false, + "reserved": false + }, + "sku": { + "capacity": 1, + "name": "F1", + "size": "0", + "tier": "Free" + }, + "tags": {}, + "type": "Microsoft.Web/serverfarms" + }, + { + "apiVersion": "2020-06-01", + "dependsOn": [ + "[resourceId('Microsoft.Web/sites', 'farmerwebapp1979')]" + ], + "location": "northeurope", + "name": "farmerwebapp1979/Microsoft.AspNetCore.AzureAppServices.SiteExtension", + "type": "Microsoft.Web/sites/siteextensions" + }, + { + "apiVersion": "2021-03-01", + "dependsOn": [ + "[resourceId('Microsoft.Insights/components', 'farmerwebapp1979-ai')]", + "[resourceId('Microsoft.Web/serverfarms', 'farmerwebapp1979-farm')]" + ], + "kind": "app", + "location": "northeurope", + "name": "farmerwebapp1979", + "properties": { + "httpsOnly": false, + "serverFarmId": "[resourceId('Microsoft.Web/serverfarms', 'farmerwebapp1979-farm')]", + "siteConfig": { + "alwaysOn": false, + "appSettings": [ + { + "name": "APPINSIGHTS_INSTRUMENTATIONKEY", + "value": "[reference(resourceId('Microsoft.Insights/components', 'farmerwebapp1979-ai'), '2014-04-01').InstrumentationKey]" + }, + { + "name": "APPINSIGHTS_PROFILERFEATURE_VERSION", + "value": "1.0.0" + }, + { + "name": "APPINSIGHTS_SNAPSHOTFEATURE_VERSION", + "value": "1.0.0" + }, + { + "name": "ApplicationInsightsAgent_EXTENSION_VERSION", + "value": "~2" + }, + { + "name": "DiagnosticServices_EXTENSION_VERSION", + "value": "~3" + }, + { + "name": "InstrumentationEngine_EXTENSION_VERSION", + "value": "~1" + }, + { + "name": "SnapshotDebugger_EXTENSION_VERSION", + "value": "~1" + }, + { + "name": "XDT_MicrosoftApplicationInsights_BaseExtensions", + "value": "~1" + }, + { + "name": "XDT_MicrosoftApplicationInsights_Mode", + "value": "recommended" + } + ], + "connectionStrings": [], + "metadata": [ + { + "name": "CURRENT_STACK", + "value": "dotnetcore" + } + ] + } + }, + "tags": {}, + "type": "Microsoft.Web/sites" + }, + { + "apiVersion": "2018-02-01", + "location": "northeurope", + "name": "farmerfuncs1979-farm", + "properties": { + "name": "farmerfuncs1979-farm", + "perSiteScaling": false, + "reserved": false + }, + "sku": { + "capacity": 0, + "name": "Y1", + "size": "Y1", + "tier": "Dynamic" + }, + "tags": {}, + "type": "Microsoft.Web/serverfarms" + }, + { + "apiVersion": "2019-06-01", + "dependsOn": [], + "kind": "StorageV2", + "location": "northeurope", + "name": "farmerfuncs1979storage", + "properties": {}, + "sku": { + "name": "Standard_LRS" + }, + "tags": {}, + "type": "Microsoft.Storage/storageAccounts" + }, + { + "apiVersion": "2014-04-01", + "dependsOn": [], + "kind": "web", + "location": "northeurope", + "name": "farmerfuncs1979-ai", + "properties": { + "ApplicationId": "farmerfuncs1979", + "Application_Type": "web", + "DisableIpMasking": false, + "SamplingPercentage": 100, + "name": "farmerfuncs1979-ai" + }, + "tags": { + "[concat('hidden-link:', resourceGroup().id, '/providers/Microsoft.Web/sites/', 'farmerfuncs1979')]": "Resource" + }, + "type": "Microsoft.Insights/components" + }, + { + "apiVersion": "2021-03-01", + "dependsOn": [ + "[resourceId('Microsoft.Insights/components', 'farmerfuncs1979-ai')]", + "[resourceId('Microsoft.Storage/storageAccounts', 'farmerfuncs1979storage')]", + "[resourceId('Microsoft.Web/serverfarms', 'farmerfuncs1979-farm')]" + ], + "kind": "functionapp", + "location": "northeurope", + "name": "farmerfuncs1979", + "properties": { + "httpsOnly": false, + "serverFarmId": "[resourceId('Microsoft.Web/serverfarms', 'farmerfuncs1979-farm')]", + "siteConfig": { + "alwaysOn": false, + "appSettings": [ + { + "name": "APPINSIGHTS_INSTRUMENTATIONKEY", + "value": "[reference(resourceId('Microsoft.Insights/components', 'farmerfuncs1979-ai'), '2014-04-01').InstrumentationKey]" + }, + { + "name": "AzureWebJobsDashboard", + "value": "[concat('DefaultEndpointsProtocol=https;AccountName=farmerfuncs1979storage;AccountKey=', listKeys(resourceId('Microsoft.Storage/storageAccounts', 'farmerfuncs1979storage'), '2017-10-01').keys[0].value)]" + }, + { + "name": "AzureWebJobsStorage", + "value": "[concat('DefaultEndpointsProtocol=https;AccountName=farmerfuncs1979storage;AccountKey=', listKeys(resourceId('Microsoft.Storage/storageAccounts', 'farmerfuncs1979storage'), '2017-10-01').keys[0].value)]" + }, + { + "name": "FUNCTIONS_EXTENSION_VERSION", + "value": "~3" + }, + { + "name": "FUNCTIONS_WORKER_RUNTIME", + "value": "dotnet" + }, + { + "name": "WEBSITE_CONTENTAZUREFILECONNECTIONSTRING", + "value": "[concat('DefaultEndpointsProtocol=https;AccountName=farmerfuncs1979storage;AccountKey=', listKeys(resourceId('Microsoft.Storage/storageAccounts', 'farmerfuncs1979storage'), '2017-10-01').keys[0].value)]" + }, + { + "name": "WEBSITE_CONTENTSHARE", + "value": "farmerfuncs1979" + }, + { + "name": "WEBSITE_NODE_DEFAULT_VERSION", + "value": "10.14.1" + } + ], + "connectionStrings": [], + "metadata": [] + } + }, + "tags": {}, + "type": "Microsoft.Web/sites" + }, + { + "apiVersion": "2017-04-01", + "dependsOn": [], + "location": "northeurope", + "name": "farmerbus1979", + "sku": { + "name": "Standard", + "tier": "Standard" + }, + "tags": {}, + "type": "Microsoft.ServiceBus/namespaces" + }, + { + "apiVersion": "2017-04-01", + "dependsOn": [ + "[resourceId('Microsoft.ServiceBus/namespaces', 'farmerbus1979')]" + ], + "name": "farmerbus1979/queue1", + "properties": {}, + "type": "Microsoft.ServiceBus/namespaces/queues" + }, + { + "apiVersion": "2017-04-01", + "dependsOn": [ + "[resourceId('Microsoft.ServiceBus/namespaces', 'farmerbus1979')]" + ], + "name": "farmerbus1979/topic1", + "properties": {}, + "type": "Microsoft.ServiceBus/namespaces/topics" + }, + { + "apiVersion": "2017-04-01", + "dependsOn": [ + "[resourceId('Microsoft.ServiceBus/namespaces/topics', 'farmerbus1979', 'topic1')]" + ], + "name": "farmerbus1979/topic1/sub1", + "properties": {}, + "resources": [], + "type": "Microsoft.ServiceBus/namespaces/topics/subscriptions" + }, + { + "apiVersion": "2019-04-15", + "location": "global", + "name": "farmercdn1979", + "properties": {}, + "sku": { + "name": "Standard_Akamai" + }, + "tags": {}, + "type": "Microsoft.Cdn/profiles" + }, + { + "apiVersion": "2019-04-15", + "dependsOn": [ + "[resourceId('Microsoft.Cdn/profiles', 'farmercdn1979')]", + "[resourceId('Microsoft.Storage/storageAccounts', 'farmerstorage1979')]" + ], + "location": "global", + "name": "farmercdn1979/farmercdnendpoint1979", + "properties": { + "contentTypesToCompress": [], + "deliveryPolicy": { + "description": "", + "rules": [ + { + "actions": [ + { + "name": "UrlRewrite", + "parameters": { + "@odata.type": "#Microsoft.Azure.Cdn.Models.DeliveryRuleUrlRewriteActionParameters", + "destination": "/destination", + "preserveUnmatchedPath": true, + "sourcePattern": "/pattern" + } + } + ], + "conditions": [ + { + "name": "IsDevice", + "parameters": { + "@odata.type": "#Microsoft.Azure.Cdn.Models.DeliveryRuleIsDeviceConditionParameters", + "matchValues": [ + "Mobile" + ], + "negateCondition": false, + "operator": "Equal", + "selector": "", + "transforms": [] + } + } + ], + "name": "farmerrule1979", + "order": 1 + } + ] + }, + "isCompressionEnabled": false, + "isHttpAllowed": true, + "isHttpsAllowed": true, + "optimizationType": "GeneralWebDelivery", + "originHostHeader": "[replace(replace(reference(resourceId('Microsoft.Storage/storageAccounts', 'farmerstorage1979'), '2019-06-01').primaryEndpoints.web, 'https://', ''), '/', '')]", + "origins": [ + { + "name": "origin", + "properties": { + "hostName": "[replace(replace(reference(resourceId('Microsoft.Storage/storageAccounts', 'farmerstorage1979'), '2019-06-01').primaryEndpoints.web, 'https://', ''), '/', '')]" + } + } + ], + "queryStringCachingBehavior": "UseQueryString" + }, + "tags": {}, + "type": "Microsoft.Cdn/profiles/endpoints" + }, + { + "apiVersion": "2019-12-01", + "dependsOn": [], + "identity": { + "type": "None" + }, + "location": "northeurope", + "name": "farmeraci1979", + "properties": { + "containers": [ + { + "name": "webserver", + "properties": { + "command": [], + "environmentVariables": [], + "image": "nginx:latest", + "ports": [ + { + "port": 80 + } + ], + "resources": { + "requests": { + "cpu": 1, + "memoryInGB": 1.5 + } + }, + "volumeMounts": [ + { + "mountPath": "/src/farmer", + "name": "source-code" + } + ] + } + } + ], + "imageRegistryCredentials": [], + "initContainers": [], + "ipAddress": { + "ports": [ + { + "port": 80, + "protocol": "TCP" + } + ], + "type": "Public" + }, + "osType": "Linux", + "restartPolicy": "Always", + "volumes": [ + { + "gitRepo": { + "repository": "https://github.com/CompositionalIT/farmer" + }, + "name": "source-code" + } + ] + }, + "tags": {}, + "type": "Microsoft.ContainerInstance/containerGroups" + }, + { + "apiVersion": "2020-08-20-preview", + "location": "northeurope", + "name": "test", + "properties": { + "dataLocation": "Australia" + }, + "tags": { + "a": "b" + }, + "type": "Microsoft.Communication/communicationServices" + }, + { + "apiVersion": "2020-10-01", + "dependsOn": [], + "name": "nested-resources", + "properties": { + "expressionEvaluationOptions": { + "scope": "Inner" + }, + "mode": "Incremental", + "parameters": { + "password-for-farmervm": { + "value": "[parameters('password-for-farmervm')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "outputs": {}, + "parameters": { + "password-for-farmervm": { + "type": "securestring" + } + }, + "resources": [ + { + "apiVersion": "2021-04-15", + "kind": "GlobalDocumentDB", + "location": "uksouth", + "name": "testaccount", + "properties": { + "consistencyPolicy": { + "defaultConsistencyLevel": "BoundedStaleness", + "maxIntervalInSeconds": 1000, + "maxStalenessPrefix": 500 + }, + "databaseAccountOfferType": "Standard", + "enableFreeTier": false, + "publicNetworkAccess": "Enabled" + }, + "tags": {}, + "type": "Microsoft.DocumentDb/databaseAccounts" + }, + { + "apiVersion": "2021-04-15", + "dependsOn": [ + "[resourceId('Microsoft.DocumentDb/databaseAccounts', 'testaccount')]" + ], + "name": "testaccount/testdb", + "properties": { + "options": { + "throughput": "400" + }, + "resource": { + "id": "testdb" + } + }, + "type": "Microsoft.DocumentDb/databaseAccounts/sqlDatabases" + }, + { + "apiVersion": "2021-04-15", + "dependsOn": [ + "[resourceId('Microsoft.DocumentDb/databaseAccounts/sqlDatabases', 'testaccount', 'testdb')]" + ], + "name": "testaccount/testdb/myContainer", + "properties": { + "resource": { + "id": "myContainer", + "indexingPolicy": { + "excludedPaths": [ + { + "path": "/excluded/*" + } + ], + "includedPaths": [ + { + "indexes": [ + { + "dataType": "number", + "kind": "Hash", + "precision": -1 + } + ], + "path": "/path" + } + ], + "indexingMode": "consistent" + }, + "partitionKey": { + "kind": "Hash", + "paths": [ + "/id" + ] + }, + "uniqueKeyPolicy": { + "uniqueKeys": [] + } + } + }, + "type": "Microsoft.DocumentDb/databaseAccounts/sqlDatabases/containers" + }, + { + "apiVersion": "2021-04-15", + "kind": "MongoDB", + "location": "uksouth", + "name": "testaccountmongo", + "properties": { + "consistencyPolicy": { + "defaultConsistencyLevel": "BoundedStaleness", + "maxIntervalInSeconds": 1000, + "maxStalenessPrefix": 500 + }, + "databaseAccountOfferType": "Standard", + "enableFreeTier": false, + "publicNetworkAccess": "Enabled" + }, + "tags": {}, + "type": "Microsoft.DocumentDb/databaseAccounts" + }, + { + "apiVersion": "2021-04-15", + "dependsOn": [ + "[resourceId('Microsoft.DocumentDb/databaseAccounts', 'testaccountmongo')]" + ], + "name": "testaccountmongo/testdbmongo", + "properties": { + "options": { + "throughput": "400" + }, + "resource": { + "id": "testdbmongo" + } + }, + "type": "Microsoft.DocumentDb/databaseAccounts/mongodbDatabases" + }, + { + "apiVersion": "2019-03-01", + "dependsOn": [ + "[resourceId('Microsoft.Network/networkInterfaces', 'farmervm-nic')]" + ], + "location": "uksouth", + "name": "farmervm", + "properties": { + "diagnosticsProfile": { + "bootDiagnostics": { + "enabled": false + } + }, + "hardwareProfile": { + "vmSize": "Basic_A0" + }, + "networkProfile": { + "networkInterfaces": [ + { + "id": "[resourceId('Microsoft.Network/networkInterfaces', 'farmervm-nic')]" + } + ] + }, + "osProfile": { + "adminPassword": "[parameters('password-for-farmervm')]", + "adminUsername": "farmer-admin", + "computerName": "farmervm" + }, + "priority": "Regular", + "storageProfile": { + "dataDisks": [ + { + "createOption": "Empty", + "diskSizeGB": 1024, + "lun": 0, + "managedDisk": { + "storageAccountType": "Standard_LRS" + }, + "name": "farmervm-datadisk-0" + } + ], + "imageReference": { + "offer": "WindowsServer", + "publisher": "MicrosoftWindowsServer", + "sku": "2012-Datacenter", + "version": "latest" + }, + "osDisk": { + "createOption": "FromImage", + "diskSizeGB": 128, + "managedDisk": { + "storageAccountType": "Standard_LRS" + }, + "name": "farmervm-osdisk" + } + } + }, + "tags": {}, + "type": "Microsoft.Compute/virtualMachines" + }, + { + "apiVersion": "2018-11-01", + "dependsOn": [ + "[resourceId('Microsoft.Network/virtualNetworks', 'farmervm-vnet')]", + "[resourceId('Microsoft.Network/publicIPAddresses', 'farmervm-ip')]" + ], + "location": "uksouth", + "name": "farmervm-nic", + "properties": { + "ipConfigurations": [ + { + "name": "ipconfig1", + "properties": { + "privateIPAllocationMethod": "Dynamic", + "publicIPAddress": { + "id": "[resourceId('Microsoft.Network/publicIPAddresses', 'farmervm-ip')]" + }, + "subnet": { + "id": "[resourceId('Microsoft.Network/virtualNetworks/subnets', 'farmervm-vnet', 'farmervm-subnet')]" + } + } + } + ] + }, + "tags": {}, + "type": "Microsoft.Network/networkInterfaces" + }, + { + "apiVersion": "2020-07-01", + "dependsOn": [], + "location": "uksouth", + "name": "farmervm-vnet", + "properties": { + "addressSpace": { + "addressPrefixes": [ + "10.0.0.0/16" + ] + }, + "subnets": [ + { + "name": "farmervm-subnet", + "properties": { + "addressPrefix": "10.0.0.0/24", + "delegations": [] + } + } + ] + }, + "tags": {}, + "type": "Microsoft.Network/virtualNetworks" + }, + { + "apiVersion": "2018-11-01", + "location": "uksouth", + "name": "farmervm-ip", + "properties": { + "publicIPAllocationMethod": "Dynamic" + }, + "sku": { + "name": "Basic" + }, + "tags": {}, + "type": "Microsoft.Network/publicIPAddresses" + } + ] + } + }, + "resourceGroup": "nested-resources", + "tags": {}, + "type": "Microsoft.Resources/deployments" + }, + { + "apiVersion": "2018-02-01", + "location": "northeurope", + "name": "docker-func-farm", + "properties": { + "name": "docker-func-farm", + "perSiteScaling": false, + "reserved": false + }, + "sku": { + "capacity": 0, + "name": "Y1", + "size": "Y1", + "tier": "Dynamic" + }, + "tags": {}, + "type": "Microsoft.Web/serverfarms" + }, + { + "apiVersion": "2019-06-01", + "dependsOn": [], + "kind": "StorageV2", + "location": "northeurope", + "name": "dockerfuncstorage", + "properties": {}, + "sku": { + "name": "Standard_LRS" + }, + "tags": {}, + "type": "Microsoft.Storage/storageAccounts" + }, + { + "apiVersion": "2021-03-01", + "dependsOn": [ + "[resourceId('Microsoft.Storage/storageAccounts', 'dockerfuncstorage')]", + "[resourceId('Microsoft.Web/serverfarms', 'docker-func-farm')]" + ], + "kind": "functionapp", + "location": "northeurope", + "name": "docker-func", + "properties": { + "httpsOnly": false, + "serverFarmId": "[resourceId('Microsoft.Web/serverfarms', 'docker-func-farm')]", + "siteConfig": { + "alwaysOn": false, + "appCommandLine": "do it", + "appSettings": [ + { + "name": "AzureWebJobsDashboard", + "value": "[concat('DefaultEndpointsProtocol=https;AccountName=dockerfuncstorage;AccountKey=', listKeys(resourceId('Microsoft.Storage/storageAccounts', 'dockerfuncstorage'), '2017-10-01').keys[0].value)]" + }, + { + "name": "AzureWebJobsStorage", + "value": "[concat('DefaultEndpointsProtocol=https;AccountName=dockerfuncstorage;AccountKey=', listKeys(resourceId('Microsoft.Storage/storageAccounts', 'dockerfuncstorage'), '2017-10-01').keys[0].value)]" + }, + { + "name": "DOCKER_REGISTRY_SERVER_PASSWORD", + "value": "[parameters('secure_pass_param')]" + }, + { + "name": "DOCKER_REGISTRY_SERVER_URL", + "value": "http://www.farmer.io/" + }, + { + "name": "DOCKER_REGISTRY_SERVER_USERNAME", + "value": "Robert Lewandowski" + }, + { + "name": "FUNCTIONS_EXTENSION_VERSION", + "value": "~3" + }, + { + "name": "FUNCTIONS_WORKER_RUNTIME", + "value": "dotnet" + }, + { + "name": "WEBSITE_CONTENTAZUREFILECONNECTIONSTRING", + "value": "[concat('DefaultEndpointsProtocol=https;AccountName=dockerfuncstorage;AccountKey=', listKeys(resourceId('Microsoft.Storage/storageAccounts', 'dockerfuncstorage'), '2017-10-01').keys[0].value)]" + }, + { + "name": "WEBSITE_CONTENTSHARE", + "value": "docker-func" + }, + { + "name": "WEBSITE_NODE_DEFAULT_VERSION", + "value": "10.14.1" + } + ], + "connectionStrings": [], + "metadata": [] + } + }, + "tags": {}, + "type": "Microsoft.Web/sites" + } + ] +} \ No newline at end of file diff --git a/src/Tests/test-data/service-bus.jsonout/deployment.json b/src/Tests/test-data/service-bus.jsonout/deployment.json new file mode 100644 index 000000000..003114b81 --- /dev/null +++ b/src/Tests/test-data/service-bus.jsonout/deployment.json @@ -0,0 +1,100 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "outputs": {}, + "parameters": {}, + "resources": [ + { + "apiVersion": "2017-04-01", + "dependsOn": [], + "location": "northeurope", + "name": "farmer-bus", + "sku": { + "capacity": 1, + "name": "Premium", + "tier": "Premium" + }, + "tags": {}, + "type": "Microsoft.ServiceBus/namespaces" + }, + { + "apiVersion": "2017-04-01", + "dependsOn": [ + "[resourceId('Microsoft.ServiceBus/namespaces', 'farmer-bus')]" + ], + "name": "farmer-bus/queue1", + "properties": {}, + "type": "Microsoft.ServiceBus/namespaces/queues" + }, + { + "apiVersion": "2017-04-01", + "dependsOn": [ + "[resourceId('Microsoft.ServiceBus/namespaces', 'farmer-bus')]" + ], + "name": "farmer-bus/topic1", + "properties": {}, + "type": "Microsoft.ServiceBus/namespaces/topics" + }, + { + "apiVersion": "2017-04-01", + "dependsOn": [ + "[resourceId('Microsoft.ServiceBus/namespaces/topics', 'farmer-bus', 'topic1')]" + ], + "name": "farmer-bus/topic1/sub1", + "properties": {}, + "resources": [ + { + "apiVersion": "2017-04-01", + "dependsOn": [ + "sub1" + ], + "name": "filter1", + "properties": { + "correlationFilter": { + "properties": { + "header1": "headervalue1" + } + }, + "filterType": "CorrelationFilter" + }, + "type": "Rules" + } + ], + "type": "Microsoft.ServiceBus/namespaces/topics/subscriptions" + }, + { + "apiVersion": "2017-04-01", + "dependsOn": [], + "name": "farmer-bus/unmanaged-topic", + "properties": {}, + "type": "Microsoft.ServiceBus/namespaces/topics" + }, + { + "apiVersion": "2017-04-01", + "dependsOn": [ + "[resourceId('Microsoft.ServiceBus/namespaces/topics', 'farmer-bus', 'unmanaged-topic')]" + ], + "name": "farmer-bus/unmanaged-topic/sub1", + "properties": {}, + "resources": [ + { + "apiVersion": "2017-04-01", + "dependsOn": [ + "sub1" + ], + "name": "filter1", + "properties": { + "correlationFilter": { + "properties": { + "header1": "headervalue1" + } + }, + "filterType": "CorrelationFilter" + }, + "type": "Rules" + } + ], + "type": "Microsoft.ServiceBus/namespaces/topics/subscriptions" + } + ] +} \ No newline at end of file diff --git a/src/Tests/test-data/virtual-wan.jsonout/deployment.json b/src/Tests/test-data/virtual-wan.jsonout/deployment.json new file mode 100644 index 000000000..54d4df8db --- /dev/null +++ b/src/Tests/test-data/virtual-wan.jsonout/deployment.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "outputs": {}, + "parameters": {}, + "resources": [ + { + "apiVersion": "2020-07-01", + "location": "northeurope", + "name": "farmer-vwan", + "properties": { + "allowBranchToBranchTraffic": true, + "disableVpnEncryption": true, + "office365LocalBreakoutCategory": "None", + "type": "Standard" + }, + "type": "Microsoft.Network/virtualWans" + } + ] +} \ No newline at end of file diff --git a/src/Tests/test-data/vm.jsonout/deployment.json b/src/Tests/test-data/vm.jsonout/deployment.json new file mode 100644 index 000000000..046bb19ce --- /dev/null +++ b/src/Tests/test-data/vm.jsonout/deployment.json @@ -0,0 +1,160 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "outputs": {}, + "parameters": { + "password-for-isaacsVM": { + "type": "securestring" + } + }, + "resources": [ + { + "apiVersion": "2019-03-01", + "dependsOn": [ + "[resourceId('Microsoft.Network/networkInterfaces', 'isaacsVM-nic')]", + "[resourceId('Microsoft.Storage/storageAccounts', 'isaacsvmstorage')]" + ], + "location": "northeurope", + "name": "isaacsVM", + "properties": { + "diagnosticsProfile": { + "bootDiagnostics": { + "enabled": true, + "storageUri": "[reference('isaacsvmstorage').primaryEndpoints.blob]" + } + }, + "hardwareProfile": { + "vmSize": "Standard_A2" + }, + "networkProfile": { + "networkInterfaces": [ + { + "id": "[resourceId('Microsoft.Network/networkInterfaces', 'isaacsVM-nic')]" + } + ] + }, + "osProfile": { + "adminPassword": "[parameters('password-for-isaacsVM')]", + "adminUsername": "isaac", + "computerName": "isaacsVM" + }, + "priority": "Regular", + "storageProfile": { + "dataDisks": [ + { + "createOption": "Empty", + "diskSizeGB": 512, + "lun": 0, + "managedDisk": { + "storageAccountType": "Standard_LRS" + }, + "name": "isaacsvm-datadisk-0" + }, + { + "createOption": "Empty", + "diskSizeGB": 128, + "lun": 1, + "managedDisk": { + "storageAccountType": "StandardSSD_LRS" + }, + "name": "isaacsvm-datadisk-1" + } + ], + "imageReference": { + "offer": "WindowsServer", + "publisher": "MicrosoftWindowsServer", + "sku": "2012-Datacenter", + "version": "latest" + }, + "osDisk": { + "createOption": "FromImage", + "diskSizeGB": 128, + "managedDisk": { + "storageAccountType": "StandardSSD_LRS" + }, + "name": "isaacsvm-osdisk" + } + } + }, + "tags": {}, + "type": "Microsoft.Compute/virtualMachines" + }, + { + "apiVersion": "2018-11-01", + "dependsOn": [ + "[resourceId('Microsoft.Network/virtualNetworks', 'isaacsVM-vnet')]", + "[resourceId('Microsoft.Network/publicIPAddresses', 'isaacsVM-ip')]" + ], + "location": "northeurope", + "name": "isaacsVM-nic", + "properties": { + "ipConfigurations": [ + { + "name": "ipconfig1", + "properties": { + "privateIPAllocationMethod": "Dynamic", + "publicIPAddress": { + "id": "[resourceId('Microsoft.Network/publicIPAddresses', 'isaacsVM-ip')]" + }, + "subnet": { + "id": "[resourceId('Microsoft.Network/virtualNetworks/subnets', 'isaacsVM-vnet', 'isaacsVM-subnet')]" + } + } + } + ] + }, + "tags": {}, + "type": "Microsoft.Network/networkInterfaces" + }, + { + "apiVersion": "2020-07-01", + "dependsOn": [], + "location": "northeurope", + "name": "isaacsVM-vnet", + "properties": { + "addressSpace": { + "addressPrefixes": [ + "10.0.0.0/16" + ] + }, + "subnets": [ + { + "name": "isaacsVM-subnet", + "properties": { + "addressPrefix": "10.0.0.0/24", + "delegations": [] + } + } + ] + }, + "tags": {}, + "type": "Microsoft.Network/virtualNetworks" + }, + { + "apiVersion": "2018-11-01", + "location": "northeurope", + "name": "isaacsVM-ip", + "properties": { + "publicIPAllocationMethod": "Dynamic" + }, + "sku": { + "name": "Basic" + }, + "tags": {}, + "type": "Microsoft.Network/publicIPAddresses" + }, + { + "apiVersion": "2019-06-01", + "dependsOn": [], + "kind": "StorageV2", + "location": "northeurope", + "name": "isaacsvmstorage", + "properties": {}, + "sku": { + "name": "Standard_LRS" + }, + "tags": {}, + "type": "Microsoft.Storage/storageAccounts" + } + ] +} \ No newline at end of file