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/RELEASE_NOTES.md b/RELEASE_NOTES.md index 40401e07b..9fcd33017 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,7 +1,33 @@ Release Notes ============= +## vNext +* WebApp/Functions: Support for deployment slot settings with `slot_setting` and `slot_settings` + +## 1.6.31 +* AzureFirewall: Supports availability zones +* WebApps/Functions: Add support for vnet integration + +## 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. + +## 1.6.28 +* ServicePlan/WebApp: Support for enabling ZoneRedundant + +## 1.6.27 +* 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. +* WebApps: Allow multiple custom domains +* WebApps: Support custom port for docker container with `docker_port` + ## 1.6.26 -* WebApp/Functions: Fix .NET on Linux deployments +* WebApps/Functions: Fix .NET 5/6 on Linux deployments. ## 1.6.25 * CosmosDb: Add support for serverless capacity mode. 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/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. diff --git a/docs/content/api-overview/resources/functions.md b/docs/content/api-overview/resources/functions.md index 10208fa72..1b1db7374 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. | @@ -45,8 +47,13 @@ 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.| - +| add_allowed_ip_restriction | Adds an 'allow' rule for an ip | +| add_denied_ip_restriction | Adds an 'deny' rule for an ip | +| 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/virtual-machine.md b/docs/content/api-overview/resources/virtual-machine.md index 07bfd1b85..a84a5a467 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,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. 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.| diff --git a/docs/content/api-overview/resources/web-app.md b/docs/content/api-overview/resources/web-app.md index 28607f20d..5f8a60c15 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. | @@ -55,14 +55,22 @@ 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 | 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 custom domain to the app, containing an app service managed certificate | +| 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 | +| 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 | 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. | | 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 | 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/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 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 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 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/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/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/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/Arm/Insights.fs b/src/Farmer/Arm/Insights.fs index 0ee64a418..3a48ce2fb 100644 --- a/src/Farmer/Arm/Insights.fs +++ b/src/Farmer/Arm/Insights.fs @@ -3,7 +3,20 @@ module Farmer.Arm.Insights open Farmer -let components = ResourceType("Microsoft.Insights/components", "2014-04-01") +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 InstanceKind = + | Classic + | Workspace of ResourceId + member this.ResourceType = + match this with + | Classic -> components + | Workspace _ -> componentsWorkspace type Components = { Name : ResourceName @@ -11,7 +24,9 @@ type Components = LinkedWebsite : ResourceName option DisableIpMasking : bool SamplingPercentage : int - Tags: Map } + InstanceKind : InstanceKind + Tags: Map + Dependencies : ResourceId Set } interface IArmResource with member this.ResourceId = components.resourceId this.Name member this.JsonModel = @@ -19,16 +34,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.InstanceKind.ResourceType.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.InstanceKind with + | Workspace _ -> "LogAnalytics" + | Classic -> null + WorkspaceResourceId = + match this.InstanceKind with + | Workspace resourceId -> resourceId.Eval() + | Classic -> null |} |} \ No newline at end of file diff --git a/src/Farmer/Arm/Network.fs b/src/Farmer/Arm/Network.fs index a015502c0..799ad5d06 100644 --- a/src/Farmer/Arm/Network.fs +++ b/src/Farmer/Arm/Network.fs @@ -20,6 +20,34 @@ 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 (id,_) + | Direct (Managed id) -> Some id + | _ -> None + static member create(vnetRef:LinkedResource, subnetName:ResourceName) = + 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 { vnetId with Type = subnets; Segments = [subnetName] } ) + static member create(subnetRef:LinkedResource) = + if subnetRef.ResourceId.Type.Type <> subnets.Type then + raiseFarmer $"given resource was not of type '{subnets.Type}'." + Direct subnetRef + type PublicIpAddress = { Name : ResourceName Location : Location @@ -419,13 +447,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 +462,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 37f736c21..c6878191d 100644 --- a/src/Farmer/Arm/Web.fs +++ b/src/Farmer/Arm/Web.fs @@ -8,8 +8,8 @@ open Farmer.WebApp 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 sites = ResourceType ("Microsoft.Web/sites", "2021-03-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") @@ -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<_> @@ -29,6 +31,7 @@ type ServerFarm = WorkerCount : int MaximumElasticWorkerCount : int option OperatingSystem : OS + ZoneRedundant : FeatureFlag option Tags: Map } member this.IsDynamic = match this.Sku, this.WorkerSize with @@ -106,7 +109,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.map(fun f -> f.AsBoolean) |> Option.toNullable |} kind = this.Kind |> Option.toObj |} @@ -160,8 +164,7 @@ type SiteType = match this with | Slot _ -> slots | Site _ -> sites - - + [] type FTPState = | AllAllowed @@ -198,7 +201,9 @@ type Site = Metadata : List AutoSwapSlotName: string option ZipDeployPath : (string * ZipDeploy.ZipDeployTarget * ZipDeploy.ZipDeploySlot) option - HealthCheckPath : string option } + HealthCheckPath : string option + IpSecurityRestrictions : IpSecurityRestriction list + LinkToSubnet : SubnetReference option } /// Shorthand for SiteType.ResourceType member this.ResourceType = this.SiteType.ResourceType /// Shorthand for SiteType.ResourceName @@ -233,7 +238,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, _ @@ -250,6 +255,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 = @@ -272,6 +281,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 = 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 @@ -290,6 +309,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() |} |} |} @@ -312,6 +333,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 @@ -350,7 +391,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 +402,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 @@ -380,7 +424,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") @@ -400,6 +444,17 @@ type Certificate = canonicalName = this.DomainName |} |} +type SlotConfigName = + { SiteName : ResourceName + SlotSettingNames: string Set } + 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.AppInsights.fs b/src/Farmer/Builders/Builders.AppInsights.fs index ddd7d87a1..fd5ddf62d 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, ?resourceType) = + let resourceType = resourceType |> Option.defaultValue components + AppInsights.getInstrumentationKey(ResourceId.create (resourceType, name, ?group = resourceGroup)) type AppInsightsConfig = { Name : ResourceName DisableIpMasking : bool SamplingPercentage : int + 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 + 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 = [ @@ -28,6 +32,8 @@ type AppInsightsConfig = LinkedWebsite = None DisableIpMasking = this.DisableIpMasking SamplingPercentage = this.SamplingPercentage + Dependencies = this.Dependencies + InstanceKind = this.InstanceKind 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 + InstanceKind = 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 + InstanceKind = 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.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/Farmer/Builders/Builders.Functions.fs b/src/Farmer/Builders/Builders.Functions.fs index a9bf1db5d..54ddd7c6d 100644 --- a/src/Farmer/Builders/Builders.Functions.fs +++ b/src/Farmer/Builders/Builders.Functions.fs @@ -11,11 +11,12 @@ 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 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" @@ -200,7 +201,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 @@ -246,12 +247,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}" @@ -273,17 +274,20 @@ type FunctionsConfig = Some sc | _ -> None WorkerProcess = this.CommonWebConfig.WorkerProcess - HealthCheckPath = this.CommonWebConfig.HealthCheckPath } + HealthCheckPath = this.CommonWebConfig.HealthCheckPath + IpSecurityRestrictions = this.CommonWebConfig.IpSecurityRestrictions + LinkToSubnet = this.CommonWebConfig.IntegratedSubnet } match this.CommonWebConfig.ServicePlan with | DeployableResource this.Name.ResourceName resourceId -> { Name = resourceId.Name Location = location - Sku = Sku.Y1 + Sku = this.CommonWebConfig.Sku WorkerSize = Serverless WorkerCount = 0 MaximumElasticWorkerCount = None OperatingSystem = this.CommonWebConfig.OperatingSystem + ZoneRedundant = None Tags = this.Tags } | _ -> () @@ -308,6 +312,8 @@ type FunctionsConfig = Location = location DisableIpMasking = false SamplingPercentage = 100 + Dependencies = Set.empty + InstanceKind = Classic LinkedWebsite = match this.CommonWebConfig.OperatingSystem with | Windows -> Some this.Name.ResourceName @@ -316,6 +322,14 @@ type FunctionsConfig = | Some _ | None -> () + + match this.CommonWebConfig.IntegratedSubnet with + | None -> () + | Some subnetRef -> + { 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 @@ -323,6 +337,13 @@ type FunctionsConfig = { site with AppSettings = None; ConnectionStrings = None } for (_, slot) in this.CommonWebConfig.Slots |> Map.toSeq do slot.ToSite site + + match this.CommonWebConfig.SlotSettingNames with + | x when Set.empty <> x -> + { SiteName = this.Name.ResourceName + SlotSettingNames = this.CommonWebConfig.SlotSettingNames } + | _ -> + () ] type FunctionsBuilder() = @@ -331,6 +352,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 @@ -340,10 +362,15 @@ 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 - HealthCheckPath = None } + HealthCheckPath = None + IpSecurityRestrictions = [] + IntegratedSubnet = None + PrivateEndpoints = Set.empty + SlotSettingNames = Set.empty } StorageAccount = derived (fun config -> let storage = config.Name.ResourceName.Map (sprintf "%sstorage") |> sanitiseStorage |> ResourceName storageAccounts.resourceId storage) @@ -354,6 +381,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.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 diff --git a/src/Farmer/Builders/Builders.ServicePlan.fs b/src/Farmer/Builders/Builders.ServicePlan.fs index 49f9147b1..b211a27e2 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 : FeatureFlag 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, 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.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.Vm.fs b/src/Farmer/Builders/Builders.Vm.fs index c2b75b0f5..09a84f30a 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 option + 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 |> Option.defaultValue Regular Credentials = match this.Username with | Some username -> @@ -189,6 +192,7 @@ type VirtualMachineBuilder() = member _.Yield _ = { Name = ResourceName.Empty DiagnosticsStorageAccount = None + Priority = None Size = Basic_A0 Username = None PasswordParameter = None @@ -258,6 +262,20 @@ 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) = + 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 = + 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. [] member this.AddSsd(state:VmConfig, size) = this.AddDisk(state, size, StandardSSD_LRS) diff --git a/src/Farmer/Builders/Builders.WebApp.fs b/src/Farmer/Builders/Builders.WebApp.fs index f370b7042..2dbe704a5 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 @@ -62,6 +63,7 @@ type Runtime = module AppSettings = let WebsiteNodeDefaultVersion version = "WEBSITE_NODE_DEFAULT_VERSION", version let RunFromPackage = "WEBSITE_RUN_FROM_PACKAGE", "1" + let WebsitesPort (port:int) = "WEBSITES_PORT", port.ToString() let publishingPassword (name:ResourceName) = let resourceId = config.resourceId (name, ResourceName "publishingCredentials") @@ -80,7 +82,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) @@ -90,6 +93,7 @@ type SlotConfig = ConnectionStrings = owner.ConnectionStrings |> Option.map (Map.merge (this.ConnectionStrings |> Map.toList)) Identity = this.Identity + owner.Identity KeyVaultReferenceIdentity = this.KeyVaultReferenceIdentity |> Option.orElse owner.KeyVaultReferenceIdentity + IpSecurityRestrictions = this.IpSecurityRestrictions ZipDeployPath = None } type SlotBuilder() = @@ -101,7 +105,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} @@ -156,6 +161,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 = + { 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) + 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 = + { 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) + member this.DenyIp(state, name, ip:string) : SlotConfig = + 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 } @@ -166,6 +192,7 @@ type CommonWebConfig = { Name : WebAppName AlwaysOn : bool AppInsights : ResourceRef option + ConnectionStrings : Map Cors : Cors option FTPState : FTPState option HTTPSOnly : bool @@ -175,20 +202,38 @@ type CommonWebConfig = SecretStore : SecretStore ServicePlan : ResourceRef Settings : Map + Sku : Sku Slots : Map WorkerProcess : Bitness option ZipDeployPath : (string*ZipDeploy.ZipDeploySlot) option - HealthCheckPath: string option } + HealthCheckPath: string option + IpSecurityRestrictions: IpSecurityRestriction list + IntegratedSubnet : SubnetReference option + PrivateEndpoints: (SubnetReference * string option) Set + SlotSettingNames: string 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 HTTP20Enabled : bool option ClientAffinityEnabled : bool option WebSocketsEnabled: bool option - ConnectionStrings : Map Dependencies : ResourceId Set Tags : Map - Sku : Sku WorkerSize : WorkerSize WorkerCount : int MaximumElasticWorkerCount : int option @@ -202,7 +247,9 @@ type WebAppConfig = AutomaticLoggingExtension : bool SiteExtensions : ExtensionName Set PrivateEndpoints: (LinkedResource * string option) Set - CustomDomain : DomainConfig } + CustomDomains : Map + 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 @@ -284,6 +331,8 @@ type WebAppConfig = | _ , None -> () + yield! this.DockerPort |> Option.mapList AppSettings.WebsitesPort + if this.DockerCi then "DOCKER_ENABLE_CI", "true" ] @@ -329,7 +378,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 = [ @@ -424,7 +473,8 @@ 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 + LinkToSubnet = this.CommonWebConfig.IntegratedSubnet } match keyVault with | Some keyVault -> @@ -449,6 +499,8 @@ type WebAppConfig = Location = location DisableIpMasking = false SamplingPercentage = 100 + InstanceKind = Classic + Dependencies = Set.empty LinkedWebsite = match this.CommonWebConfig.OperatingSystem with | Windows -> Some this.Name.ResourceName @@ -462,12 +514,13 @@ 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 OperatingSystem = this.CommonWebConfig.OperatingSystem - Tags = this.Tags} + ZoneRedundant = this.ZoneRedundant + Tags = this.Tags } | _ -> () @@ -483,65 +536,88 @@ type WebAppConfig = for (_,slot) in this.CommonWebConfig.Slots |> Map.toSeq do slot.ToSite site - match this.CustomDomain with - | SecureDomain (customDomain, certOptions) -> + // 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 - 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 } + 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 - // 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 -> () - - yield! (PrivateEndpoint.create location this.ResourceId ["sites"] this.PrivateEndpoints) + previousHostNameBinding <- Some hostNameBinding.ResourceId + match customDomain with + | SecureDomain (customDomain, certOptions) -> + 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 hostNameBinding.ResourceId + } + 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 + DependsOn = Set.empty // Don't want the dependency in this nested template. + } + depends_on certRg + } :> IBuilder).BuildResources location + | _ -> () + + match this.CommonWebConfig.IntegratedSubnet with + | None -> () + | Some subnetRef -> + { Site = site + Subnet = subnetRef.ResourceId + Dependencies = subnetRef.Dependency |> Option.toList } + yield! (PrivateEndpoint.create location this.ResourceId ["sites"] this.CommonWebConfig.PrivateEndpoints) + + match this.CommonWebConfig.SlotSettingNames with + | x when Set.empty <> x -> + { SiteName = this.Name.ResourceName + SlotSettingNames = this.CommonWebConfig.SlotSettingNames } + | _ -> + () ] type WebAppBuilder() = @@ -550,6 +626,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 @@ -559,11 +636,15 @@ 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 - HealthCheckPath = None } - Sku = Sku.F1 + HealthCheckPath = None + IpSecurityRestrictions = [] + IntegratedSubnet = None + PrivateEndpoints = Set.empty + SlotSettingNames = Set.empty } WorkerSize = Small WorkerCount = 1 MaximumElasticWorkerCount = None @@ -572,7 +653,6 @@ type WebAppBuilder() = HTTP20Enabled = None ClientAffinityEnabled = None WebSocketsEnabled = None - ConnectionStrings = Map.empty Tags = Map.empty Dependencies = Set.empty Runtime = Runtime.DotNetCoreLts @@ -583,17 +663,24 @@ type WebAppBuilder() = AutomaticLoggingExtension = true SiteExtensions = Set.empty PrivateEndpoints = Set.empty - CustomDomain = NoDomain} + CustomDomains = Map.empty + DockerPort = None + ZoneRedundant = None } member _.Run(state:WebAppConfig) = if state.Name.ResourceName = ResourceName.Empty then raiseFarmer "No Web App name has been set." + state.CommonWebConfig.Validate() { state with SiteExtensions = match state with // 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 = @@ -607,7 +694,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 } @@ -620,17 +707,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 } @@ -683,11 +759,22 @@ 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)) + [] + 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} + /// 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 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 @@ -704,7 +791,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 @@ -773,6 +860,27 @@ 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)) = + 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, 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) = + 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) = @@ -888,3 +996,53 @@ 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) = + 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 }) + /// Integrate this app with a virtual network subnet + [] + 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.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, subnetName):VirtualNetworkConfig*ResourceName) = this.LinkToVNet (state, SubnetReference.create (vnet,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]) + /// 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) } + |> 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 + |> this.Wrap state \ No newline at end of file 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/Common.fs b/src/Farmer/Common.fs index 489d5218c..34e6da7c6 100644 --- a/src/Farmer/Common.fs +++ b/src/Farmer/Common.fs @@ -309,6 +309,21 @@ module Vm = /// Represents a disk in a VM. type DiskInfo = { Size : int; DiskType : DiskType } + type EvictionPolicy = + | Deallocate + | Delete + member this.ArmValue = match this with | Deallocate -> "Deallocate" | Delete -> "Delete" + type BillingProfile = + { MaxPrice: decimal } + type Priority = + | Low + | Regular + | Spot of evictionPolicy:EvictionPolicy * maxPrice:decimal + member this.ArmValue = + match this with + | Low -> "Low" + | Regular -> "Regular" + | Spot _ -> "Spot" module internal Validation = // ANDs two validation rules @@ -595,6 +610,82 @@ module Storage = [] type StorageService = Blobs | Tables | Files | Queues +/// A network represented by an IP address and CIDR prefix. +type public IPAddressCidr = + { Address : Net.IPAddress + Prefix : int } + +/// Functions for IP networks and CIDR notation. +module IPAddressCidr = + let parse (s:string) : IPAddressCidr = + match s.Split([|'/'|], StringSplitOptions.RemoveEmptyEntries) with + | [| ip; prefix |] -> + { Address = Net.IPAddress.Parse (ip.Trim()) + Prefix = int prefix } + | [| 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: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 |> 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 @@ -636,6 +727,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 + IpAddressCidr: IPAddressCidr + Action: IpSecurityAction } + static member Create name cidr action = + { Name = name + IpAddressCidr = cidr + Action = action } module Extensions = /// The Microsoft.AspNetCore.AzureAppServices logging extension. let Logging = ExtensionName "Microsoft.AspNetCore.AzureAppServices.SiteExtension" @@ -1502,79 +1604,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/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) diff --git a/src/Farmer/Farmer.fsproj b/src/Farmer/Farmer.fsproj index 3b86d2ae8..5a3fc4890 100644 --- a/src/Farmer/Farmer.fsproj +++ b/src/Farmer/Farmer.fsproj @@ -2,7 +2,7 @@ Farmer - 1.6.26 + 1.6.30 Farmer makes authoring ARM templates easy! Copyright 2019-2022 Compositional IT Ltd. Compositional IT @@ -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 @@ + + + + + @@ -104,7 +110,6 @@ - diff --git a/src/Farmer/Types.fs b/src/Farmer/Types.fs index c44b7d4d9..2350f4779 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/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/AppInsights.fs b/src/Tests/AppInsights.fs index 28772bcfe..6fa51462f 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,31 @@ 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 select query = json.SelectToken(query).ToString() + + 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 (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 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." } ] 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" diff --git a/src/Tests/Functions.fs b/src/Tests/Functions.fs index 9463fc4b6..51a97d51a 100644 --- a/src/Tests/Functions.fs +++ b/src/Tests/Functions.fs @@ -364,4 +364,149 @@ 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" + } + + test "Supports adding ip restriction" { + 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 + + 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 = 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 + + 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 = 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 = + 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" + } + test "Can integrate unmanaged vnet" { + let subnetId = Arm.Network.subnets.resourceId (ResourceName "my-vnet", ResourceName "my-subnet") + 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 + 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 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 + 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" + } + + test "Supports slot settings" { + 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 + + 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" ] + + 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" + 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" + + } + + test "Supports slot setting" { + 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 + + 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" ] + + 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 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', '{functionsApp.Name.ResourceName.Value}')]"] "Slot config names resource should depend on web site" + + } ] \ No newline at end of file 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/ServicePlan.fs b/src/Tests/ServicePlan.fs new file mode 100644 index 000000000..47af24405 --- /dev/null +++ b/src/Tests/ServicePlan.fs @@ -0,0 +1,56 @@ +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 "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 servicePlan = servicePlan { name "test"; zone_redundant Enabled } + 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 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/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/VirtualMachine.fs b/src/Tests/VirtualMachine.fs index 644007f8b..6fea32dbd 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 "spot_instance set more than once" + } + + 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 "priority and spot_instance both set" + } + ] \ No newline at end of file diff --git a/src/Tests/WebApp.fs b/src/Tests/WebApp.fs index c852370b2..ee5d04eed 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 @@ -280,7 +281,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 @@ -700,14 +701,210 @@ 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 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}" + } + + 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 hostnameBinding = resources |> getResource + 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" + } + test "Supports adding ip restriction for allowed ip" { + 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" (IPAddressCidr.parse ip) Allow + Expect.equal site.IpSecurityRestrictions [ expectedRestriction ] "Should add allowed ip security restriction" + } + test "Supports adding ip restriction for denied ip" { + 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 + + 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 = 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 = + 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" + } + + 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" + } + + 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" + + let site = wa |> getResources|> getResource |> List.head - Expect.equal hostnameBinding.Length 0 $"There should not be a hostname binding as a result of choosing the 'NoDomain' option" + let settings = Expect.wantSome site.AppSettings "AppSettings should be set" + let (hasValue, value) = settings.TryGetValue("WEBSITES_PORT"); + + 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" + } + + 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" + } + 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; link_to_unmanaged_vnet 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 with managed vnet" { + let vnetConfig = vnet { name "my-vnet" } + 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 + 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" + } + test "Supports slot settings" { + 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 + + 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" ] + + 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" + 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" + + } + + test "Supports slot setting" { + 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 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 + + 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" ] + + 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.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", 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.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/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.json b/src/Tests/test-data/lots-of-resources.json index 061382fbf..dfff0f497 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", @@ -140,7 +141,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')]" @@ -236,6 +237,7 @@ }, { "apiVersion": "2014-04-01", + "dependsOn": [], "kind": "web", "location": "northeurope", "name": "farmerfuncs1979-ai", @@ -252,7 +254,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')]", @@ -629,7 +631,7 @@ "type": "Microsoft.DocumentDb/databaseAccounts/mongodbDatabases" }, { - "apiVersion": "2018-10-01", + "apiVersion": "2019-03-01", "dependsOn": [ "[resourceId('Microsoft.Network/networkInterfaces', 'farmervm-nic')]" ], @@ -656,6 +658,7 @@ "adminUsername": "farmer-admin", "computerName": "farmervm" }, + "priority": "Regular", "storageProfile": { "dataDisks": [ { @@ -790,7 +793,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')]" 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.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": [ { 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