diff --git a/src/Farmer/Arm/Alert.fs b/src/Farmer/Arm/Alert.fs index edec3422a..c78b8bef3 100644 --- a/src/Farmer/Arm/Alert.fs +++ b/src/Farmer/Arm/Alert.fs @@ -38,33 +38,31 @@ type MetricAggregation = // E.g. if average of VM CPU is going over 80% for 15 minutes -> alert /// See the MetricNames and their Aggregations: /// https://docs.microsoft.com/en-us/azure/azure-monitor/essentials/metrics-supported -type ResourceCriteria = - { - /// Resource type name - MetricNamespace: ResourceType - /// Name of the metric - MetricName: MetricsName - /// Threshold to exceed to hit the alert - Threshold: int - /// Equals, GreaterThan, GreaterThanOrEqual, LessThan or LessThanOrEqual - Comparison: MetricComparison - /// Average, Count, Total, Maximum, Minimum - Aggregation: MetricAggregation - } - -type CustomMetricCriteria = - { - /// Resource type name. Default value is "Azure.ApplicationInsights" - MetricNamespace: ResourceType option - /// Name of the metric - MetricName: MetricsName - /// Threshold to exceed to hit the alert - Threshold: int - /// Equals, GreaterThan, GreaterThanOrEqual, LessThan or LessThanOrEqual - Comparison: MetricComparison - /// Average, Count, Total, Maximum, Minimum - Aggregation: MetricAggregation - } +type ResourceCriteria = { + /// Resource type name + MetricNamespace: ResourceType + /// Name of the metric + MetricName: MetricsName + /// Threshold to exceed to hit the alert + Threshold: int + /// Equals, GreaterThan, GreaterThanOrEqual, LessThan or LessThanOrEqual + Comparison: MetricComparison + /// Average, Count, Total, Maximum, Minimum + Aggregation: MetricAggregation +} + +type CustomMetricCriteria = { + /// Resource type name. Default value is "Azure.ApplicationInsights" + MetricNamespace: ResourceType option + /// Name of the metric + MetricName: MetricsName + /// Threshold to exceed to hit the alert + Threshold: int + /// Equals, GreaterThan, GreaterThanOrEqual, LessThan or LessThanOrEqual + Comparison: MetricComparison + /// Average, Count, Total, Maximum, Minimum + Aggregation: MetricAggregation +} /// Metric criterias /// https://docs.microsoft.com/en-us/azure/templates/microsoft.insights/metricalerts?tabs=json#metricalertcriteria @@ -79,11 +77,10 @@ type MetricAlertCriteria = WebTestId: Farmer.ResourceId * FailedLocationCount: int -type AlertAction = - { - actionGroupId: string - webHookProperties: obj - } +type AlertAction = { + actionGroupId: string + webHookProperties: obj +} let mapResourceCriteriaOperator (comparison: MetricComparison) = match comparison with @@ -103,7 +100,7 @@ let mapResourceCriteriaTimeAggregation (aggregation: MetricAggregation) = let createCriteria (criteria: MetricAlertCriteria) = match criteria with - | MultipleResourceMultipleMetricCriteria (multicriteria: obj list) -> + | MultipleResourceMultipleMetricCriteria(multicriteria: obj list) -> {| allOf = multicriteria ``odata.type`` = "Microsoft.Azure.Monitor.MultipleResourceMultipleMetricCriteria" @@ -113,19 +110,18 @@ let createCriteria (criteria: MetricAlertCriteria) = {| allOf = criterias - |> List.map (fun resourcecriteria -> - {| - threshold = resourcecriteria.Threshold - name = "Metric1" - metricNamespace = resourcecriteria.MetricNamespace.Type - metricName = - resourcecriteria.MetricName - |> (function - | MetricsName n -> n) - operator = resourcecriteria.Comparison |> mapResourceCriteriaOperator - timeAggregation = resourcecriteria.Aggregation |> mapResourceCriteriaTimeAggregation - criterionType = "StaticThresholdCriterion" - |}) + |> List.map (fun resourcecriteria -> {| + threshold = resourcecriteria.Threshold + name = "Metric1" + metricNamespace = resourcecriteria.MetricNamespace.Type + metricName = + resourcecriteria.MetricName + |> (function + | MetricsName n -> n) + operator = resourcecriteria.Comparison |> mapResourceCriteriaOperator + timeAggregation = resourcecriteria.Aggregation |> mapResourceCriteriaTimeAggregation + criterionType = "StaticThresholdCriterion" + |}) ``odata.type`` = "Microsoft.Azure.Monitor.SingleResourceMultipleMetricCriteria" |} :> obj @@ -133,27 +129,26 @@ let createCriteria (criteria: MetricAlertCriteria) = {| allOf = criterias - |> List.map (fun resourcecriteria -> - {| - threshold = resourcecriteria.Threshold - name = "Metric1" - metricNamespace = - resourcecriteria.MetricNamespace - |> Option.defaultValue (ResourceType("Azure.ApplicationInsights", "")) - |> (fun resourceType -> resourceType.Type) - metricName = - resourcecriteria.MetricName - |> (function - | MetricsName n -> n) - operator = resourcecriteria.Comparison |> mapResourceCriteriaOperator - timeAggregation = resourcecriteria.Aggregation |> mapResourceCriteriaTimeAggregation - criterionType = "StaticThresholdCriterion" - skipMetricValidation = true - |}) + |> List.map (fun resourcecriteria -> {| + threshold = resourcecriteria.Threshold + name = "Metric1" + metricNamespace = + resourcecriteria.MetricNamespace + |> Option.defaultValue (ResourceType("Azure.ApplicationInsights", "")) + |> (fun resourceType -> resourceType.Type) + metricName = + resourcecriteria.MetricName + |> (function + | MetricsName n -> n) + operator = resourcecriteria.Comparison |> mapResourceCriteriaOperator + timeAggregation = resourcecriteria.Aggregation |> mapResourceCriteriaTimeAggregation + criterionType = "StaticThresholdCriterion" + skipMetricValidation = true + |}) ``odata.type`` = "Microsoft.Azure.Monitor.SingleResourceMultipleMetricCriteria" |} :> obj - | WebtestLocationAvailabilityCriteria (componentId, webTestId, failedLocationCount) -> + | WebtestLocationAvailabilityCriteria(componentId, webTestId, failedLocationCount) -> {| webTestId = webTestId.Eval() componentId = componentId.Eval() @@ -162,17 +157,16 @@ let createCriteria (criteria: MetricAlertCriteria) = |} :> obj -type AlertData = - { - Name: Farmer.ResourceName - Description: string - Severity: AlertSeverity - Frequency: IsoDateTime - Window: IsoDateTime - Actions: List - LinkedResources: ResourceId list - Criteria: MetricAlertCriteria - } +type AlertData = { + Name: Farmer.ResourceName + Description: string + Severity: AlertSeverity + Frequency: IsoDateTime + Window: IsoDateTime + Actions: List + LinkedResources: ResourceId list + Criteria: MetricAlertCriteria +} with interface Farmer.IArmResource with member this.ResourceId = metricAlert.resourceId this.Name @@ -185,12 +179,12 @@ type AlertData = let scopes = this.LinkedResources |> List.rev |> List.map (fun r -> r.Eval()) - {| metricAlert.Create(this.Name) with - tags = tags - location = "global" - dependsOn = this.LinkedResources |> List.map (fun r -> r.Eval()) - properties = - {| + {| + metricAlert.Create(this.Name) with + tags = tags + location = "global" + dependsOn = this.LinkedResources |> List.map (fun r -> r.Eval()) + properties = {| description = this.Description severity = match this.Severity with diff --git a/src/Farmer/Arm/App.fs b/src/Farmer/Arm/App.fs index ee978cae7..2f958c04d 100644 --- a/src/Farmer/Arm/App.fs +++ b/src/Farmer/Arm/App.fs @@ -16,494 +16,438 @@ let storages = open Farmer.ContainerAppValidation open Farmer.Identity -type Container = - { - Name: string - DockerImage: Containers.DockerImage - VolumeMounts: Map - Resources: {| CPU: float - Memory: float - EphemeralStorage: float option |} - } +type Container = { + Name: string + DockerImage: Containers.DockerImage + VolumeMounts: Map + Resources: {| + CPU: float + Memory: float + EphemeralStorage: float option + |} +} -type ManagedEnvironmentStorage = - { - Name: ResourceName - Environment: ResourceId - AzureFile: {| ShareName: ResourceName - AccountName: Storage.StorageAccountName - AccountKey: string - AccessMode: StorageAccessMode |} - Dependencies: Set - } +type ManagedEnvironmentStorage = { + Name: ResourceName + Environment: ResourceId + AzureFile: {| + ShareName: ResourceName + AccountName: Storage.StorageAccountName + AccountKey: string + AccessMode: StorageAccessMode + |} + Dependencies: Set +} with interface IArmResource with member this.ResourceId = storages.resourceId this.Name - member this.JsonModel = - {| storages.Create( - ResourceName $"{this.Environment.Name.Value}/{this.Name.Value}", - dependsOn = this.Dependencies - ) with - properties = - {| - azureFile = - {| - shareName = this.AzureFile.ShareName.Value - accountName = this.AzureFile.AccountName.ResourceName.Value - accountKey = this.AzureFile.AccountKey - accessMode = this.AzureFile.AccessMode.ArmValue - |} + member this.JsonModel = {| + storages.Create( + ResourceName $"{this.Environment.Name.Value}/{this.Name.Value}", + dependsOn = this.Dependencies + ) with + properties = {| + azureFile = {| + shareName = this.AzureFile.ShareName.Value + accountName = this.AzureFile.AccountName.ResourceName.Value + accountKey = this.AzureFile.AccountKey + accessMode = this.AzureFile.AccessMode.ArmValue |} - |} + |} + |} static member from(env: ResourceId) = function - | KeyValue (name, Volume.AzureFileShare (share, accountName, accessMode)) -> - Some - { - Name = ResourceName name - Environment = env - Dependencies = Set.ofList [ env; Storage.storageAccounts.resourceId accountName.ResourceName ] - AzureFile = - {| - ShareName = share - AccountName = accountName - AccountKey = - $"[listKeys('Microsoft.Storage/storageAccounts/{accountName.ResourceName.Value}', '2018-07-01').keys[0].value]" - AccessMode = accessMode - |} - } + | KeyValue(name, Volume.AzureFileShare(share, accountName, accessMode)) -> + Some { + Name = ResourceName name + Environment = env + Dependencies = Set.ofList [ env; Storage.storageAccounts.resourceId accountName.ResourceName ] + AzureFile = {| + ShareName = share + AccountName = accountName + AccountKey = + $"[listKeys('Microsoft.Storage/storageAccounts/{accountName.ResourceName.Value}', '2018-07-01').keys[0].value]" + AccessMode = accessMode + |} + } | _ -> None -type ContainerApp = - { - Name: ResourceName - Environment: ResourceId - ActiveRevisionsMode: ActiveRevisionsMode - IngressMode: IngressMode option - ScaleRules: Map - Identity: ManagedIdentity - Replicas: {| Min: int; Max: int |} option - DaprConfig: {| AppId: string |} option - Secrets: Map - EnvironmentVariables: Map - ImageRegistryCredentials: ImageRegistryAuthentication list - Containers: Container list - Location: Location - Dependencies: Set - Volumes: Map - } +type ContainerApp = { + Name: ResourceName + Environment: ResourceId + ActiveRevisionsMode: ActiveRevisionsMode + IngressMode: IngressMode option + ScaleRules: Map + Identity: ManagedIdentity + Replicas: {| Min: int; Max: int |} option + DaprConfig: {| AppId: string |} option + Secrets: Map + EnvironmentVariables: Map + ImageRegistryCredentials: ImageRegistryAuthentication list + Containers: Container list + Location: Location + Dependencies: Set + Volumes: Map +} with - member private this.dependencies = - [ - this.Environment - yield! this.Dependencies - yield! - this.Volumes - |> Seq.choose (function - | KeyValue (name, Volume.AzureFileShare (_)) -> - storages.resourceId (this.Environment.Name, ResourceName name) |> Some - | _ -> None) - yield! this.Identity.Dependencies - ] + member private this.dependencies = [ + this.Environment + yield! this.Dependencies + yield! + this.Volumes + |> Seq.choose (function + | KeyValue(name, Volume.AzureFileShare(_)) -> + storages.resourceId (this.Environment.Name, ResourceName name) |> Some + | _ -> None) + yield! this.Identity.Dependencies + ] member private this.ResourceId = containerApps.resourceId this.Name member this.SystemIdentity = SystemIdentity this.ResourceId interface IParameters with - member this.SecureParameters = - [ - for secret in this.Secrets do - match secret.Value with - | ParameterSecret sp -> sp - | ExpressionSecret _ -> () - for credential in this.ImageRegistryCredentials do - match credential with - | ImageRegistryAuthentication.Credential credential -> credential.Password - | ImageRegistryAuthentication.ListCredentials _ -> () - | ImageRegistryAuthentication.ManagedIdentityCredential _ -> () + member this.SecureParameters = [ + for secret in this.Secrets do + match secret.Value with + | ParameterSecret sp -> sp + | ExpressionSecret _ -> () + for credential in this.ImageRegistryCredentials do + match credential with + | ImageRegistryAuthentication.Credential credential -> credential.Password + | ImageRegistryAuthentication.ListCredentials _ -> () + | ImageRegistryAuthentication.ManagedIdentityCredential _ -> () - ] + ] interface IArmResource with member this.ResourceId = containerApps.resourceId this.Name - member this.JsonModel = - {| containerApps.Create(this.Name, this.Location, this.dependencies) with + member this.JsonModel = {| + containerApps.Create(this.Name, this.Location, this.dependencies) with kind = "containerapp" identity = if this.Identity = ManagedIdentity.Empty then Unchecked.defaultof<_> else this.Identity.ToArmJson - properties = - {| - managedEnvironmentId = this.Environment.Eval() - configuration = - let buildPasswordRef (resourceId: ResourceId) = - $"password-for-%s{resourceId.Name.Value}-registry" + properties = {| + managedEnvironmentId = this.Environment.Eval() + configuration = + let buildPasswordRef (resourceId: ResourceId) = + $"password-for-%s{resourceId.Name.Value}-registry" - {| - secrets = - [| - for cred in this.ImageRegistryCredentials do - match cred with - | ImageRegistryAuthentication.Credential cred -> - {| - name = cred.Username - value = cred.Password.ArmExpression.Eval() - |} - | ImageRegistryAuthentication.ListCredentials resourceId -> - {| - name = buildPasswordRef resourceId - value = - ArmExpression - .create( - $"listCredentials({resourceId.ArmExpression.Value}, '2019-05-01').passwords[0].value" - ) - .Eval() - |} - | ImageRegistryAuthentication.ManagedIdentityCredential cred -> - {| - name = cred.Server - value = - match cred.Identity.Dependencies with - | [] -> String.Empty - | primaryDependency :: _ -> - primaryDependency.ArmExpression.Eval() - |} - for setting in this.Secrets do - {| - name = setting.Key.Value - value = setting.Value.Value - |} + {| + secrets = [| + for cred in this.ImageRegistryCredentials do + match cred with + | ImageRegistryAuthentication.Credential cred -> {| + name = cred.Username + value = cred.Password.ArmExpression.Eval() + |} + | ImageRegistryAuthentication.ListCredentials resourceId -> {| + name = buildPasswordRef resourceId + value = + ArmExpression + .create( + $"listCredentials({resourceId.ArmExpression.Value}, '2019-05-01').passwords[0].value" + ) + .Eval() + |} + | ImageRegistryAuthentication.ManagedIdentityCredential cred -> {| + name = cred.Server + value = + match cred.Identity.Dependencies with + | [] -> String.Empty + | primaryDependency :: _ -> primaryDependency.ArmExpression.Eval() + |} + for setting in this.Secrets do + {| + name = setting.Key.Value + value = setting.Value.Value + |} + |] + activeRevisionsMode = + match this.ActiveRevisionsMode with + | Single -> "Single" + | Multiple -> "Multiple" + registries = [| + for cred in this.ImageRegistryCredentials do + match cred with + | ImageRegistryAuthentication.Credential cred -> {| + server = cred.Server + username = cred.Username + passwordSecretRef = cred.Username + identity = null + |} + | ImageRegistryAuthentication.ListCredentials resourceId -> {| + server = + ArmExpression + .create( + $"reference({resourceId.ArmExpression.Value}, '2019-05-01').loginServer" + ) + .Eval() + username = + ArmExpression + .create( + $"listCredentials({resourceId.ArmExpression.Value}, '2019-05-01').username" + ) + .Eval() + passwordSecretRef = buildPasswordRef resourceId + identity = null + |} + | ImageRegistryAuthentication.ManagedIdentityCredential cred -> {| + server = cred.Server + username = String.Empty + passwordSecretRef = null + identity = + if cred.Identity.Dependencies.Length > 0 then + cred.Identity.Dependencies.Head.ArmExpression.Eval() + else + String.Empty + |} + |] + ingress = + match this.IngressMode with + | Some InternalOnly -> box {| external = false |} + | Some(External(targetPort, transport)) -> + box {| + external = true + targetPort = targetPort + transport = + match transport with + | Some HTTP1 -> "http" + | Some HTTP2 -> "http2" + | Some Auto -> "auto" + | None -> null + |} + | None -> null + |} + + template = {| + containers = [| + for container in this.Containers do + {| + image = container.DockerImage.ImageTag + name = container.Name + env = [| + for env in this.EnvironmentVariables do + match env.Value with + | EnvValue value -> {| + name = env.Key + value = value + secretref = null + |} + | SecureEnvExpression armExpr -> {| + name = env.Key + value = armExpr.Eval() + secretref = null + |} + | SecureEnvValue _ -> {| + name = env.Key + value = null + secretref = env.Key + |} |] - activeRevisionsMode = - match this.ActiveRevisionsMode with - | Single -> "Single" - | Multiple -> "Multiple" - registries = - [| - for cred in this.ImageRegistryCredentials do - match cred with - | ImageRegistryAuthentication.Credential cred -> - {| - server = cred.Server - username = cred.Username - passwordSecretRef = cred.Username - identity = null - |} - | ImageRegistryAuthentication.ListCredentials resourceId -> - {| - server = - ArmExpression - .create( - $"reference({resourceId.ArmExpression.Value}, '2019-05-01').loginServer" - ) - .Eval() - username = - ArmExpression - .create( - $"listCredentials({resourceId.ArmExpression.Value}, '2019-05-01').username" - ) - .Eval() - passwordSecretRef = buildPasswordRef resourceId - identity = null - |} - | ImageRegistryAuthentication.ManagedIdentityCredential cred -> - {| - server = cred.Server - username = String.Empty - passwordSecretRef = null - identity = - if cred.Identity.Dependencies.Length > 0 then - cred.Identity.Dependencies.Head.ArmExpression.Eval() - else - String.Empty + resources = + {| + cpu = container.Resources.CPU + ephemeralStorage = + container.Resources.EphemeralStorage + |> Option.map (sprintf "%.2fGi") + |> Option.toObj + memory = container.Resources.Memory |> sprintf "%.2fGi" + |} + :> obj + volumeMounts = + container.VolumeMounts + |> Seq.map (fun kvp -> {| + volumeName = kvp.Key + mountPath = kvp.Value + |}) + |> List.ofSeq + |> function + | [] -> Unchecked.defaultof<_> + | vms -> vms + |} + |] + scale = {| + minReplicas = this.Replicas |> Option.map (fun c -> c.Min) |> Option.toNullable + maxReplicas = this.Replicas |> Option.map (fun c -> c.Max) |> Option.toNullable + rules = [| + for rule in this.ScaleRules do + match rule.Value with + | ScaleRule.Custom customRule -> + {| + name = rule.Key + custom = customRule + |} + :> obj + | ScaleRule.EventHub settings -> + {| + name = rule.Key + custom = {| + ``type`` = "azure-eventhub" + metadata = {| + consumerGroup = settings.ConsumerGroup + unprocessedEventThreshold = + string settings.UnprocessedEventThreshold + blobContainer = settings.CheckpointBlobContainerName + checkpointStrategy = "blobMetadata" |} - |] - ingress = - match this.IngressMode with - | Some InternalOnly -> box {| external = false |} - | Some (External (targetPort, transport)) -> - box - {| - external = true - targetPort = targetPort - transport = - match transport with - | Some HTTP1 -> "http" - | Some HTTP2 -> "http2" - | Some Auto -> "auto" - | None -> null + auth = [| + {| + secretRef = settings.EventHubConnectionSecretRef + triggerParameter = "connection" + |} + {| + secretRef = settings.StorageConnectionSecretRef + triggerParameter = "storageConnection" + |} + |] |} - | None -> null - |} - - template = - {| - containers = - [| - for container in this.Containers do - {| - image = container.DockerImage.ImageTag - name = container.Name - env = - [| - for env in this.EnvironmentVariables do - match env.Value with - | EnvValue value -> - {| - name = env.Key - value = value - secretref = null - |} - | SecureEnvExpression armExpr -> - {| - name = env.Key - value = armExpr.Eval() - secretref = null - |} - | SecureEnvValue _ -> - {| - name = env.Key - value = null - secretref = env.Key - |} - |] - resources = + |} + :> obj + | ScaleRule.ServiceBus settings -> + {| + name = rule.Key + custom = {| + ``type`` = "azure-servicebus" + metadata = {| + queueName = settings.QueueName + messageCount = string settings.MessageCount + |} + auth = [| {| - cpu = container.Resources.CPU - ephemeralStorage = - container.Resources.EphemeralStorage - |> Option.map (sprintf "%.2fGi") - |> Option.toObj - memory = container.Resources.Memory |> sprintf "%.2fGi" + secretRef = settings.SecretRef + triggerParameter = "connection" |} - :> obj - volumeMounts = - container.VolumeMounts - |> Seq.map (fun kvp -> - {| - volumeName = kvp.Key - mountPath = kvp.Value - |}) - |> List.ofSeq - |> function - | [] -> Unchecked.defaultof<_> - | vms -> vms + |] |} - |] - scale = - {| - minReplicas = this.Replicas |> Option.map (fun c -> c.Min) |> Option.toNullable - maxReplicas = this.Replicas |> Option.map (fun c -> c.Max) |> Option.toNullable - rules = - [| - for rule in this.ScaleRules do - match rule.Value with - | ScaleRule.Custom customRule -> - {| - name = rule.Key - custom = customRule - |} - :> obj - | ScaleRule.EventHub settings -> - {| - name = rule.Key - custom = - {| - ``type`` = "azure-eventhub" - metadata = - {| - consumerGroup = settings.ConsumerGroup - unprocessedEventThreshold = - string - settings.UnprocessedEventThreshold - blobContainer = - settings.CheckpointBlobContainerName - checkpointStrategy = "blobMetadata" - |} - auth = - [| - {| - secretRef = - settings.EventHubConnectionSecretRef - triggerParameter = "connection" - |} - {| - secretRef = - settings.StorageConnectionSecretRef - triggerParameter = "storageConnection" - |} - |] - |} - |} - :> obj - | ScaleRule.ServiceBus settings -> - {| - name = rule.Key - custom = - {| - ``type`` = "azure-servicebus" - metadata = - {| - queueName = settings.QueueName - messageCount = string settings.MessageCount - |} - auth = - [| - {| - secretRef = settings.SecretRef - triggerParameter = "connection" - |} - |] - |} - |} - :> obj - | ScaleRule.Http settings -> - {| - name = rule.Key - http = - {| - metadata = - {| - concurrentRequests = - string settings.ConcurrentRequests - |} - |} - |} - :> obj - | ScaleRule.CPU settings -> - {| - name = rule.Key - custom = - {| - ``type`` = "cpu" - metadata = - {| - ``type`` = - match settings with - | Utilization _ -> "Utilization" - | AverageValue _ -> "AverageValue" - value = - match settings with - | Utilization v -> - v.Utilization |> string - | AverageValue v -> - v.AverageValue |> string - |} - |} - |} - :> obj - | ScaleRule.Memory settings -> - {| - name = rule.Key - custom = - {| - ``type`` = "memory" - metadata = - {| - ``type`` = - match settings with - | Utilization _ -> "Utilization" - | AverageValue _ -> "AverageValue" - value = - match settings with - | Utilization v -> - v.Utilization |> string - | AverageValue v -> - v.AverageValue |> string - |} - |} - |} - :> obj - | ScaleRule.StorageQueue settings -> - {| - name = rule.Key - custom = - {| - ``type`` = "azure-queue" - metadata = - {| - queueName = settings.QueueName - queueLength = string settings.QueueLength - connectionFromEnv = - settings.StorageConnectionSecretRef - accountName = settings.AccountName - |} - |} - |} - |] - |} - dapr = - match this.DaprConfig with - | Some settings -> + |} + :> obj + | ScaleRule.Http settings -> {| - enabled = true - appId = settings.AppId + name = rule.Key + http = {| + metadata = {| + concurrentRequests = string settings.ConcurrentRequests + |} + |} |} :> obj - | None -> {| enabled = false |} - volumes = - [ - for key, value in Map.toSeq this.Volumes do - match key, value with - | volumeName, Volume.AzureFileShare (shareName, accountName, _) -> - {| - name = volumeName - storageType = "AzureFile" - storageName = volumeName + | ScaleRule.CPU settings -> + {| + name = rule.Key + custom = {| + ``type`` = "cpu" + metadata = {| + ``type`` = + match settings with + | Utilization _ -> "Utilization" + | AverageValue _ -> "AverageValue" + value = + match settings with + | Utilization v -> v.Utilization |> string + | AverageValue v -> v.AverageValue |> string |} - | volumeName, Volume.EmptyDirectory -> - {| - name = volumeName - storageType = "EmptyDir" - storageName = null + |} + |} + :> obj + | ScaleRule.Memory settings -> + {| + name = rule.Key + custom = {| + ``type`` = "memory" + metadata = {| + ``type`` = + match settings with + | Utilization _ -> "Utilization" + | AverageValue _ -> "AverageValue" + value = + match settings with + | Utilization v -> v.Utilization |> string + | AverageValue v -> v.AverageValue |> string |} - ] - |> function - | [] -> Unchecked.defaultof<_> - | vs -> vs - |} + |} + |} + :> obj + | ScaleRule.StorageQueue settings -> {| + name = rule.Key + custom = {| + ``type`` = "azure-queue" + metadata = {| + queueName = settings.QueueName + queueLength = string settings.QueueLength + connectionFromEnv = settings.StorageConnectionSecretRef + accountName = settings.AccountName + |} + |} + |} + |] + |} + dapr = + match this.DaprConfig with + | Some settings -> + {| + enabled = true + appId = settings.AppId + |} + :> obj + | None -> {| enabled = false |} + volumes = + [ + for key, value in Map.toSeq this.Volumes do + match key, value with + | volumeName, Volume.AzureFileShare(shareName, accountName, _) -> {| + name = volumeName + storageType = "AzureFile" + storageName = volumeName + |} + | volumeName, Volume.EmptyDirectory -> {| + name = volumeName + storageType = "EmptyDir" + storageName = null + |} + ] + |> function + | [] -> Unchecked.defaultof<_> + | vs -> vs |} - |} + |} + |} -type ManagedEnvironment = - { - Name: ResourceName - Location: Location - InternalLoadBalancerState: FeatureFlag - LogAnalytics: ResourceId - AppInsightsInstrumentationKey: ArmExpression option - Dependencies: Set - Tags: Map - } +type ManagedEnvironment = { + Name: ResourceName + Location: Location + InternalLoadBalancerState: FeatureFlag + LogAnalytics: ResourceId + AppInsightsInstrumentationKey: ArmExpression option + Dependencies: Set + Tags: Map +} with interface IArmResource with member this.ResourceId = managedEnvironments.resourceId this.Name - member this.JsonModel = - {| managedEnvironments.Create(this.Name, this.Location, this.Dependencies, this.Tags) with + member this.JsonModel = {| + managedEnvironments.Create(this.Name, this.Location, this.Dependencies, this.Tags) with kind = "containerenvironment" - properties = - {| - ``type`` = "managed" - internalLoadBalancerEnabled = this.InternalLoadBalancerState.AsBoolean - daprAIInstrumentationKey = - this.AppInsightsInstrumentationKey - |> Option.map (fun key -> key.Eval()) - |> Option.toObj - appLogsConfiguration = - {| - destination = "log-analytics" - logAnalyticsConfiguration = - {| - customerId = LogAnalytics.getCustomerId(this.LogAnalytics).Eval() - sharedKey = LogAnalytics.getPrimarySharedKey(this.LogAnalytics).Eval() - |} - |} + properties = {| + ``type`` = "managed" + internalLoadBalancerEnabled = this.InternalLoadBalancerState.AsBoolean + daprAIInstrumentationKey = + this.AppInsightsInstrumentationKey + |> Option.map (fun key -> key.Eval()) + |> Option.toObj + appLogsConfiguration = {| + destination = "log-analytics" + logAnalyticsConfiguration = {| + customerId = LogAnalytics.getCustomerId(this.LogAnalytics).Eval() + sharedKey = LogAnalytics.getPrimarySharedKey(this.LogAnalytics).Eval() + |} |} - |} + |} + |} diff --git a/src/Farmer/Arm/ApplicationGateway.fs b/src/Farmer/Arm/ApplicationGateway.fs index 28194026d..a403498bd 100644 --- a/src/Farmer/Arm/ApplicationGateway.fs +++ b/src/Farmer/Arm/ApplicationGateway.fs @@ -52,150 +52,239 @@ let applicationGatewayTrustedRootCertificates = let applicationGatewayUrlPathMaps = ResourceType("Microsoft.Network/applicationGateways/urlPathMap", "2020-11-01") -type ApplicationGateway = - { - Name: ResourceName - Location: Location - Sku: ApplicationGatewaySku - Identity: ManagedIdentity - AuthenticationCertificates: {| Name: ResourceName; Data: string |} list - AutoscaleConfiguration: {| MaxCapacity: int option - MinCapacity: int |} option - FrontendPorts: {| Name: ResourceName; Port: uint16 |} list - FrontendIpConfigs: {| Name: ResourceName - PrivateIpAllocationMethod: PrivateIpAddress.AllocationMethod - PublicIp: ResourceId option |} list - BackendAddressPools: {| Name: ResourceName - Addresses: BackendAddress list |} list - BackendHttpSettingsCollection: {| Name: ResourceName - AffinityCookieName: string option - AuthenticationCertificates: ResourceName list - ConnectionDraining: {| DrainTimeoutInSeconds: int - Enabled: bool |} option - CookieBasedAffinity: FeatureFlag - HostName: string option - Path: string option - Port: uint16 - Protocol: Protocol - PickHostNameFromBackendAddress: bool - RequestTimeoutInSeconds: int - Probe: ResourceName option - ProbeEnabled: bool - TrustedRootCertificates: ResourceName list |} list - CustomErrorConfigurations: {| CustomErrorPageUrl: string - StatusCode: HttpStatusCode |} list - EnableFips: bool option - EnableHttp2: bool option - FirewallPolicy: ResourceId option - ForceFirewallPolicyAssociation: bool - GatewayIPConfigurations: {| Name: ResourceName - Subnet: ResourceId option |} list - HttpListeners: {| Name: ResourceName - FrontendIpConfiguration: ResourceName - BackendAddressPool: ResourceName - CustomErrorConfigurations: {| CustomErrorPageUrl: string - StatusCode: HttpStatusCode |} list - FirewallPolicy: ResourceId option - FrontendPort: ResourceName - RequireServerNameIndication: bool - HostNames: string list - Protocol: Protocol - SslCertificate: ResourceName option - SslProfile: ResourceName option |} list - Probes: {| Name: ResourceName - Host: string - Port: uint16 option - Path: string - Protocol: Protocol - IntervalInSeconds: int - TimeoutInSeconds: int - UnhealthyThreshold: uint16 - PickHostNameFromBackendHttpSettings: bool - MinServers: uint16 option - Match: {| Body: string option - StatusCodes: uint16 list |} option |} list - RedirectConfigurations: {| Name: ResourceName - IncludePath: bool - IncludeQueryString: bool - PathRules: ResourceName list - RedirectType: RedirectType - RequestRoutingRules: ResourceName list - TargetListener: ResourceName - TargetUrl: string - UrlPathMaps: ResourceName list |} list - RequestRoutingRules: {| Name: ResourceName - RuleType: RuleType - HttpListener: ResourceName - BackendAddressPool: ResourceName - BackendHttpSettings: ResourceName - RedirectConfiguration: ResourceName option - RewriteRuleSet: ResourceName option - UrlPathMap: ResourceName option - Priority: int option |} list - RewriteRuleSets: {| Name: ResourceName - RewriteRules: {| ActionSet: {| RequestHeaderConfigurations: {| HeaderName: string - HeaderValue: string |} list - ResponseHeaderConfigurations: {| HeaderName: string - HeaderValue: string |} list - UrlConfiguration: {| ModifiedPath: string - ModifiedQueryString: string - Reroute: bool |} |} - Conditions: {| IgnoreCase: bool - Negate: bool - Pattern: string - Variable: string |} list - Name: string - RuleSequence: int |} list |} list - SslCertificates: {| Name: ResourceName - Data: string option - KeyVaultSecretId: string - Password: string option |} list - SslPolicy: {| CipherSuites: CipherSuite list - DisabledSslProtocols: SslProtocol list - MinProtocolVersion: SslProtocol - PolicyName: PolicyName - PolicyType: PolicyType |} option - SslProfiles: {| Name: ResourceName - ClientAuthConfiguration: {| VerifyClientCertIssuerDN: bool |} - SslPolicy: {| CipherSuites: CipherSuite list - DisabledSslProtocols: SslProtocol list - MinProtocolVersion: SslProtocol - PolicyName: PolicyName - PolicyType: PolicyType |} option - TrustedClientCertificates: ResourceName list |} list - TrustedClientCertificates: {| Name: ResourceName; Data: string |} list - TrustedRootCertificates: {| Name: ResourceName - Data: string option - KeyVaultSecretId: string |} list - UrlPathMaps: {| Name: ResourceName - DefaultBackendAddressPool: ResourceName - DefaultBackendHttpSettings: ResourceName - DefaultRedirectConfiguration: ResourceName - DefaultRewriteRuleSet: ResourceName - PathRules: {| Name: ResourceName - BackendAddressPool: ResourceName - BackendHttpSettings: ResourceName - FirewallPolicy: ResourceId - Paths: string list - RedirectConfiguration: ResourceName - RewriteRuleSet: ResourceName |} list |} list - WebApplicationFirewallConfiguration: {| DisabledRuleGroups: {| RuleGroupName: string - Rules: int list |} list - Enabled: bool - Exclusions: {| MatchVariable: string - Selector: string - SelectorMatchOperator: string |} list - FileUploadLimitInMb: int option - FirewallMode: FirewallMode option - MaxRequestBodySize: int option - MaxRequestBodySizeInKb: int option - RequestBodyCheck: bool option - RuleSetType: RuleSetType - RuleSetVersion: string |} option - Zones: uint16 list - Dependencies: Set - Tags: Map - } +type ApplicationGateway = { + Name: ResourceName + Location: Location + Sku: ApplicationGatewaySku + Identity: ManagedIdentity + AuthenticationCertificates: {| Name: ResourceName; Data: string |} list + AutoscaleConfiguration: + {| + MaxCapacity: int option + MinCapacity: int + |} option + FrontendPorts: {| Name: ResourceName; Port: uint16 |} list + FrontendIpConfigs: + {| + Name: ResourceName + PrivateIpAllocationMethod: PrivateIpAddress.AllocationMethod + PublicIp: ResourceId option + |} list + BackendAddressPools: + {| + Name: ResourceName + Addresses: BackendAddress list + |} list + BackendHttpSettingsCollection: + {| + Name: ResourceName + AffinityCookieName: string option + AuthenticationCertificates: ResourceName list + ConnectionDraining: + {| + DrainTimeoutInSeconds: int + Enabled: bool + |} option + CookieBasedAffinity: FeatureFlag + HostName: string option + Path: string option + Port: uint16 + Protocol: Protocol + PickHostNameFromBackendAddress: bool + RequestTimeoutInSeconds: int + Probe: ResourceName option + ProbeEnabled: bool + TrustedRootCertificates: ResourceName list + |} list + CustomErrorConfigurations: + {| + CustomErrorPageUrl: string + StatusCode: HttpStatusCode + |} list + EnableFips: bool option + EnableHttp2: bool option + FirewallPolicy: ResourceId option + ForceFirewallPolicyAssociation: bool + GatewayIPConfigurations: + {| + Name: ResourceName + Subnet: ResourceId option + |} list + HttpListeners: + {| + Name: ResourceName + FrontendIpConfiguration: ResourceName + BackendAddressPool: ResourceName + CustomErrorConfigurations: + {| + CustomErrorPageUrl: string + StatusCode: HttpStatusCode + |} list + FirewallPolicy: ResourceId option + FrontendPort: ResourceName + RequireServerNameIndication: bool + HostNames: string list + Protocol: Protocol + SslCertificate: ResourceName option + SslProfile: ResourceName option + |} list + Probes: + {| + Name: ResourceName + Host: string + Port: uint16 option + Path: string + Protocol: Protocol + IntervalInSeconds: int + TimeoutInSeconds: int + UnhealthyThreshold: uint16 + PickHostNameFromBackendHttpSettings: bool + MinServers: uint16 option + Match: + {| + Body: string option + StatusCodes: uint16 list + |} option + |} list + RedirectConfigurations: + {| + Name: ResourceName + IncludePath: bool + IncludeQueryString: bool + PathRules: ResourceName list + RedirectType: RedirectType + RequestRoutingRules: ResourceName list + TargetListener: ResourceName + TargetUrl: string + UrlPathMaps: ResourceName list + |} list + RequestRoutingRules: + {| + Name: ResourceName + RuleType: RuleType + HttpListener: ResourceName + BackendAddressPool: ResourceName + BackendHttpSettings: ResourceName + RedirectConfiguration: ResourceName option + RewriteRuleSet: ResourceName option + UrlPathMap: ResourceName option + Priority: int option + |} list + RewriteRuleSets: + {| + Name: ResourceName + RewriteRules: + {| + ActionSet: + {| + RequestHeaderConfigurations: + {| + HeaderName: string + HeaderValue: string + |} list + ResponseHeaderConfigurations: + {| + HeaderName: string + HeaderValue: string + |} list + UrlConfiguration: + {| + ModifiedPath: string + ModifiedQueryString: string + Reroute: bool + |} + |} + Conditions: + {| + IgnoreCase: bool + Negate: bool + Pattern: string + Variable: string + |} list + Name: string + RuleSequence: int + |} list + |} list + SslCertificates: + {| + Name: ResourceName + Data: string option + KeyVaultSecretId: string + Password: string option + |} list + SslPolicy: + {| + CipherSuites: CipherSuite list + DisabledSslProtocols: SslProtocol list + MinProtocolVersion: SslProtocol + PolicyName: PolicyName + PolicyType: PolicyType + |} option + SslProfiles: + {| + Name: ResourceName + ClientAuthConfiguration: {| VerifyClientCertIssuerDN: bool |} + SslPolicy: + {| + CipherSuites: CipherSuite list + DisabledSslProtocols: SslProtocol list + MinProtocolVersion: SslProtocol + PolicyName: PolicyName + PolicyType: PolicyType + |} option + TrustedClientCertificates: ResourceName list + |} list + TrustedClientCertificates: {| Name: ResourceName; Data: string |} list + TrustedRootCertificates: + {| + Name: ResourceName + Data: string option + KeyVaultSecretId: string + |} list + UrlPathMaps: + {| + Name: ResourceName + DefaultBackendAddressPool: ResourceName + DefaultBackendHttpSettings: ResourceName + DefaultRedirectConfiguration: ResourceName + DefaultRewriteRuleSet: ResourceName + PathRules: + {| + Name: ResourceName + BackendAddressPool: ResourceName + BackendHttpSettings: ResourceName + FirewallPolicy: ResourceId + Paths: string list + RedirectConfiguration: ResourceName + RewriteRuleSet: ResourceName + |} list + |} list + WebApplicationFirewallConfiguration: + {| + DisabledRuleGroups: + {| + RuleGroupName: string + Rules: int list + |} list + Enabled: bool + Exclusions: + {| + MatchVariable: string + Selector: string + SelectorMatchOperator: string + |} list + FileUploadLimitInMb: int option + FirewallMode: FirewallMode option + MaxRequestBodySize: int option + MaxRequestBodySizeInKb: int option + RequestBodyCheck: bool option + RuleSetType: RuleSetType + RuleSetVersion: string + |} option + Zones: uint16 list + Dependencies: Set + Tags: Map +} with member private this.dependencies = [ @@ -211,535 +300,469 @@ type ApplicationGateway = interface IArmResource with member this.ResourceId = applicationGateways.resourceId this.Name - member this.JsonModel = - {| applicationGateways.Create(this.Name, this.Location, this.dependencies, this.Tags) with + member this.JsonModel = {| + applicationGateways.Create(this.Name, this.Location, this.dependencies, this.Tags) with identity = this.Identity.ToArmJson - properties = - {| - sku = - {| - name = this.Sku.Name.ArmValue - capacity = this.Sku.Capacity |> Option.toNullable - tier = this.Sku.Tier.ArmValue + properties = {| + sku = {| + name = this.Sku.Name.ArmValue + capacity = this.Sku.Capacity |> Option.toNullable + tier = this.Sku.Tier.ArmValue + |} + autoscaleConfiguration = + this.AutoscaleConfiguration + |> Option.map (fun a -> {| + maxCapacity = a.MaxCapacity + minCapacity = a.MinCapacity + |}) + |> Option.defaultValue Unchecked.defaultof<_> + backendAddressPools = + this.BackendAddressPools + |> List.map (fun backend -> {| + name = backend.Name.Value + properties = {| + backendAddresses = + backend.Addresses + |> List.map (function + | BackendAddress.Ip ip -> {| fqdn = null; ipAddress = string ip |} + | BackendAddress.Fqdn fqdn -> {| fqdn = fqdn; ipAddress = null |}) |} - autoscaleConfiguration = - this.AutoscaleConfiguration - |> Option.map (fun a -> - {| - maxCapacity = a.MaxCapacity - minCapacity = a.MinCapacity - |}) - |> Option.defaultValue Unchecked.defaultof<_> - backendAddressPools = - this.BackendAddressPools - |> List.map (fun backend -> - {| - name = backend.Name.Value - properties = - {| - backendAddresses = - backend.Addresses - |> List.map (function - | BackendAddress.Ip ip -> {| fqdn = null; ipAddress = string ip |} - | BackendAddress.Fqdn fqdn -> {| fqdn = fqdn; ipAddress = null |}) - |} - |}) - backendHttpSettingsCollection = - this.BackendHttpSettingsCollection - |> List.map (fun settings -> - {| - name = settings.Name.Value - properties = - {| - affinityCookieName = settings.AffinityCookieName |> Option.toObj - authenticationCertificates = - settings.AuthenticationCertificates - |> List.map ( - tuple this.Name - >> applicationGatewayAuthenticationCertificates.resourceId - >> ResourceId.AsIdObject - ) - connectionDraining = - settings.ConnectionDraining - |> Option.map (fun drain -> - {| - drainTimeoutInSec = drain.DrainTimeoutInSeconds - enabled = drain.Enabled - |}) - cookieBasedAffinity = settings.CookieBasedAffinity.ArmValue - hostName = settings.HostName |> Option.toObj - path = settings.Path |> Option.toObj - pickHostNameFromBackendAddress = settings.PickHostNameFromBackendAddress - port = settings.Port - probe = - settings.Probe - |> Option.map ( - tuple this.Name - >> ApplicationGatewayProbes.resourceId - >> ResourceId.AsIdObject - ) - |> Option.defaultValue Unchecked.defaultof<_> - probeEnabled = settings.ProbeEnabled - protocol = settings.Protocol.ArmValue - requestTimeout = settings.RequestTimeoutInSeconds - trustedRootCertificates = - settings.TrustedRootCertificates - |> List.map ( - tuple this.Name - >> applicationGatewayTrustedRootCertificates.resourceId - >> ResourceId.AsIdObject - ) - |} - |}) - customErrorConfigurations = - this.CustomErrorConfigurations - |> List.map (fun conf -> - {| - customErrorPageUrl = conf.CustomErrorPageUrl - statusCode = conf.StatusCode.ArmValue - |}) - enableFips = this.EnableFips |> Option.toNullable - enableHttp2 = this.EnableHttp2 |> Option.toNullable - firewallPolicy = - this.FirewallPolicy - |> Option.map ResourceId.AsIdObject - |> Option.defaultValue Unchecked.defaultof<_> - frontendPorts = - this.FrontendPorts - |> List.map (fun frontend -> - {| - name = frontend.Name.Value - properties = {| port = frontend.Port |} - |}) - gatewayIPConfigurations = - this.GatewayIPConfigurations - |> List.map (fun gwip -> - {| - name = gwip.Name.Value - properties = - {| - subnet = - gwip.Subnet - |> Option.map ResourceId.AsIdObject - |> Option.defaultValue Unchecked.defaultof<_> - |} - |}) - httpListeners = - this.HttpListeners - |> List.map (fun listener -> - {| - name = listener.Name.Value - properties = - {| - customErrorConfigurations = - listener.CustomErrorConfigurations - |> List.map (fun cfg -> - {| - customErrorPageUrl = cfg.CustomErrorPageUrl - statusCode = cfg.StatusCode.ArmValue - |}) - firewallPolicy = - listener.FirewallPolicy - |> Option.map ResourceId.AsIdObject - |> Option.defaultValue Unchecked.defaultof<_> - frontendIPConfiguration = - (this.Name, listener.FrontendIpConfiguration) - |> applicationGatewayFrontendIPConfigurations.resourceId - |> ResourceId.AsIdObject - frontendPort = - (this.Name, listener.FrontendPort) - |> applicationGatewayFrontendPorts.resourceId - |> ResourceId.AsIdObject - hostNames = listener.HostNames - protocol = listener.Protocol.ArmValue - requireServerNameIndication = listener.RequireServerNameIndication - sslCertificate = - listener.SslCertificate - |> Option.map ( - tuple this.Name - >> applicationGatewaySslCertificates.resourceId - >> ResourceId.AsIdObject - ) - |> Option.defaultValue Unchecked.defaultof<_> - sslProfile = - listener.SslProfile - |> Option.map ( - tuple this.Name - >> applicationGatewaySslProfiles.resourceId - >> ResourceId.AsIdObject - ) - |> Option.defaultValue Unchecked.defaultof<_> - |} - |}) - frontendIPConfigurations = - this.FrontendIpConfigs - |> List.map (fun frontend -> - let allocationMethod, ip = - match frontend.PrivateIpAllocationMethod with - | PrivateIpAddress.DynamicPrivateIp -> "Dynamic", null - | PrivateIpAddress.StaticPrivateIp ip -> "Static", string ip + |}) + backendHttpSettingsCollection = + this.BackendHttpSettingsCollection + |> List.map (fun settings -> {| + name = settings.Name.Value + properties = {| + affinityCookieName = settings.AffinityCookieName |> Option.toObj + authenticationCertificates = + settings.AuthenticationCertificates + |> List.map ( + tuple this.Name + >> applicationGatewayAuthenticationCertificates.resourceId + >> ResourceId.AsIdObject + ) + connectionDraining = + settings.ConnectionDraining + |> Option.map (fun drain -> {| + drainTimeoutInSec = drain.DrainTimeoutInSeconds + enabled = drain.Enabled + |}) + cookieBasedAffinity = settings.CookieBasedAffinity.ArmValue + hostName = settings.HostName |> Option.toObj + path = settings.Path |> Option.toObj + pickHostNameFromBackendAddress = settings.PickHostNameFromBackendAddress + port = settings.Port + probe = + settings.Probe + |> Option.map ( + tuple this.Name >> ApplicationGatewayProbes.resourceId >> ResourceId.AsIdObject + ) + |> Option.defaultValue Unchecked.defaultof<_> + probeEnabled = settings.ProbeEnabled + protocol = settings.Protocol.ArmValue + requestTimeout = settings.RequestTimeoutInSeconds + trustedRootCertificates = + settings.TrustedRootCertificates + |> List.map ( + tuple this.Name + >> applicationGatewayTrustedRootCertificates.resourceId + >> ResourceId.AsIdObject + ) + |} + |}) + customErrorConfigurations = + this.CustomErrorConfigurations + |> List.map (fun conf -> {| + customErrorPageUrl = conf.CustomErrorPageUrl + statusCode = conf.StatusCode.ArmValue + |}) + enableFips = this.EnableFips |> Option.toNullable + enableHttp2 = this.EnableHttp2 |> Option.toNullable + firewallPolicy = + this.FirewallPolicy + |> Option.map ResourceId.AsIdObject + |> Option.defaultValue Unchecked.defaultof<_> + frontendPorts = + this.FrontendPorts + |> List.map (fun frontend -> {| + name = frontend.Name.Value + properties = {| port = frontend.Port |} + |}) + gatewayIPConfigurations = + this.GatewayIPConfigurations + |> List.map (fun gwip -> {| + name = gwip.Name.Value + properties = {| + subnet = + gwip.Subnet + |> Option.map ResourceId.AsIdObject + |> Option.defaultValue Unchecked.defaultof<_> + |} + |}) + httpListeners = + this.HttpListeners + |> List.map (fun listener -> {| + name = listener.Name.Value + properties = {| + customErrorConfigurations = + listener.CustomErrorConfigurations + |> List.map (fun cfg -> {| + customErrorPageUrl = cfg.CustomErrorPageUrl + statusCode = cfg.StatusCode.ArmValue + |}) + firewallPolicy = + listener.FirewallPolicy + |> Option.map ResourceId.AsIdObject + |> Option.defaultValue Unchecked.defaultof<_> + frontendIPConfiguration = + (this.Name, listener.FrontendIpConfiguration) + |> applicationGatewayFrontendIPConfigurations.resourceId + |> ResourceId.AsIdObject + frontendPort = + (this.Name, listener.FrontendPort) + |> applicationGatewayFrontendPorts.resourceId + |> ResourceId.AsIdObject + hostNames = listener.HostNames + protocol = listener.Protocol.ArmValue + requireServerNameIndication = listener.RequireServerNameIndication + sslCertificate = + listener.SslCertificate + |> Option.map ( + tuple this.Name + >> applicationGatewaySslCertificates.resourceId + >> ResourceId.AsIdObject + ) + |> Option.defaultValue Unchecked.defaultof<_> + sslProfile = + listener.SslProfile + |> Option.map ( + tuple this.Name + >> applicationGatewaySslProfiles.resourceId + >> ResourceId.AsIdObject + ) + |> Option.defaultValue Unchecked.defaultof<_> + |} + |}) + frontendIPConfigurations = + this.FrontendIpConfigs + |> List.map (fun frontend -> + let allocationMethod, ip = + match frontend.PrivateIpAllocationMethod with + | PrivateIpAddress.DynamicPrivateIp -> "Dynamic", null + | PrivateIpAddress.StaticPrivateIp ip -> "Static", string ip - {| - name = frontend.Name.Value - properties = - {| - privateIPAllocationMethod = allocationMethod - privateIPAddress = ip - publicIPAddress = - frontend.PublicIp - |> Option.map (fun pip -> {| id = pip.Eval() |}) - |> Option.defaultValue Unchecked.defaultof<_> - |} - |}) - probes = - this.Probes - |> List.map (fun probe -> - {| - name = probe.Name.Value - properties = - {| - host = probe.Host - port = probe.Port |> Option.toNullable - path = probe.Path - protocol = probe.Protocol.ArmValue - pickHostNameFromBackendHttpSettings = - probe.PickHostNameFromBackendHttpSettings - ``match`` = - probe.Match - |> Option.map (fun m -> - {| - body = m.Body |> Option.toObj - statusCodes = m.StatusCodes |> List.map string - |}) - minServers = probe.MinServers |> Option.toNullable - interval = probe.IntervalInSeconds - timeout = probe.TimeoutInSeconds - unhealthyThreshold = probe.UnhealthyThreshold - |} - |}) - redirectConfigurations = - this.RedirectConfigurations - |> List.map (fun cfg -> - {| - name = cfg.Name.Value - properties = - {| - includePath = cfg.IncludePath - includeQueryString = cfg.IncludeQueryString - pathRules = - cfg.PathRules - |> List.map ( - tuple this.Name - >> applicationGatewayPathRules.resourceId - >> ResourceId.AsIdObject - ) - redirectType = cfg.RedirectType.ArmValue - requestRoutingRules = - cfg.RequestRoutingRules - |> List.map ( - tuple this.Name - >> applicationGatewayRequestRoutingRules.resourceId - >> ResourceId.AsIdObject - ) - targetListener = - applicationGatewayHttpListeners.resourceId ( - this.Name, - cfg.TargetListener - ) - |> ResourceId.AsIdObject - targetUrl = cfg.TargetUrl - urlPathMaps = - cfg.UrlPathMaps - |> List.map ( - tuple this.Name - >> applicationGatewayUrlPathMaps.resourceId - >> ResourceId.AsIdObject - ) + {| + name = frontend.Name.Value + properties = {| + privateIPAllocationMethod = allocationMethod + privateIPAddress = ip + publicIPAddress = + frontend.PublicIp + |> Option.map (fun pip -> {| id = pip.Eval() |}) + |> Option.defaultValue Unchecked.defaultof<_> + |} + |}) + probes = + this.Probes + |> List.map (fun probe -> {| + name = probe.Name.Value + properties = {| + host = probe.Host + port = probe.Port |> Option.toNullable + path = probe.Path + protocol = probe.Protocol.ArmValue + pickHostNameFromBackendHttpSettings = probe.PickHostNameFromBackendHttpSettings + ``match`` = + probe.Match + |> Option.map (fun m -> {| + body = m.Body |> Option.toObj + statusCodes = m.StatusCodes |> List.map string + |}) + minServers = probe.MinServers |> Option.toNullable + interval = probe.IntervalInSeconds + timeout = probe.TimeoutInSeconds + unhealthyThreshold = probe.UnhealthyThreshold + |} + |}) + redirectConfigurations = + this.RedirectConfigurations + |> List.map (fun cfg -> {| + name = cfg.Name.Value + properties = {| + includePath = cfg.IncludePath + includeQueryString = cfg.IncludeQueryString + pathRules = + cfg.PathRules + |> List.map ( + tuple this.Name + >> applicationGatewayPathRules.resourceId + >> ResourceId.AsIdObject + ) + redirectType = cfg.RedirectType.ArmValue + requestRoutingRules = + cfg.RequestRoutingRules + |> List.map ( + tuple this.Name + >> applicationGatewayRequestRoutingRules.resourceId + >> ResourceId.AsIdObject + ) + targetListener = + applicationGatewayHttpListeners.resourceId (this.Name, cfg.TargetListener) + |> ResourceId.AsIdObject + targetUrl = cfg.TargetUrl + urlPathMaps = + cfg.UrlPathMaps + |> List.map ( + tuple this.Name + >> applicationGatewayUrlPathMaps.resourceId + >> ResourceId.AsIdObject + ) + |} + |}) + requestRoutingRules = + this.RequestRoutingRules + |> List.map (fun routingRule -> {| + name = routingRule.Name.Value + properties = {| + backendAddressPool = + applicationGatewayBackendAddressPools.resourceId ( + this.Name, + routingRule.BackendAddressPool + ) + |> ResourceId.AsIdObject + backendHttpSettings = + applicationGatewayBackendHttpSettingsCollection.resourceId ( + this.Name, + routingRule.BackendHttpSettings + ) + |> ResourceId.AsIdObject + httpListener = + applicationGatewayHttpListeners.resourceId (this.Name, routingRule.HttpListener) + |> ResourceId.AsIdObject + priority = routingRule.Priority |> Option.toNullable + redirectConfiguration = + routingRule.RedirectConfiguration + |> Option.map ( + tuple this.Name + >> applicationGatewayRedirectConfigurations.resourceId + >> ResourceId.AsIdObject + ) + |> Option.defaultValue Unchecked.defaultof<_> + rewriteRuleSet = + routingRule.RewriteRuleSet + |> Option.map ( + tuple this.Name + >> applicationGatewayRewriteRuleSets.resourceId + >> ResourceId.AsIdObject + ) + |> Option.defaultValue Unchecked.defaultof<_> + ruleType = routingRule.RuleType.ArmValue + urlPathMap = + routingRule.UrlPathMap + |> Option.map ( + tuple this.Name + >> applicationGatewayUrlPathMaps.resourceId + >> ResourceId.AsIdObject + ) + |> Option.defaultValue Unchecked.defaultof<_> + |} + |}) + rewriteRuleSets = + this.RewriteRuleSets + |> List.map (fun ruleSet -> {| + name = ruleSet.Name.Value + properties = {| + rewriteRules = + ruleSet.RewriteRules + |> List.map (fun rule -> {| + actionSet = {| + requestHeaderConfigurations = + rule.ActionSet.RequestHeaderConfigurations + |> List.map (fun cfg -> {| + headerName = cfg.HeaderName + headerValue = cfg.HeaderValue + |}) + responseHeaderConfigurations = + rule.ActionSet.ResponseHeaderConfigurations + |> List.map (fun cfg -> {| + headerName = cfg.HeaderName + headerValue = cfg.HeaderValue + |}) + urlConfiguration = {| + modifiedPath = rule.ActionSet.UrlConfiguration.ModifiedPath + modifiedQueryString = + rule.ActionSet.UrlConfiguration.ModifiedQueryString + reroute = rule.ActionSet.UrlConfiguration.Reroute + |} |} - |}) - requestRoutingRules = - this.RequestRoutingRules - |> List.map (fun routingRule -> - {| - name = routingRule.Name.Value - properties = - {| + conditions = + rule.Conditions + |> List.map (fun c -> {| + ignoreCase = c.IgnoreCase + negate = c.Negate + pattern = c.Pattern + variable = c.Variable + |}) + name = rule.Name + ruleSequence = rule.RuleSequence + |}) + |} + |}) + sslCertificates = + this.SslCertificates + |> List.map (fun cert -> {| + name = cert.Name.Value + properties = {| + data = cert.Data |> Option.toObj + keyVaultSecretId = cert.KeyVaultSecretId + password = cert.Password |> Option.toObj + |} + |}) + sslPolicy = + this.SslPolicy + |> Option.map (fun sslPolicy -> {| + cipherSuites = sslPolicy.CipherSuites |> List.map CipherSuite.toString + disabledSslProtocols = sslPolicy.DisabledSslProtocols |> List.map SslProtocol.toString + minProtocolVersion = sslPolicy.MinProtocolVersion.ArmValue + policyName = sslPolicy.PolicyName.ArmValue + policyType = sslPolicy.PolicyType.ArmValue + |}) + |> Option.defaultValue Unchecked.defaultof<_> + sslProfiles = + this.SslProfiles + |> List.map (fun sslProfile -> {| + name = sslProfile.Name.Value + properties = {| + clientAuthConfiguration = {| + verifyClientCertIssuerDN = + sslProfile.ClientAuthConfiguration.VerifyClientCertIssuerDN + |} + sslPolicy = + sslProfile.SslPolicy + |> Option.map (fun sslPolicy -> {| + cipherSuites = sslPolicy.CipherSuites |> List.map CipherSuite.toString + disabledSslProtocols = + sslPolicy.DisabledSslProtocols |> List.map SslProtocol.toString + minProtocolVersion = sslPolicy.MinProtocolVersion.ArmValue + policyName = sslPolicy.PolicyName.ArmValue + policyType = sslPolicy.PolicyType.ArmValue + |}) + |> Option.defaultValue Unchecked.defaultof<_> + trustedClientCertificates = + sslProfile.TrustedClientCertificates + |> List.map ( + tuple this.Name + >> applicationGatewayTrustedRootCertificates.resourceId + >> ResourceId.AsIdObject + ) + |} + |}) + trustedClientCertificates = + this.TrustedClientCertificates + |> List.map (fun cert -> {| + name = cert.Name.Value + properties = {| data = cert.Data |} + |}) + trustedRootCertificates = + this.TrustedRootCertificates + |> List.map (fun cert -> {| + name = cert.Name.Value + properties = {| + data = cert.Data |> Option.toObj + keyVaultSecretId = cert.KeyVaultSecretId + |} + |}) + urlPathMaps = + this.UrlPathMaps + |> List.map (fun pathMap -> {| + name = pathMap.Name.Value + properties = {| + defaultBackendAddressPool = + applicationGatewayBackendAddressPools.resourceId ( + this.Name, + pathMap.DefaultBackendAddressPool + ) + |> ResourceId.AsIdObject + defaultBackendHttpSettings = + applicationGatewayBackendHttpSettingsCollection.resourceId ( + this.Name, + pathMap.DefaultBackendHttpSettings + ) + |> ResourceId.AsIdObject + defaultRedirectConfiguration = + applicationGatewayRedirectConfigurations.resourceId ( + this.Name, + pathMap.DefaultRedirectConfiguration + ) + |> ResourceId.AsIdObject + defaultRewriteRuleSet = + applicationGatewayRewriteRuleSets.resourceId ( + this.Name, + pathMap.DefaultRewriteRuleSet + ) + |> ResourceId.AsIdObject + pathRules = + pathMap.PathRules + |> List.map (fun pathRule -> {| + name = pathRule.Name.Value + properties = {| backendAddressPool = applicationGatewayBackendAddressPools.resourceId ( this.Name, - routingRule.BackendAddressPool + pathRule.BackendAddressPool ) |> ResourceId.AsIdObject backendHttpSettings = applicationGatewayBackendHttpSettingsCollection.resourceId ( this.Name, - routingRule.BackendHttpSettings - ) - |> ResourceId.AsIdObject - httpListener = - applicationGatewayHttpListeners.resourceId ( - this.Name, - routingRule.HttpListener + pathRule.BackendHttpSettings ) |> ResourceId.AsIdObject - priority = routingRule.Priority |> Option.toNullable + firewallPolicy = pathRule.FirewallPolicy |> ResourceId.AsIdObject redirectConfiguration = - routingRule.RedirectConfiguration - |> Option.map ( - tuple this.Name - >> applicationGatewayRedirectConfigurations.resourceId - >> ResourceId.AsIdObject - ) - |> Option.defaultValue Unchecked.defaultof<_> - rewriteRuleSet = - routingRule.RewriteRuleSet - |> Option.map ( - tuple this.Name - >> applicationGatewayRewriteRuleSets.resourceId - >> ResourceId.AsIdObject - ) - |> Option.defaultValue Unchecked.defaultof<_> - ruleType = routingRule.RuleType.ArmValue - urlPathMap = - routingRule.UrlPathMap - |> Option.map ( - tuple this.Name - >> applicationGatewayUrlPathMaps.resourceId - >> ResourceId.AsIdObject - ) - |> Option.defaultValue Unchecked.defaultof<_> - |} - |}) - rewriteRuleSets = - this.RewriteRuleSets - |> List.map (fun ruleSet -> - {| - name = ruleSet.Name.Value - properties = - {| - rewriteRules = - ruleSet.RewriteRules - |> List.map (fun rule -> - {| - actionSet = - {| - requestHeaderConfigurations = - rule.ActionSet.RequestHeaderConfigurations - |> List.map (fun cfg -> - {| - headerName = cfg.HeaderName - headerValue = cfg.HeaderValue - |}) - responseHeaderConfigurations = - rule.ActionSet.ResponseHeaderConfigurations - |> List.map (fun cfg -> - {| - headerName = cfg.HeaderName - headerValue = cfg.HeaderValue - |}) - urlConfiguration = - {| - modifiedPath = - rule.ActionSet.UrlConfiguration.ModifiedPath - modifiedQueryString = - rule.ActionSet.UrlConfiguration.ModifiedQueryString - reroute = - rule.ActionSet.UrlConfiguration.Reroute - |} - |} - conditions = - rule.Conditions - |> List.map (fun c -> - {| - ignoreCase = c.IgnoreCase - negate = c.Negate - pattern = c.Pattern - variable = c.Variable - |}) - name = rule.Name - ruleSequence = rule.RuleSequence - |}) - |} - |}) - sslCertificates = - this.SslCertificates - |> List.map (fun cert -> - {| - name = cert.Name.Value - properties = - {| - data = cert.Data |> Option.toObj - keyVaultSecretId = cert.KeyVaultSecretId - password = cert.Password |> Option.toObj - |} - |}) - sslPolicy = - this.SslPolicy - |> Option.map (fun sslPolicy -> - {| - cipherSuites = sslPolicy.CipherSuites |> List.map CipherSuite.toString - disabledSslProtocols = - sslPolicy.DisabledSslProtocols |> List.map SslProtocol.toString - minProtocolVersion = sslPolicy.MinProtocolVersion.ArmValue - policyName = sslPolicy.PolicyName.ArmValue - policyType = sslPolicy.PolicyType.ArmValue - |}) - |> Option.defaultValue Unchecked.defaultof<_> - sslProfiles = - this.SslProfiles - |> List.map (fun sslProfile -> - {| - name = sslProfile.Name.Value - properties = - {| - clientAuthConfiguration = - {| - verifyClientCertIssuerDN = - sslProfile.ClientAuthConfiguration.VerifyClientCertIssuerDN - |} - sslPolicy = - sslProfile.SslPolicy - |> Option.map (fun sslPolicy -> - {| - cipherSuites = - sslPolicy.CipherSuites |> List.map CipherSuite.toString - disabledSslProtocols = - sslPolicy.DisabledSslProtocols - |> List.map SslProtocol.toString - minProtocolVersion = sslPolicy.MinProtocolVersion.ArmValue - policyName = sslPolicy.PolicyName.ArmValue - policyType = sslPolicy.PolicyType.ArmValue - |}) - |> Option.defaultValue Unchecked.defaultof<_> - trustedClientCertificates = - sslProfile.TrustedClientCertificates - |> List.map ( - tuple this.Name - >> applicationGatewayTrustedRootCertificates.resourceId - >> ResourceId.AsIdObject - ) - |} - |}) - trustedClientCertificates = - this.TrustedClientCertificates - |> List.map (fun cert -> - {| - name = cert.Name.Value - properties = {| data = cert.Data |} - |}) - trustedRootCertificates = - this.TrustedRootCertificates - |> List.map (fun cert -> - {| - name = cert.Name.Value - properties = - {| - data = cert.Data |> Option.toObj - keyVaultSecretId = cert.KeyVaultSecretId - |} - |}) - urlPathMaps = - this.UrlPathMaps - |> List.map (fun pathMap -> - {| - name = pathMap.Name.Value - properties = - {| - defaultBackendAddressPool = - applicationGatewayBackendAddressPools.resourceId ( - this.Name, - pathMap.DefaultBackendAddressPool - ) - |> ResourceId.AsIdObject - defaultBackendHttpSettings = - applicationGatewayBackendHttpSettingsCollection.resourceId ( - this.Name, - pathMap.DefaultBackendHttpSettings - ) - |> ResourceId.AsIdObject - defaultRedirectConfiguration = applicationGatewayRedirectConfigurations.resourceId ( this.Name, - pathMap.DefaultRedirectConfiguration + pathRule.RedirectConfiguration ) |> ResourceId.AsIdObject - defaultRewriteRuleSet = + rewriteRuleSet = applicationGatewayRewriteRuleSets.resourceId ( this.Name, - pathMap.DefaultRewriteRuleSet + pathRule.RewriteRuleSet ) |> ResourceId.AsIdObject - pathRules = - pathMap.PathRules - |> List.map (fun pathRule -> - {| - name = pathRule.Name.Value - properties = - {| - backendAddressPool = - applicationGatewayBackendAddressPools.resourceId ( - this.Name, - pathRule.BackendAddressPool - ) - |> ResourceId.AsIdObject - backendHttpSettings = - applicationGatewayBackendHttpSettingsCollection - .resourceId ( - this.Name, - pathRule.BackendHttpSettings - ) - |> ResourceId.AsIdObject - firewallPolicy = - pathRule.FirewallPolicy |> ResourceId.AsIdObject - redirectConfiguration = - applicationGatewayRedirectConfigurations.resourceId ( - this.Name, - pathRule.RedirectConfiguration - ) - |> ResourceId.AsIdObject - rewriteRuleSet = - applicationGatewayRewriteRuleSets.resourceId ( - this.Name, - pathRule.RewriteRuleSet - ) - |> ResourceId.AsIdObject - paths = pathRule.Paths - |} - |}) + paths = pathRule.Paths |} + |}) + |} + |}) + webApplicationFirewallConfiguration = + this.WebApplicationFirewallConfiguration + |> Option.map (fun cfg -> {| + disabledRuleGroups = + cfg.DisabledRuleGroups + |> List.map (fun ruleGroup -> {| + ruleGroupName = ruleGroup.RuleGroupName + rules = ruleGroup.Rules |}) - webApplicationFirewallConfiguration = - this.WebApplicationFirewallConfiguration - |> Option.map (fun cfg -> - {| - disabledRuleGroups = - cfg.DisabledRuleGroups - |> List.map (fun ruleGroup -> - {| - ruleGroupName = ruleGroup.RuleGroupName - rules = ruleGroup.Rules - |}) - enabled = cfg.Enabled - exclusions = - cfg.Exclusions - |> List.map (fun e -> - {| - matchVariable = e.MatchVariable - selector = e.Selector - selectorMatchOperator = e.SelectorMatchOperator - |}) - fileUploadLimitInMb = cfg.FileUploadLimitInMb - firewallMode = cfg.FirewallMode |> Option.map FirewallMode.toString - maxRequestBodySize = - cfg.MaxRequestBodySize |> Option.defaultValue Unchecked.defaultof<_> - maxRequestBodySizeInKb = - cfg.MaxRequestBodySizeInKb |> Option.defaultValue Unchecked.defaultof<_> - requestBodyCheck = - cfg.RequestBodyCheck |> Option.defaultValue Unchecked.defaultof<_> - ruleSetType = cfg.RuleSetType.ArmValue - ruleSetVersion = cfg.RuleSetVersion + enabled = cfg.Enabled + exclusions = + cfg.Exclusions + |> List.map (fun e -> {| + matchVariable = e.MatchVariable + selector = e.Selector + selectorMatchOperator = e.SelectorMatchOperator |}) - |> Option.defaultValue Unchecked.defaultof<_> - |} + fileUploadLimitInMb = cfg.FileUploadLimitInMb + firewallMode = cfg.FirewallMode |> Option.map FirewallMode.toString + maxRequestBodySize = cfg.MaxRequestBodySize |> Option.defaultValue Unchecked.defaultof<_> + maxRequestBodySizeInKb = + cfg.MaxRequestBodySizeInKb |> Option.defaultValue Unchecked.defaultof<_> + requestBodyCheck = cfg.RequestBodyCheck |> Option.defaultValue Unchecked.defaultof<_> + ruleSetType = cfg.RuleSetType.ArmValue + ruleSetVersion = cfg.RuleSetVersion + |}) + |> Option.defaultValue Unchecked.defaultof<_> + |} zones = this.Zones |> List.map string - |} + |} diff --git a/src/Farmer/Arm/AvailabilityTest.fs b/src/Farmer/Arm/AvailabilityTest.fs index 0801dc248..c8ffefbb5 100644 --- a/src/Farmer/Arm/AvailabilityTest.fs +++ b/src/Farmer/Arm/AvailabilityTest.fs @@ -5,16 +5,15 @@ open Farmer let availabilitytest = ResourceType("Microsoft.Insights/webtests", "2015-05-01") -type AvailabilityTest = - { - Name: ResourceName - AppInsightsName: ResourceName - Timeout: int - VisitFrequency: int - Location: Location - Locations: AvailabilityTest.TestSiteLocation list - WebTest: AvailabilityTest.WebTestType option - } +type AvailabilityTest = { + Name: ResourceName + AppInsightsName: ResourceName + Timeout: int + VisitFrequency: int + Location: Location + Locations: AvailabilityTest.TestSiteLocation list + WebTest: AvailabilityTest.WebTestType option +} with interface IArmResource with member this.ResourceId = availabilitytest.resourceId this.Name @@ -70,12 +69,12 @@ type AvailabilityTest = """ - {| availabilitytest.Create(this.Name) with - location = this.Location.ArmValue - dependsOn = [ Farmer.ResourceId.create(components, this.AppInsightsName).Eval() ] - tags = Farmer.Serialization.ofJson ("{\"" + appInsightResource + "\": \"Resource\"}") - properties = - {| + {| + availabilitytest.Create(this.Name) with + location = this.Location.ArmValue + dependsOn = [ Farmer.ResourceId.create(components, this.AppInsightsName).Eval() ] + tags = Farmer.Serialization.ofJson ("{\"" + appInsightResource + "\": \"Resource\"}") + properties = {| SyntheticMonitorId = $"{this.Name.Value.ToLower()}-{this.AppInsightsName.Value.ToLower()}" Name = this.Name.Value @@ -86,16 +85,16 @@ type AvailabilityTest = RetryEnabled = true Locations = this.Locations - |> List.map (fun (AvailabilityTest.AvailabilityTestSite lo) -> - {| Id = lo.ArmValue |}) - Configuration = - {| - WebTest = - System.Text.RegularExpressions.Regex.Replace( - testString.Replace("\r\n", "").Replace("\n", ""), - @"\s+", - " " - ) - |} + |> List.map (fun (AvailabilityTest.AvailabilityTestSite lo) -> {| + Id = lo.ArmValue + |}) + Configuration = {| + WebTest = + System.Text.RegularExpressions.Regex.Replace( + testString.Replace("\r\n", "").Replace("\n", ""), + @"\s+", + " " + ) + |} |} |} diff --git a/src/Farmer/Arm/AzureFirewall.fs b/src/Farmer/Arm/AzureFirewall.fs index a917bba18..aba4ad621 100644 --- a/src/Farmer/Arm/AzureFirewall.fs +++ b/src/Farmer/Arm/AzureFirewall.fs @@ -12,79 +12,71 @@ let azureFirewalls = ResourceType("Microsoft.Network/azureFirewalls", "2020-07-0 let azureFirewallPolicies = ResourceType("Microsoft.Network/firewallPolicies", "2020-07-01") -type HubPublicIPAddresses = - { - Count: int - Addresses: IPAddress list - } +type HubPublicIPAddresses = { + Count: int + Addresses: IPAddress list +} with - member this.JsonModel = - {| - count = this.Count - addresses = this.Addresses |> List.map (fun x -> {| address = x.ToString() |}) - |} + member this.JsonModel = {| + count = this.Count + addresses = this.Addresses |> List.map (fun x -> {| address = x.ToString() |}) + |} -type HubIPAddresses = - { - PublicIPAddresses: HubPublicIPAddresses option - } +type HubIPAddresses = { + PublicIPAddresses: HubPublicIPAddresses option +} with - member this.JsonModel = - {| - publicIPs = - this.PublicIPAddresses - |> Option.map (fun x -> box x.JsonModel) - |> Option.defaultValue null - |} + member this.JsonModel = {| + publicIPs = + this.PublicIPAddresses + |> Option.map (fun x -> box x.JsonModel) + |> Option.defaultValue null + |} -type Sku = - { - Name: SkuName - Tier: SkuTier - } +type Sku = { + Name: SkuName + Tier: SkuTier +} with - member this.JsonModel = - {| - name = this.Name.ArmValue - tier = this.Tier.ArmValue - |} + member this.JsonModel = {| + name = this.Name.ArmValue + tier = this.Tier.ArmValue + |} -type AzureFirewall = - { - Name: ResourceName - Location: Location - Dependencies: ResourceId Set - FirewallPolicy: ResourceId option - VirtualHub: ResourceId option - HubIPAddresses: HubIPAddresses option - AvailabilityZones: string list - Sku: Sku - } +type AzureFirewall = { + Name: ResourceName + Location: Location + Dependencies: ResourceId Set + FirewallPolicy: ResourceId option + VirtualHub: ResourceId option + HubIPAddresses: HubIPAddresses option + AvailabilityZones: string list + Sku: Sku +} with interface IArmResource with member this.ResourceId = azureFirewalls.resourceId this.Name - member this.JsonModel = - {| azureFirewalls.Create(this.Name, this.Location, this.Dependencies) with - properties = - {| - sku = this.Sku.JsonModel - 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 - |} + member this.JsonModel = {| + azureFirewalls.Create(this.Name, this.Location, this.Dependencies) with + properties = {| + sku = this.Sku.JsonModel + 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/Bastion.fs b/src/Farmer/Arm/Bastion.fs index df25bcbdd..a8a025e75 100644 --- a/src/Farmer/Arm/Bastion.fs +++ b/src/Farmer/Arm/Bastion.fs @@ -6,51 +6,42 @@ open Farmer.Arm.Network let bastionHosts = ResourceType("Microsoft.Network/bastionHosts", "2020-05-01") -type BastionHost = - { - Name: ResourceName - Location: Location - VirtualNetwork: ResourceName - IpConfigs: {| PublicIpName: ResourceName |} list - Tags: Map - } +type BastionHost = { + Name: ResourceName + Location: Location + VirtualNetwork: ResourceName + IpConfigs: {| PublicIpName: ResourceName |} list + Tags: Map +} with interface IArmResource with member this.ResourceId = bastionHosts.resourceId this.Name member this.JsonModel = - let dependsOn = - [ - virtualNetworks.resourceId this.VirtualNetwork - for config in this.IpConfigs do - publicIPAddresses.resourceId config.PublicIpName - ] + let dependsOn = [ + virtualNetworks.resourceId this.VirtualNetwork + for config in this.IpConfigs do + publicIPAddresses.resourceId config.PublicIpName + ] - {| bastionHosts.Create(this.Name, this.Location, dependsOn, this.Tags) with - properties = - {| + {| + bastionHosts.Create(this.Name, this.Location, dependsOn, this.Tags) with + properties = {| ipConfigurations = this.IpConfigs - |> List.mapi (fun index ipConfig -> - {| - name = $"ipconfig{index + 1}" - properties = - {| - publicIPAddress = - {| - id = publicIPAddresses.resourceId(ipConfig.PublicIpName).Eval() - |} - subnet = - {| - id = - subnets - .resourceId( - this.VirtualNetwork, - ResourceName "AzureBastionSubnet" - ) - .Eval() - |} - |} - |}) + |> List.mapi (fun index ipConfig -> {| + name = $"ipconfig{index + 1}" + properties = {| + publicIPAddress = {| + id = publicIPAddresses.resourceId(ipConfig.PublicIpName).Eval() + |} + subnet = {| + id = + subnets + .resourceId(this.VirtualNetwork, ResourceName "AzureBastionSubnet") + .Eval() + |} + |} + |}) |} |} diff --git a/src/Farmer/Arm/BingSearch.fs b/src/Farmer/Arm/BingSearch.fs index 1539b9552..545cec07c 100644 --- a/src/Farmer/Arm/BingSearch.fs +++ b/src/Farmer/Arm/BingSearch.fs @@ -8,24 +8,22 @@ let accounts = ResourceType("Microsoft.Bing/accounts", "2020-06-10") [] let private kind = "Bing.Search.v7" -type Accounts = - { - Name: ResourceName - Location: Location - Sku: BingSearch.Sku - Tags: Map - Statistics: FeatureFlag - } +type Accounts = { + Name: ResourceName + Location: Location + Sku: BingSearch.Sku + Tags: Map + Statistics: FeatureFlag +} with interface IArmResource with member this.ResourceId = accounts.resourceId this.Name - member this.JsonModel = - {| accounts.Create(this.Name, this.Location, tags = this.Tags) with + member this.JsonModel = {| + accounts.Create(this.Name, this.Location, tags = this.Tags) with sku = {| name = string this.Sku |} kind = kind - properties = - {| - statisticsEnabled = this.Statistics.AsBoolean - |} - |} + properties = {| + statisticsEnabled = this.Statistics.AsBoolean + |} + |} diff --git a/src/Farmer/Arm/Cache.fs b/src/Farmer/Arm/Cache.fs index eb8426b21..9717a56f9 100644 --- a/src/Farmer/Arm/Cache.fs +++ b/src/Farmer/Arm/Cache.fs @@ -6,17 +6,16 @@ open Farmer.Redis let redis = ResourceType("Microsoft.Cache/Redis", "2018-03-01") -type Redis = - { - Name: ResourceName - Location: Location - Sku: {| Sku: Sku; Capacity: int |} - RedisConfiguration: Map - NonSslEnabled: bool option - ShardCount: int option - MinimumTlsVersion: TlsVersion option - Tags: Map - } +type Redis = { + Name: ResourceName + Location: Location + Sku: {| Sku: Sku; Capacity: int |} + RedisConfiguration: Map + NonSslEnabled: bool option + ShardCount: int option + MinimumTlsVersion: TlsVersion option + Tags: Map +} with member this.Family = match this.Sku.Sku with @@ -27,25 +26,23 @@ type Redis = interface IArmResource with member this.ResourceId = redis.resourceId this.Name - member this.JsonModel = - {| redis.Create(this.Name, this.Location, tags = this.Tags) with - properties = - {| - sku = - {| - name = string this.Sku.Sku - family = this.Family - capacity = this.Sku.Capacity - |} - enableNonSslPort = this.NonSslEnabled |> Option.toNullable - shardCount = this.ShardCount |> Option.toNullable - minimumTlsVersion = - this.MinimumTlsVersion - |> Option.map (function - | Tls10 -> "1.0" - | Tls11 -> "1.1" - | Tls12 -> "1.2") - |> Option.toObj - redisConfiguration = this.RedisConfiguration + member this.JsonModel = {| + redis.Create(this.Name, this.Location, tags = this.Tags) with + properties = {| + sku = {| + name = string this.Sku.Sku + family = this.Family + capacity = this.Sku.Capacity |} - |} + enableNonSslPort = this.NonSslEnabled |> Option.toNullable + shardCount = this.ShardCount |> Option.toNullable + minimumTlsVersion = + this.MinimumTlsVersion + |> Option.map (function + | Tls10 -> "1.0" + | Tls11 -> "1.1" + | Tls12 -> "1.2") + |> Option.toObj + redisConfiguration = this.RedisConfiguration + |} + |} diff --git a/src/Farmer/Arm/Cdn.fs b/src/Farmer/Arm/Cdn.fs index 82884aacf..37b171293 100644 --- a/src/Farmer/Arm/Cdn.fs +++ b/src/Farmer/Arm/Cdn.fs @@ -12,79 +12,107 @@ let endpoints = ResourceType("Microsoft.Cdn/profiles/endpoints", "2019-04-15") let customDomains = ResourceType("Microsoft.Cdn/profiles/endpoints/customDomains", "2019-04-15") -type Profile = - { - Name: ResourceName - Sku: Sku - Tags: Map - } +type Profile = { + Name: ResourceName + Sku: Sku + Tags: Map +} with interface IArmResource with member this.ResourceId = profiles.resourceId this.Name member this.JsonModel = - {| profiles.Create(this.Name, Location.Global, tags = this.Tags) with - sku = {| name = string this.Sku |} - properties = {| |} + {| + profiles.Create(this.Name, Location.Global, tags = this.Tags) with + sku = {| name = string this.Sku |} + properties = {| |} |} |> box module CdnRule = type Condition = | IsDevice of - {| Operator: EqualityOperator - DeviceType: DeviceType |} + {| + Operator: EqualityOperator + DeviceType: DeviceType + |} | HttpVersion of - {| Operator: EqualityOperator - HttpVersions: HttpVersion list |} + {| + Operator: EqualityOperator + HttpVersions: HttpVersion list + |} | RequestCookies of - {| CookiesName: string - Operator: ComparisonOperator - CookiesValue: string list - CaseTransform: CaseTransform |} + {| + CookiesName: string + Operator: ComparisonOperator + CookiesValue: string list + CaseTransform: CaseTransform + |} | PostArgument of - {| ArgumentName: string - Operator: ComparisonOperator - ArgumentValue: string list - CaseTransform: CaseTransform |} + {| + ArgumentName: string + Operator: ComparisonOperator + ArgumentValue: string list + CaseTransform: CaseTransform + |} | QueryString of - {| Operator: ComparisonOperator - QueryString: string list - CaseTransform: CaseTransform |} + {| + Operator: ComparisonOperator + QueryString: string list + CaseTransform: CaseTransform + |} | RemoteAddress of - {| Operator: RemoteAddressOperator - MatchValues: string list |} + {| + Operator: RemoteAddressOperator + MatchValues: string list + |} | RequestBody of - {| Operator: ComparisonOperator - RequestBody: string list - CaseTransform: CaseTransform |} + {| + Operator: ComparisonOperator + RequestBody: string list + CaseTransform: CaseTransform + |} | RequestHeader of - {| HeaderName: string - Operator: ComparisonOperator - HeaderValue: string list - CaseTransform: CaseTransform |} + {| + HeaderName: string + Operator: ComparisonOperator + HeaderValue: string list + CaseTransform: CaseTransform + |} | RequestMethod of - {| Operator: EqualityOperator - RequestMethod: RequestMethod |} + {| + Operator: EqualityOperator + RequestMethod: RequestMethod + |} | RequestProtocol of - {| Operator: EqualityOperator - Value: Protocol |} + {| + Operator: EqualityOperator + Value: Protocol + |} | RequestUrl of - {| Operator: ComparisonOperator - RequestUrl: string list - CaseTransform: CaseTransform |} + {| + Operator: ComparisonOperator + RequestUrl: string list + CaseTransform: CaseTransform + |} | UrlFileExtension of - {| Operator: ComparisonOperator - Extension: string list - CaseTransform: CaseTransform |} + {| + Operator: ComparisonOperator + Extension: string list + CaseTransform: CaseTransform + |} | UrlFileName of - {| Operator: ComparisonOperator - FileName: string list - CaseTransform: CaseTransform |} + {| + Operator: ComparisonOperator + FileName: string list + CaseTransform: CaseTransform + |} | UrlPath of - {| Operator: ComparisonOperator - Value: string list - CaseTransform: CaseTransform |} + {| + Operator: ComparisonOperator + Value: string list + CaseTransform: CaseTransform + |} member this.MapCondition ( @@ -256,38 +284,42 @@ module CdnRule = c.CaseTransform ) - type ModifyHeader = - { - Action: ModifyHeaderAction - HttpHeaderName: string - HttpHeaderValue: string - } + type ModifyHeader = { + Action: ModifyHeaderAction + HttpHeaderName: string + HttpHeaderValue: string + } type Action = | CacheExpiration of {| CacheBehaviour: CacheBehaviour |} | CacheKeyQueryString of - {| Behaviour: QueryStringCacheBehavior - Parameters: string |} + {| + Behaviour: QueryStringCacheBehavior + Parameters: string + |} | ModifyRequestHeader of ModifyHeader | ModifyResponseHeader of ModifyHeader | UrlRewrite of - {| SourcePattern: string - Destination: string - PreserveUnmatchedPath: bool |} + {| + SourcePattern: string + Destination: string + PreserveUnmatchedPath: bool + |} | UrlRedirect of - {| RedirectType: RedirectType - DestinationProtocol: UrlRedirectProtocol - Hostname: string option - Path: string option - QueryString: string option - Fragment: string option |} + {| + RedirectType: RedirectType + DestinationProtocol: UrlRedirectProtocol + Hostname: string option + Path: string option + QueryString: string option + Fragment: string option + |} member this.JsonModel = - let map (name: string) (dataType: string) (parameters: Map<_, obj>) = - {| - name = name - parameters = parameters.Add("@odata.type", dataType) - |} + let map (name: string) (dataType: string) (parameters: Map<_, obj>) = {| + name = name + parameters = parameters.Add("@odata.type", dataType) + |} let mapModifyHeader name (modifyHeader: ModifyHeader) = map @@ -344,51 +376,47 @@ module CdnRule = .Add("customHostname", a.Hostname |> Option.toObj) .Add("customFragment", a.Fragment |> Option.toObj)) -type Rule = - { - Name: ResourceName - Order: int - Conditions: CdnRule.Condition list - Actions: CdnRule.Action list - } +type Rule = { + Name: ResourceName + Order: int + Conditions: CdnRule.Condition list + Actions: CdnRule.Action list +} -type DeliveryPolicy = - { - Description: string - Rules: Rule list - } +type DeliveryPolicy = { + Description: string + Rules: Rule list +} module Profiles = - type Endpoint = - { - Name: ResourceName - Profile: ResourceName - Dependencies: ResourceId Set - CompressedContentTypes: string Set - QueryStringCachingBehaviour: QueryStringCachingBehaviour - Http: FeatureFlag - Https: FeatureFlag - Compression: FeatureFlag - Origin: ArmExpression - OptimizationType: OptimizationType - DeliveryPolicy: DeliveryPolicy - Tags: Map - } + type Endpoint = { + Name: ResourceName + Profile: ResourceName + Dependencies: ResourceId Set + CompressedContentTypes: string Set + QueryStringCachingBehaviour: QueryStringCachingBehaviour + Http: FeatureFlag + Https: FeatureFlag + Compression: FeatureFlag + Origin: ArmExpression + OptimizationType: OptimizationType + DeliveryPolicy: DeliveryPolicy + Tags: Map + } with interface IArmResource with member this.ResourceId = endpoints.resourceId (this.Profile / this.Name) member this.JsonModel = - let dependencies = - [ - profiles.resourceId this.Profile - yield! Option.toList this.Origin.Owner - yield! this.Dependencies - ] + let dependencies = [ + profiles.resourceId this.Profile + yield! Option.toList this.Origin.Owner + yield! this.Dependencies + ] - {| endpoints.Create(this.Profile / this.Name, Location.Global, dependencies, this.Tags) with - properties = - {| + {| + endpoints.Create(this.Profile / this.Name, Location.Global, dependencies, this.Tags) with + properties = {| originHostHeader = this.Origin.Eval() queryStringCachingBehavior = string this.QueryStringCachingBehaviour optimizationType = string this.OptimizationType @@ -396,46 +424,42 @@ module Profiles = isHttpsAllowed = this.Https.AsBoolean isCompressionEnabled = this.Compression.AsBoolean contentTypesToCompress = this.CompressedContentTypes - origins = - [ - {| - name = "origin" - properties = {| hostName = this.Origin.Eval() |} - |} - ] - deliveryPolicy = + origins = [ {| - description = this.DeliveryPolicy.Description - rules = - this.DeliveryPolicy.Rules - |> List.map (fun rule -> - {| - name = rule.Name.Value - order = rule.Order - conditions = rule.Conditions |> List.map (fun c -> c.JsonModel) - actions = rule.Actions |> List.map (fun a -> a.JsonModel) - |}) + name = "origin" + properties = {| hostName = this.Origin.Eval() |} |} + ] + deliveryPolicy = {| + description = this.DeliveryPolicy.Description + rules = + this.DeliveryPolicy.Rules + |> List.map (fun rule -> {| + name = rule.Name.Value + order = rule.Order + conditions = rule.Conditions |> List.map (fun c -> c.JsonModel) + actions = rule.Actions |> List.map (fun a -> a.JsonModel) + |}) + |} |} |} module Endpoints = - type CustomDomain = - { - Name: ResourceName - Profile: ResourceName - Endpoint: ResourceName - Hostname: string - } + type CustomDomain = { + Name: ResourceName + Profile: ResourceName + Endpoint: ResourceName + Hostname: string + } with interface IArmResource with member this.ResourceId = customDomains.resourceId (this.Profile / this.Endpoint / this.Name) - member this.JsonModel = - {| customDomains.Create( - this.Profile / this.Endpoint / this.Name, - dependsOn = [ endpoints.resourceId (this.Profile, this.Endpoint) ] - ) with + member this.JsonModel = {| + customDomains.Create( + this.Profile / this.Endpoint / this.Name, + dependsOn = [ endpoints.resourceId (this.Profile, this.Endpoint) ] + ) with properties = {| hostName = this.Hostname |} - |} + |} diff --git a/src/Farmer/Arm/CognitiveServices.fs b/src/Farmer/Arm/CognitiveServices.fs index 2d4071926..a58739505 100644 --- a/src/Farmer/Arm/CognitiveServices.fs +++ b/src/Farmer/Arm/CognitiveServices.fs @@ -5,21 +5,20 @@ open Farmer let accounts = ResourceType("Microsoft.CognitiveServices/accounts", "2017-04-18") -type Accounts = - { - Name: ResourceName - Location: Location - Sku: CognitiveServices.Sku - Kind: CognitiveServices.Kind - Tags: Map - } +type Accounts = { + Name: ResourceName + Location: Location + Sku: CognitiveServices.Sku + Kind: CognitiveServices.Kind + Tags: Map +} with interface IArmResource with member this.ResourceId = accounts.resourceId this.Name - member this.JsonModel = - {| accounts.Create(this.Name, this.Location, tags = this.Tags) with + member this.JsonModel = {| + accounts.Create(this.Name, this.Location, tags = this.Tags) with sku = {| name = string this.Sku |} kind = this.Kind.ToString().Replace("_", ".") - properties = {| |} - |} + properties = {| |} + |} diff --git a/src/Farmer/Arm/CommunicationServices.fs b/src/Farmer/Arm/CommunicationServices.fs index d4bb0bc44..3bc16fd85 100644 --- a/src/Farmer/Arm/CommunicationServices.fs +++ b/src/Farmer/Arm/CommunicationServices.fs @@ -6,20 +6,18 @@ open Farmer let communicationServices = ResourceType("Microsoft.Communication/communicationServices", "2020-08-20-preview") -type CommunicationService = - { - Name: ResourceName - DataLocation: DataLocation - Tags: Map - } +type CommunicationService = { + Name: ResourceName + DataLocation: DataLocation + Tags: Map +} with interface IArmResource with member this.ResourceId = communicationServices.resourceId this.Name - member this.JsonModel = - {| communicationServices.Create(this.Name, Location.Global, tags = this.Tags) with - properties = - {| - dataLocation = this.DataLocation.ArmValue - |} - |} + member this.JsonModel = {| + communicationServices.Create(this.Name, Location.Global, tags = this.Tags) with + properties = {| + dataLocation = this.DataLocation.ArmValue + |} + |} diff --git a/src/Farmer/Arm/Compute.fs b/src/Farmer/Arm/Compute.fs index 803ecf06c..1cd8e8c1b 100644 --- a/src/Farmer/Arm/Compute.fs +++ b/src/Farmer/Arm/Compute.fs @@ -18,27 +18,26 @@ let extensions = let hostGroups = ResourceType("Microsoft.Compute/hostGroups", "2021-03-01") let hosts = ResourceType("Microsoft.Compute/hostGroups/hosts", "2021-03-01") -type CustomScriptExtension = - { - Name: ResourceName - Location: Location - VirtualMachine: ResourceName - FileUris: Uri list - ScriptContents: string - OS: OS - Tags: Map - } +type CustomScriptExtension = { + Name: ResourceName + Location: Location + VirtualMachine: ResourceName + FileUris: Uri list + ScriptContents: string + OS: OS + Tags: Map +} with interface IArmResource with member this.ResourceId = extensions.resourceId (this.VirtualMachine / this.Name) - member this.JsonModel = - {| extensions.Create( - this.VirtualMachine / this.Name, - this.Location, - [ virtualMachines.resourceId this.VirtualMachine ], - this.Tags - ) with + member this.JsonModel = {| + extensions.Create( + this.VirtualMachine / this.Name, + this.Location, + [ virtualMachines.resourceId this.VirtualMachine ], + this.Tags + ) with properties = match this.OS with | Windows -> @@ -47,83 +46,78 @@ type CustomScriptExtension = ``type`` = "CustomScriptExtension" typeHandlerVersion = "1.10" autoUpgradeMinorVersion = true - settings = - {| - fileUris = this.FileUris |> List.map string - |} - protectedSettings = - {| - commandToExecute = this.ScriptContents - |} + settings = {| + fileUris = this.FileUris |> List.map string + |} + protectedSettings = {| + commandToExecute = this.ScriptContents + |} |} |> box - | Linux -> - {| - publisher = "Microsoft.Azure.Extensions" - ``type`` = "CustomScript" - typeHandlerVersion = "2.1" - autoUpgradeMinorVersion = true - protectedSettings = - {| - fileUris = this.FileUris |> List.map string - script = this.ScriptContents |> Encoding.UTF8.GetBytes |> Convert.ToBase64String - |} + | Linux -> {| + publisher = "Microsoft.Azure.Extensions" + ``type`` = "CustomScript" + typeHandlerVersion = "2.1" + autoUpgradeMinorVersion = true + protectedSettings = {| + fileUris = this.FileUris |> List.map string + script = this.ScriptContents |> Encoding.UTF8.GetBytes |> Convert.ToBase64String |} - |} + |} + |} -type AadSshLoginExtension = - { - Location: Location - VirtualMachine: ResourceName - Tags: Map - } +type AadSshLoginExtension = { + Location: Location + VirtualMachine: ResourceName + Tags: Map +} with member this.Name = "AADSSHLoginForLinux" interface IArmResource with member this.ResourceId = extensions.resourceId (this.VirtualMachine / this.Name) - member this.JsonModel = - {| extensions.Create( - this.VirtualMachine / this.Name, - this.Location, - [ virtualMachines.resourceId this.VirtualMachine ], - this.Tags - ) with - properties = - {| - publisher = "Microsoft.Azure.ActiveDirectory" - ``type`` = "AADSSHLoginForLinux" - typeHandlerVersion = "1.0" - autoUpgradeMinorVersion = true - |} - |} + member this.JsonModel = {| + extensions.Create( + this.VirtualMachine / this.Name, + this.Location, + [ virtualMachines.resourceId this.VirtualMachine ], + this.Tags + ) with + properties = {| + publisher = "Microsoft.Azure.ActiveDirectory" + ``type`` = "AADSSHLoginForLinux" + typeHandlerVersion = "1.0" + autoUpgradeMinorVersion = true + |} + |} -type VirtualMachine = - { - Name: ResourceName - Location: Location - AvailabilityZone: string option - DiagnosticsEnabled: bool option - StorageAccount: LinkedResource option - Size: VMSize - Priority: Priority option - Credentials: {| Username: string - Password: SecureParameter |} - CustomData: string option - DisablePasswordAuthentication: bool option - PublicKeys: (string * string) list option - OsDisk: OsDiskCreateOption - DataDisks: DataDiskCreateOption list - NetworkInterfaceIds: ResourceId list - Identity: Identity.ManagedIdentity - Tags: Map - } +type VirtualMachine = { + Name: ResourceName + Location: Location + AvailabilityZone: string option + DiagnosticsEnabled: bool option + StorageAccount: LinkedResource option + Size: VMSize + Priority: Priority option + Credentials: {| + Username: string + Password: SecureParameter + |} + CustomData: string option + DisablePasswordAuthentication: bool option + PublicKeys: (string * string) list option + OsDisk: OsDiskCreateOption + DataDisks: DataDiskCreateOption list + NetworkInterfaceIds: ResourceId list + Identity: Identity.ManagedIdentity + Tags: Map +} with interface IParameters with member this.SecureParameters = match this.DisablePasswordAuthentication, this.OsDisk with - | Some (true), _ + | Some(true), _ | _, AttachOsDisk _ -> [] // What attaching an OS disk, the osConfig cannot be set, so cannot set password | _ -> [ this.Credentials.Password ] @@ -131,227 +125,212 @@ type VirtualMachine = member this.ResourceId = virtualMachines.resourceId this.Name member this.JsonModel = - let dependsOn = - [ - yield! this.NetworkInterfaceIds - match this.StorageAccount with - | Some (Managed rid) -> rid - | Some (Unmanaged _) - | None -> () - match this.OsDisk with - | AttachOsDisk (_, Managed (resourceId)) -> resourceId + let dependsOn = [ + yield! this.NetworkInterfaceIds + match this.StorageAccount with + | Some(Managed rid) -> rid + | Some(Unmanaged _) + | None -> () + match this.OsDisk with + | AttachOsDisk(_, Managed(resourceId)) -> resourceId + | _ -> () + for disk in this.DataDisks do + match disk with + | AttachDataDisk(Managed(resourceId)) + | AttachUltra(Managed(resourceId)) -> resourceId | _ -> () - for disk in this.DataDisks do - match disk with - | AttachDataDisk (Managed (resourceId)) - | AttachUltra (Managed (resourceId)) -> resourceId - | _ -> () - ] - - let properties = - {| - additionalCapabilities = // If data disks use UltraSSD then enable that support - if this.DataDisks |> List.exists (fun disk -> disk.IsUltraDisk) then - {| ultraSSDEnabled = true |} :> obj - else - null - priority = - match this.Priority with - | Some priority -> priority.ArmValue - | _ -> Unchecked.defaultof<_> - hardwareProfile = {| vmSize = this.Size.ArmValue |} - osProfile = - match this.OsDisk with - | AttachOsDisk _ -> null - | _ -> - {| - computerName = this.Name.Value - adminUsername = this.Credentials.Username - adminPassword = - if - this.DisablePasswordAuthentication.IsSome - && this.DisablePasswordAuthentication.Value - then //If the disablePasswordAuthentication is set and the value is true then we don't need a password - null - else - this.Credentials.Password.ArmExpression.Eval() - customData = - this.CustomData - |> Option.map (System.Text.Encoding.UTF8.GetBytes >> Convert.ToBase64String) - |> Option.toObj - linuxConfiguration = - if this.DisablePasswordAuthentication.IsSome || this.PublicKeys.IsSome then - {| - disablePasswordAuthentication = - this.DisablePasswordAuthentication |> Option.map box |> Option.toObj - ssh = - match this.PublicKeys with - | Some publicKeys -> - {| - publicKeys = - publicKeys - |> List.map (fun k -> {| path = fst k; keyData = snd k |}) - |} - | None -> Unchecked.defaultof<_> - |} - else - Unchecked.defaultof<_> - |} - :> obj - storageProfile = - let vmNameLowerCase = this.Name.Value.ToLower() + ] + let properties = {| + additionalCapabilities = // If data disks use UltraSSD then enable that support + if this.DataDisks |> List.exists (fun disk -> disk.IsUltraDisk) then + {| ultraSSDEnabled = true |} :> obj + else + null + priority = + match this.Priority with + | Some priority -> priority.ArmValue + | _ -> Unchecked.defaultof<_> + hardwareProfile = {| vmSize = this.Size.ArmValue |} + osProfile = + match this.OsDisk with + | AttachOsDisk _ -> null + | _ -> {| - imageReference = - match this.OsDisk with - | FromImage (imageDefintion, _) -> + computerName = this.Name.Value + adminUsername = this.Credentials.Username + adminPassword = + if + this.DisablePasswordAuthentication.IsSome + && this.DisablePasswordAuthentication.Value + then //If the disablePasswordAuthentication is set and the value is true then we don't need a password + null + else + this.Credentials.Password.ArmExpression.Eval() + customData = + this.CustomData + |> Option.map (System.Text.Encoding.UTF8.GetBytes >> Convert.ToBase64String) + |> Option.toObj + linuxConfiguration = + if this.DisablePasswordAuthentication.IsSome || this.PublicKeys.IsSome then {| - publisher = imageDefintion.Publisher.ArmValue - offer = imageDefintion.Offer.ArmValue - sku = imageDefintion.Sku.ArmValue - version = "latest" + disablePasswordAuthentication = + this.DisablePasswordAuthentication |> Option.map box |> Option.toObj + ssh = + match this.PublicKeys with + | Some publicKeys -> {| + publicKeys = + publicKeys + |> List.map (fun k -> {| path = fst k; keyData = snd k |}) + |} + | None -> Unchecked.defaultof<_> |} - :> obj - | _ -> null - osDisk = - match this.OsDisk with - | FromImage (_, diskInfo) -> - {| - createOption = "FromImage" - name = $"{vmNameLowerCase}-osdisk" - diskSizeGB = diskInfo.Size - managedDisk = - {| - storageAccountType = diskInfo.DiskType.ArmValue - |} + else + Unchecked.defaultof<_> + |} + :> obj + storageProfile = + let vmNameLowerCase = this.Name.Value.ToLower() + + {| + imageReference = + match this.OsDisk with + | FromImage(imageDefintion, _) -> + {| + publisher = imageDefintion.Publisher.ArmValue + offer = imageDefintion.Offer.ArmValue + sku = imageDefintion.Sku.ArmValue + version = "latest" + |} + :> obj + | _ -> null + osDisk = + match this.OsDisk with + | FromImage(_, diskInfo) -> + {| + createOption = "FromImage" + name = $"{vmNameLowerCase}-osdisk" + diskSizeGB = diskInfo.Size + managedDisk = {| + storageAccountType = diskInfo.DiskType.ArmValue |} - :> obj - | AttachOsDisk (os, managedDiskId) -> + |} + :> obj + | AttachOsDisk(os, managedDiskId) -> {| + createOption = "Attach" + managedDisk = {| + id = managedDiskId.ResourceId.Eval() + |} + name = managedDiskId.Name.Value + osType = string os + |} + dataDisks = + this.DataDisks + |> List.mapi (fun lun dataDisk -> + match dataDisk with + | AttachDataDisk(managedDiskId) + | AttachUltra(managedDiskId) -> {| createOption = "Attach" - managedDisk = - {| - id = managedDiskId.ResourceId.Eval() - |} name = managedDiskId.Name.Value - osType = string os - |} - dataDisks = - this.DataDisks - |> List.mapi (fun lun dataDisk -> - match dataDisk with - | AttachDataDisk (managedDiskId) - | AttachUltra (managedDiskId) -> - {| - createOption = "Attach" - name = managedDiskId.Name.Value - lun = lun - managedDisk = - {| - id = managedDiskId.ResourceId.Eval() - |} + lun = lun + managedDisk = {| + id = managedDiskId.ResourceId.Eval() |} - :> obj - | Empty diskInfo -> - {| - createOption = "Empty" - name = $"{vmNameLowerCase}-datadisk-{lun}" - diskSizeGB = diskInfo.Size - lun = lun - managedDisk = - {| - storageAccountType = diskInfo.DiskType.ArmValue - |} - |} - :> obj) - |} - networkProfile = - {| - networkInterfaces = - this.NetworkInterfaceIds - |> List.mapi (fun idx id -> - {| - id = id.Eval() - properties = - if this.NetworkInterfaceIds.Length > 1 then - box {| primary = idx = 0 |} // First NIC is primary - else - null // Don't emit primary if there aren't multiple NICs - |}) - |} - diagnosticsProfile = - match this.DiagnosticsEnabled with - | None - | Some false -> - box - {| - bootDiagnostics = {| enabled = false |} - |} - | Some true -> - match this.StorageAccount with - | Some storageAccount -> - let resourceId = storageAccount.ResourceId - - let storageUriExpr = - ArmExpression - .reference(storageAccounts, resourceId) - .Map(fun r -> r + ".primaryEndpoints.blob") - .WithOwner(resourceId) - .Eval() - - box - {| - bootDiagnostics = - {| - enabled = true - storageUri = storageUriExpr - |} |} - | None -> - box + :> obj + | Empty diskInfo -> {| - bootDiagnostics = {| enabled = true |} + createOption = "Empty" + name = $"{vmNameLowerCase}-datadisk-{lun}" + diskSizeGB = diskInfo.Size + lun = lun + managedDisk = {| + storageAccountType = diskInfo.DiskType.ArmValue + |} |} + :> obj) + |} + networkProfile = {| + networkInterfaces = + this.NetworkInterfaceIds + |> List.mapi (fun idx id -> {| + id = id.Eval() + properties = + if this.NetworkInterfaceIds.Length > 1 then + box {| primary = idx = 0 |} // First NIC is primary + else + null // Don't emit primary if there aren't multiple NICs + |}) |} - - {| 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 + diagnosticsProfile = + match this.DiagnosticsEnabled with | None - | Some Low - | Some Regular -> box properties - | Some (Spot (evictionPolicy, maxPrice)) -> - {| properties with - evictionPolicy = evictionPolicy.ArmValue - billingProfile = {| maxPrice = maxPrice |} + | Some false -> + box {| + bootDiagnostics = {| enabled = false |} |} - zones = this.AvailabilityZone |> Option.map ResizeArray |> Option.toObj + | Some true -> + match this.StorageAccount with + | Some storageAccount -> + let resourceId = storageAccount.ResourceId + + let storageUriExpr = + ArmExpression + .reference(storageAccounts, resourceId) + .Map(fun r -> r + ".primaryEndpoints.blob") + .WithOwner(resourceId) + .Eval() + + box {| + bootDiagnostics = {| + enabled = true + storageUri = storageUriExpr + |} + |} + | None -> + box {| + bootDiagnostics = {| enabled = true |} + |} + |} + + {| + 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 + | None + | Some Low + | Some Regular -> box properties + | Some(Spot(evictionPolicy, maxPrice)) -> {| + properties with + evictionPolicy = evictionPolicy.ArmValue + billingProfile = {| maxPrice = maxPrice |} + |} + zones = this.AvailabilityZone |> Option.map ResizeArray |> Option.toObj |} -type Host = - { - Name: ResourceName - Location: Location - Sku: HostSku - ParentHostGroupName: ResourceName - AutoReplaceOnFailure: FeatureFlag - LicenseType: HostLicenseType - PlatformFaultDomain: PlatformFaultDomainCount - Tags: Map - DependsOn: Set - } +type Host = { + Name: ResourceName + Location: Location + Sku: HostSku + ParentHostGroupName: ResourceName + AutoReplaceOnFailure: FeatureFlag + LicenseType: HostLicenseType + PlatformFaultDomain: PlatformFaultDomainCount + Tags: Map + DependsOn: Set +} with - member internal this.JsonModelProperties = - {| - autoReplaceOnFailure = this.AutoReplaceOnFailure.AsBoolean - licenseType = HostLicenseType.Print this.LicenseType - platformFaultDomain = PlatformFaultDomainCount.ToArmValue this.PlatformFaultDomain - |} + member internal this.JsonModelProperties = {| + autoReplaceOnFailure = this.AutoReplaceOnFailure.AsBoolean + licenseType = HostLicenseType.Print this.LicenseType + platformFaultDomain = PlatformFaultDomainCount.ToArmValue this.PlatformFaultDomain + |} interface IArmResource with member this.ResourceId = hosts.resourceId this.Name @@ -363,33 +342,32 @@ type Host = let hostResourceName = ResourceName($"{this.ParentHostGroupName.Value}/{this.Name.Value}") - {| hosts.Create(hostResourceName, this.Location, dependsOn, tags = this.Tags) with - sku = this.Sku.JsonProperties - properties = this.JsonModelProperties + {| + hosts.Create(hostResourceName, this.Location, dependsOn, tags = this.Tags) with + sku = this.Sku.JsonProperties + properties = this.JsonModelProperties |} -type HostGroup = - { - Name: ResourceName - Location: Location - AvailabilityZone: string list - SupportAutomaticPlacement: FeatureFlag - PlatformFaultDomainCount: PlatformFaultDomainCount - Tags: Map - DependsOn: Set - } +type HostGroup = { + Name: ResourceName + Location: Location + AvailabilityZone: string list + SupportAutomaticPlacement: FeatureFlag + PlatformFaultDomainCount: PlatformFaultDomainCount + Tags: Map + DependsOn: Set +} with - member internal this.JsonModelProperties = - {| - supportAutomaticPlacement = this.SupportAutomaticPlacement.AsBoolean - platformFaultDomainCount = PlatformFaultDomainCount.ToArmValue this.PlatformFaultDomainCount - |} + member internal this.JsonModelProperties = {| + supportAutomaticPlacement = this.SupportAutomaticPlacement.AsBoolean + platformFaultDomainCount = PlatformFaultDomainCount.ToArmValue this.PlatformFaultDomainCount + |} interface IArmResource with member this.ResourceId = hostGroups.resourceId this.Name - member this.JsonModel = - {| hostGroups.Create(this.Name, this.Location, tags = this.Tags, dependsOn = this.DependsOn) with + member this.JsonModel = {| + hostGroups.Create(this.Name, this.Location, tags = this.Tags, dependsOn = this.DependsOn) with zones = this.AvailabilityZone properties = this.JsonModelProperties - |} + |} diff --git a/src/Farmer/Arm/ContainerInstance.fs b/src/Farmer/Arm/ContainerInstance.fs index 5ee45dd84..87b8b842c 100644 --- a/src/Farmer/Arm/ContainerInstance.fs +++ b/src/Farmer/Arm/ContainerInstance.fs @@ -10,18 +10,19 @@ open System let containerGroups = ResourceType("Microsoft.ContainerInstance/containerGroups", "2021-10-01") -type ContainerGroupIpAddress = - { - Type: IpAddressType - Ports: {| Protocol: TransmissionProtocol - Port: uint16 |} Set - } +type ContainerGroupIpAddress = { + Type: IpAddressType + Ports: + {| + Protocol: TransmissionProtocol + Port: uint16 + |} Set +} -type ContainerInstanceGpu = - { - Count: int - Sku: Gpu.Sku - } +type ContainerInstanceGpu = { + Count: int + Sku: Gpu.Sku +} with static member internal JsonModel = function @@ -34,21 +35,20 @@ type ContainerInstanceGpu = :> obj /// Defines a command or HTTP request to get the status of a container. -type ContainerProbe = - { - Exec: string list - HttpGet: Uri option - /// The probe will not run until this delay after container startup. Default is 0 - runs immediately. - InitialDelaySeconds: int option - /// How often to execute the probe against the container - default is 10 seconds. - PeriodSeconds: int option - /// Number of failures before this container is considered unhealthy - default is 3. - FailureThreshold: int option - /// Number of successes before this container is considered healthy - default is 1. - SuccessThreshold: int option - /// Number of seconds for the probe to run - default is 1 second. - TimeoutSeconds: int option - } +type ContainerProbe = { + Exec: string list + HttpGet: Uri option + /// The probe will not run until this delay after container startup. Default is 0 - runs immediately. + InitialDelaySeconds: int option + /// How often to execute the probe against the container - default is 10 seconds. + PeriodSeconds: int option + /// Number of failures before this container is considered unhealthy - default is 3. + FailureThreshold: int option + /// Number of successes before this container is considered healthy - default is 1. + SuccessThreshold: int option + /// Number of seconds for the probe to run - default is 1 second. + TimeoutSeconds: int option +} with static member internal JsonModel = function @@ -78,11 +78,10 @@ type ContainerProbe = |} :> obj -type ContainerGroupDiagnostics = - { - LogType: LogType - Workspace: LogAnalyticsWorkspace - } +type ContainerGroupDiagnostics = { + LogType: LogType + Workspace: LogAnalyticsWorkspace +} with static member internal JsonModel = function @@ -90,30 +89,28 @@ type ContainerGroupDiagnostics = | Some diag -> let logAnalyticsId, logAnalyticsKey = match diag.Workspace with - | LogAnalyticsWorkspace.WorkspaceKey (workspaceId, workspaceKey) -> workspaceId, workspaceKey + | LogAnalyticsWorkspace.WorkspaceKey(workspaceId, workspaceKey) -> workspaceId, workspaceKey | LogAnalyticsWorkspace.WorkspaceResourceId resourceRef -> (LogAnalytics.LogAnalytics.getCustomerId resourceRef.ResourceId).Eval(), (LogAnalytics.LogAnalytics.getPrimarySharedKey resourceRef.ResourceId).Eval() {| - logAnalytics = - {| - logType = - match diag.LogType with - | ContainerInsights -> "ContainerInsights" - | ContainerInstanceLogs -> "ContainerInstanceLogs" - workspaceId = logAnalyticsId - workspaceKey = logAnalyticsKey - |} + logAnalytics = {| + logType = + match diag.LogType with + | ContainerInsights -> "ContainerInsights" + | ContainerInstanceLogs -> "ContainerInstanceLogs" + workspaceId = logAnalyticsId + workspaceKey = logAnalyticsKey + |} |} :> obj -type ContainerGroupDnsConfiguration = - { - NameServers: string list - SearchDomains: string list - Options: string list - } +type ContainerGroupDnsConfiguration = { + NameServers: string list + SearchDomains: string list + Options: string list +} with static member internal JsonModel = function @@ -134,104 +131,107 @@ type ContainerGroupDnsConfiguration = |} :> obj -type ContainerGroup = - { - Name: ResourceName - Location: Location - AvailabilityZone: string option - ContainerInstances: {| Name: ResourceName - Image: Containers.DockerImage - Command: string list - Ports: uint16 Set - Cpu: float - Memory: float - Gpu: ContainerInstanceGpu option - EnvironmentVariables: Map - VolumeMounts: Map - LivenessProbe: ContainerProbe option - ReadinessProbe: ContainerProbe option |} list - Diagnostics: ContainerGroupDiagnostics option - DnsConfig: ContainerGroupDnsConfiguration option - OperatingSystem: OS - RestartPolicy: RestartPolicy - Identity: ManagedIdentity - ImageRegistryCredentials: ImageRegistryAuthentication list - InitContainers: {| Name: ResourceName - Image: Containers.DockerImage - Command: string list - EnvironmentVariables: Map - VolumeMounts: Map |} list - IpAddress: ContainerGroupIpAddress option - NetworkProfile: ResourceName option - SubnetIds: LinkedResource list - Volumes: Map - Tags: Map - Dependencies: Set - } +type ContainerGroup = { + Name: ResourceName + Location: Location + AvailabilityZone: string option + ContainerInstances: + {| + Name: ResourceName + Image: Containers.DockerImage + Command: string list + Ports: uint16 Set + Cpu: float + Memory: float + Gpu: ContainerInstanceGpu option + EnvironmentVariables: Map + VolumeMounts: Map + LivenessProbe: ContainerProbe option + ReadinessProbe: ContainerProbe option + |} list + Diagnostics: ContainerGroupDiagnostics option + DnsConfig: ContainerGroupDnsConfiguration option + OperatingSystem: OS + RestartPolicy: RestartPolicy + Identity: ManagedIdentity + ImageRegistryCredentials: ImageRegistryAuthentication list + InitContainers: + {| + Name: ResourceName + Image: Containers.DockerImage + Command: string list + EnvironmentVariables: Map + VolumeMounts: Map + |} list + IpAddress: ContainerGroupIpAddress option + NetworkProfile: ResourceName option + SubnetIds: LinkedResource list + Volumes: Map + Tags: Map + Dependencies: Set +} with member this.NetworkProfilePath = this.NetworkProfile |> Option.map networkProfiles.resourceId - member private this.dependencies = - [ - yield! Option.toList this.NetworkProfilePath - for id in this.SubnetIds do - match id with - | Managed subnetId -> - { subnetId with - Type = virtualNetworks - Segments = [] - } // should be vnet ID - | Unmanaged _ -> () + member private this.dependencies = [ + yield! Option.toList this.NetworkProfilePath + for id in this.SubnetIds do + match id with + | Managed subnetId -> { + subnetId with + Type = virtualNetworks + Segments = [] + } // should be vnet ID + | Unmanaged _ -> () - for _, volume in this.Volumes |> Map.toSeq do - match volume with - | Volume.AzureFileShare (shareName, storageAccountName) -> - fileShares.resourceId (storageAccountName.ResourceName, ResourceName "default", shareName) - | _ -> () - - match this.Diagnostics with - | Some { - Workspace = LogAnalyticsWorkspace.WorkspaceResourceId (LinkedResource.Managed (resId)) - } -> resId + for _, volume in this.Volumes |> Map.toSeq do + match volume with + | Volume.AzureFileShare(shareName, storageAccountName) -> + fileShares.resourceId (storageAccountName.ResourceName, ResourceName "default", shareName) | _ -> () - // If the identity is set, include any dependent identity's resource ID - yield! this.Identity.Dependencies - yield! this.Dependencies - ] + + match this.Diagnostics with + | Some { + Workspace = LogAnalyticsWorkspace.WorkspaceResourceId(LinkedResource.Managed(resId)) + } -> resId + | _ -> () + // If the identity is set, include any dependent identity's resource ID + yield! this.Identity.Dependencies + yield! this.Dependencies + ] interface IParameters with - member this.SecureParameters = - [ - for credential in this.ImageRegistryCredentials do - match credential with - | ImageRegistryAuthentication.Credential credential -> credential.Password - | ImageRegistryAuthentication.ListCredentials _ -> () - | ImageRegistryAuthentication.ManagedIdentityCredential _ -> () - for container in this.ContainerInstances do - for envVar in container.EnvironmentVariables do - match envVar.Value with - | SecureEnvValue p -> p - | SecureEnvExpression _ -> () - | EnvValue _ -> () - for container in this.InitContainers do - for envVar in container.EnvironmentVariables do - match envVar.Value with - | SecureEnvValue p -> p - | SecureEnvExpression _ -> () - | EnvValue _ -> () - for volume in this.Volumes do - match volume.Value with - | Volume.Secret secrets -> - for secret in secrets do - match secret with - | SecretFileParameter (_, parameter) -> parameter - | SecretFileContents _ -> () - | Volume.EmptyDirectory - | Volume.AzureFileShare _ - | Volume.Secret _ - | Volume.GitRepo _ -> () - ] + member this.SecureParameters = [ + for credential in this.ImageRegistryCredentials do + match credential with + | ImageRegistryAuthentication.Credential credential -> credential.Password + | ImageRegistryAuthentication.ListCredentials _ -> () + | ImageRegistryAuthentication.ManagedIdentityCredential _ -> () + for container in this.ContainerInstances do + for envVar in container.EnvironmentVariables do + match envVar.Value with + | SecureEnvValue p -> p + | SecureEnvExpression _ -> () + | EnvValue _ -> () + for container in this.InitContainers do + for envVar in container.EnvironmentVariables do + match envVar.Value with + | SecureEnvValue p -> p + | SecureEnvExpression _ -> () + | EnvValue _ -> () + for volume in this.Volumes do + match volume.Value with + | Volume.Secret secrets -> + for secret in secrets do + match secret with + | SecretFileParameter(_, parameter) -> parameter + | SecretFileContents _ -> () + | Volume.EmptyDirectory + | Volume.AzureFileShare _ + | Volume.Secret _ + | Volume.GitRepo _ -> () + ] /// Creates a version depending on whether this needs the legacy API features. member private this.resourceCommonProps = @@ -248,252 +248,221 @@ type ContainerGroup = interface IArmResource with member this.ResourceId = containerGroups.resourceId this.Name - member this.JsonModel = - {| this.resourceCommonProps with + member this.JsonModel = {| + this.resourceCommonProps with identity = this.Identity.ToArmJson - properties = - {| - containers = - this.ContainerInstances - |> List.map (fun container -> - {| - name = container.Name.Value.ToLowerInvariant() - properties = - {| - image = container.Image.ImageTag - command = container.Command - ports = container.Ports |> Set.map (fun port -> {| port = port |}) - environmentVariables = - [ - for key, value in Map.toSeq container.EnvironmentVariables do - match value with - | EnvValue value -> - {| - name = key - value = value - secureValue = null - |} - | SecureEnvExpression armExpression -> - {| - name = key - value = null - secureValue = armExpression.Eval() - |} - | SecureEnvValue value -> - {| - name = key - value = null - secureValue = value.ArmExpression.Eval() - |} - ] - livenessProbe = ContainerProbe.JsonModel container.LivenessProbe - readinessProbe = ContainerProbe.JsonModel container.ReadinessProbe - resources = - {| - requests = - {| - cpu = container.Cpu - memoryInGB = container.Memory - gpu = ContainerInstanceGpu.JsonModel container.Gpu - |} - |} - volumeMounts = - container.VolumeMounts - |> Seq.map (fun kvp -> - {| - name = kvp.Key - mountPath = kvp.Value - |}) - |> List.ofSeq - |} - |}) - diagnostics = ContainerGroupDiagnostics.JsonModel this.Diagnostics - dnsConfig = ContainerGroupDnsConfiguration.JsonModel this.DnsConfig - initContainers = - this.InitContainers - |> List.map (fun container -> - {| - name = container.Name.Value.ToLowerInvariant() - properties = - {| - image = container.Image.ImageTag - command = container.Command - environmentVariables = - [ - for key, value in Map.toSeq container.EnvironmentVariables do - match value with - | EnvValue value -> - {| - name = key - value = value - secureValue = null - |} - | SecureEnvExpression armExpression -> - {| - name = key - value = null - secureValue = armExpression.Eval() - |} - | SecureEnvValue value -> - {| - name = key - value = null - secureValue = value.ArmExpression.Eval() - |} - ] - volumeMounts = - container.VolumeMounts - |> Seq.map (fun kvp -> - {| - name = kvp.Key - mountPath = kvp.Value - |}) - |> List.ofSeq - |} - |}) - osType = string this.OperatingSystem - restartPolicy = - match this.RestartPolicy with - | AlwaysRestart -> "Always" - | NeverRestart -> "Never" - | RestartOnFailure -> "OnFailure" - imageRegistryCredentials = - this.ImageRegistryCredentials - |> List.map (fun cred -> - match cred with - | ImageRegistryAuthentication.Credential cred -> - {| - server = cred.Server - username = cred.Username - password = cred.Password.ArmExpression.Eval() - identity = null - |} - | ImageRegistryAuthentication.ListCredentials resourceId -> - {| - server = - ArmExpression - .create( - $"reference({resourceId.ArmExpression.Value}, '2019-05-01').loginServer" - ) - .Eval() - username = - ArmExpression - .create( - $"listCredentials({resourceId.ArmExpression.Value}, '2019-05-01').username" - ) - .Eval() - password = - ArmExpression - .create( - $"listCredentials({resourceId.ArmExpression.Value}, '2019-05-01').passwords[0].value" - ) - .Eval() - identity = null + properties = {| + containers = + this.ContainerInstances + |> List.map (fun container -> {| + name = container.Name.Value.ToLowerInvariant() + properties = {| + image = container.Image.ImageTag + command = container.Command + ports = container.Ports |> Set.map (fun port -> {| port = port |}) + environmentVariables = [ + for key, value in Map.toSeq container.EnvironmentVariables do + match value with + | EnvValue value -> {| + name = key + value = value + secureValue = null + |} + | SecureEnvExpression armExpression -> {| + name = key + value = null + secureValue = armExpression.Eval() + |} + | SecureEnvValue value -> {| + name = key + value = null + secureValue = value.ArmExpression.Eval() + |} + ] + livenessProbe = ContainerProbe.JsonModel container.LivenessProbe + readinessProbe = ContainerProbe.JsonModel container.ReadinessProbe + resources = {| + requests = {| + cpu = container.Cpu + memoryInGB = container.Memory + gpu = ContainerInstanceGpu.JsonModel container.Gpu |} - | ImageRegistryAuthentication.ManagedIdentityCredential cred -> - {| - server = cred.Server - username = String.Empty - password = null - identity = - cred.Identity.UserAssigned - |> List.tryHead - |> Option.map (fun upn -> upn.ResourceId.ArmExpression.Eval()) - |> Option.defaultValue null - |}) - ipAddress = - match this.IpAddress with - | Some ipAddresses -> - {| - ``type`` = - match ipAddresses.Type with - | PublicAddress - | PublicAddressWithDns _ -> "Public" - | PrivateAddress _ -> "Private" - ports = - [ - for port in ipAddresses.Ports do - {| - protocol = string port.Protocol - port = port.Port - |} - ] - dnsNameLabel = - match ipAddresses.Type with - | PublicAddressWithDns dnsLabel -> dnsLabel - | _ -> null |} - |> box - | None -> null - networkProfile = - this.NetworkProfilePath - |> Option.map (fun path -> box {| id = path.Eval() |}) - |> Option.toObj - subnetIds = - if this.SubnetIds.IsEmpty then - null - else - this.SubnetIds - |> List.map (fun subnetId -> {| id = subnetId.ResourceId.Eval() |}) - |> box - volumes = - [ - for key, value in Map.toSeq this.Volumes do - match key, value with - | volumeName, Volume.AzureFileShare (shareName, accountName) -> - {| - name = volumeName - azureFile = - {| - shareName = shareName.Value - storageAccountName = accountName.ResourceName.Value - storageAccountKey = - $"[listKeys('Microsoft.Storage/storageAccounts/{accountName.ResourceName.Value}', '2018-07-01').keys[0].value]" - |} - emptyDir = null - gitRepo = Unchecked.defaultof<_> - secret = Unchecked.defaultof<_> - |} - | volumeName, Volume.EmptyDirectory -> - {| - name = volumeName - azureFile = Unchecked.defaultof<_> - emptyDir = obj () - gitRepo = Unchecked.defaultof<_> - secret = Unchecked.defaultof<_> - |} - | volumeName, Volume.GitRepo (repository, directory, revision) -> - {| - name = volumeName - azureFile = Unchecked.defaultof<_> - emptyDir = null - gitRepo = - {| - repository = repository - directory = directory |> Option.toObj - revision = revision |> Option.toObj - |} - secret = Unchecked.defaultof<_> - |} - | volumeName, Volume.Secret secrets -> + volumeMounts = + container.VolumeMounts + |> Seq.map (fun kvp -> {| + name = kvp.Key + mountPath = kvp.Value + |}) + |> List.ofSeq + |} + |}) + diagnostics = ContainerGroupDiagnostics.JsonModel this.Diagnostics + dnsConfig = ContainerGroupDnsConfiguration.JsonModel this.DnsConfig + initContainers = + this.InitContainers + |> List.map (fun container -> {| + name = container.Name.Value.ToLowerInvariant() + properties = {| + image = container.Image.ImageTag + command = container.Command + environmentVariables = [ + for key, value in Map.toSeq container.EnvironmentVariables do + match value with + | EnvValue value -> {| + name = key + value = value + secureValue = null + |} + | SecureEnvExpression armExpression -> {| + name = key + value = null + secureValue = armExpression.Eval() + |} + | SecureEnvValue value -> {| + name = key + value = null + secureValue = value.ArmExpression.Eval() + |} + ] + volumeMounts = + container.VolumeMounts + |> Seq.map (fun kvp -> {| + name = kvp.Key + mountPath = kvp.Value + |}) + |> List.ofSeq + |} + |}) + osType = string this.OperatingSystem + restartPolicy = + match this.RestartPolicy with + | AlwaysRestart -> "Always" + | NeverRestart -> "Never" + | RestartOnFailure -> "OnFailure" + imageRegistryCredentials = + this.ImageRegistryCredentials + |> List.map (fun cred -> + match cred with + | ImageRegistryAuthentication.Credential cred -> {| + server = cred.Server + username = cred.Username + password = cred.Password.ArmExpression.Eval() + identity = null + |} + | ImageRegistryAuthentication.ListCredentials resourceId -> {| + server = + ArmExpression + .create( + $"reference({resourceId.ArmExpression.Value}, '2019-05-01').loginServer" + ) + .Eval() + username = + ArmExpression + .create( + $"listCredentials({resourceId.ArmExpression.Value}, '2019-05-01').username" + ) + .Eval() + password = + ArmExpression + .create( + $"listCredentials({resourceId.ArmExpression.Value}, '2019-05-01').passwords[0].value" + ) + .Eval() + identity = null + |} + | ImageRegistryAuthentication.ManagedIdentityCredential cred -> {| + server = cred.Server + username = String.Empty + password = null + identity = + cred.Identity.UserAssigned + |> List.tryHead + |> Option.map (fun upn -> upn.ResourceId.ArmExpression.Eval()) + |> Option.defaultValue null + |}) + ipAddress = + match this.IpAddress with + | Some ipAddresses -> + {| + ``type`` = + match ipAddresses.Type with + | PublicAddress + | PublicAddressWithDns _ -> "Public" + | PrivateAddress _ -> "Private" + ports = [ + for port in ipAddresses.Ports do {| - name = volumeName - azureFile = Unchecked.defaultof<_> - emptyDir = null - gitRepo = Unchecked.defaultof<_> - secret = - dict - [ - for secret in secrets do - match secret with - | SecretFileContents (name, secret) -> - name, Convert.ToBase64String secret - | SecretFileParameter (name, parameter) -> - name, - parameter.ArmExpression.Map(sprintf "base64(%s)").Eval() - ] + protocol = string port.Protocol + port = port.Port |} - ] - |} + ] + dnsNameLabel = + match ipAddresses.Type with + | PublicAddressWithDns dnsLabel -> dnsLabel + | _ -> null + |} + |> box + | None -> null + networkProfile = + this.NetworkProfilePath + |> Option.map (fun path -> box {| id = path.Eval() |}) + |> Option.toObj + subnetIds = + if this.SubnetIds.IsEmpty then + null + else + this.SubnetIds + |> List.map (fun subnetId -> {| id = subnetId.ResourceId.Eval() |}) + |> box + volumes = [ + for key, value in Map.toSeq this.Volumes do + match key, value with + | volumeName, Volume.AzureFileShare(shareName, accountName) -> {| + name = volumeName + azureFile = {| + shareName = shareName.Value + storageAccountName = accountName.ResourceName.Value + storageAccountKey = + $"[listKeys('Microsoft.Storage/storageAccounts/{accountName.ResourceName.Value}', '2018-07-01').keys[0].value]" + |} + emptyDir = null + gitRepo = Unchecked.defaultof<_> + secret = Unchecked.defaultof<_> + |} + | volumeName, Volume.EmptyDirectory -> {| + name = volumeName + azureFile = Unchecked.defaultof<_> + emptyDir = obj () + gitRepo = Unchecked.defaultof<_> + secret = Unchecked.defaultof<_> + |} + | volumeName, Volume.GitRepo(repository, directory, revision) -> {| + name = volumeName + azureFile = Unchecked.defaultof<_> + emptyDir = null + gitRepo = {| + repository = repository + directory = directory |> Option.toObj + revision = revision |> Option.toObj + |} + secret = Unchecked.defaultof<_> + |} + | volumeName, Volume.Secret secrets -> {| + name = volumeName + azureFile = Unchecked.defaultof<_> + emptyDir = null + gitRepo = Unchecked.defaultof<_> + secret = + dict [ + for secret in secrets do + match secret with + | SecretFileContents(name, secret) -> name, Convert.ToBase64String secret + | SecretFileParameter(name, parameter) -> + name, parameter.ArmExpression.Map(sprintf "base64(%s)").Eval() + ] + |} + ] + |} zones = this.AvailabilityZone |> Option.map Array.singleton |> Option.defaultValue null - |} + |} diff --git a/src/Farmer/Arm/ContainerRegistry.fs b/src/Farmer/Arm/ContainerRegistry.fs index 2d51905b5..2b9fd4a6f 100644 --- a/src/Farmer/Arm/ContainerRegistry.fs +++ b/src/Farmer/Arm/ContainerRegistry.fs @@ -7,23 +7,21 @@ open Farmer.ContainerRegistry let registries = ResourceType("Microsoft.ContainerRegistry/registries", "2019-05-01") -type Registries = - { - Name: ResourceName - Location: Location - Sku: Sku - AdminUserEnabled: bool - Tags: Map - } +type Registries = { + Name: ResourceName + Location: Location + Sku: Sku + AdminUserEnabled: bool + Tags: Map +} with interface IArmResource with member this.ResourceId = registries.resourceId this.Name - member this.JsonModel = - {| registries.Create(this.Name, this.Location, tags = this.Tags) with + member this.JsonModel = {| + registries.Create(this.Name, this.Location, tags = this.Tags) with sku = {| name = this.Sku.ToString() |} - properties = - {| - adminUserEnabled = this.AdminUserEnabled - |} - |} + properties = {| + adminUserEnabled = this.AdminUserEnabled + |} + |} diff --git a/src/Farmer/Arm/ContainerService.fs b/src/Farmer/Arm/ContainerService.fs index d4de7d13d..391720ec5 100644 --- a/src/Farmer/Arm/ContainerService.fs +++ b/src/Farmer/Arm/ContainerService.fs @@ -9,192 +9,191 @@ let managedClusters = ResourceType("Microsoft.ContainerService/managedClusters", "2021-03-01") module AddonProfiles = - type AciConnectorLinux = - { - Status: FeatureFlag - } + type AciConnectorLinux = { + Status: FeatureFlag + } with member internal this.ToArmJson = {| enabled = this.Status.AsBoolean |} - type HttpApplicationRouting = - { - Status: FeatureFlag - } + type HttpApplicationRouting = { + Status: FeatureFlag + } with member internal this.ToArmJson = {| enabled = this.Status.AsBoolean |} - type IngressApplicationGateway = - { - Status: FeatureFlag - ApplicationGatewayId: ResourceId - Identity: UserAssignedIdentity option - } + type IngressApplicationGateway = { + Status: FeatureFlag + ApplicationGatewayId: ResourceId + Identity: UserAssignedIdentity option + } with - member internal this.ToArmJson = - {| - enabled = this.Status.AsBoolean - config = - match this.Status with - | Disabled -> Unchecked.defaultof<_> - | Enabled -> - {| - applicationGatewayId = this.ApplicationGatewayId.Eval() - |} - identity = - match this.Status, this.Identity with - | Disabled, _ - | Enabled, None -> Unchecked.defaultof<_> - | Enabled, Some userIdentity -> - {| - clientId = userIdentity.ClientId.Eval() - objectId = userIdentity.PrincipalId.ArmExpression.Eval() - resourceId = this.ApplicationGatewayId.Eval() - |} - |} + member internal this.ToArmJson = {| + enabled = this.Status.AsBoolean + config = + match this.Status with + | Disabled -> Unchecked.defaultof<_> + | Enabled -> {| + applicationGatewayId = this.ApplicationGatewayId.Eval() + |} + identity = + match this.Status, this.Identity with + | Disabled, _ + | Enabled, None -> Unchecked.defaultof<_> + | Enabled, Some userIdentity -> {| + clientId = userIdentity.ClientId.Eval() + objectId = userIdentity.PrincipalId.ArmExpression.Eval() + resourceId = this.ApplicationGatewayId.Eval() + |} + |} - type KubeDashboard = - { - Status: FeatureFlag - } + type KubeDashboard = { + Status: FeatureFlag + } with member internal this.ToArmJson = {| enabled = this.Status.AsBoolean |} - type OmsAgent = - { - Status: FeatureFlag - LogAnalyticsWorkspaceId: ResourceId option - } + type OmsAgent = { + Status: FeatureFlag + LogAnalyticsWorkspaceId: ResourceId option + } with - member internal this.ToArmJson = - {| - enabled = this.Status.AsBoolean - config = - match this.Status, this.LogAnalyticsWorkspaceId with - | Disabled, _ - | Enabled, None -> Unchecked.defaultof<_> - | Enabled, Some resId -> - {| - logAnalyticsWorkspaceResourceID = resId.Eval() - |} - |} + member internal this.ToArmJson = {| + enabled = this.Status.AsBoolean + config = + match this.Status, this.LogAnalyticsWorkspaceId with + | Disabled, _ + | Enabled, None -> Unchecked.defaultof<_> + | Enabled, Some resId -> {| + logAnalyticsWorkspaceResourceID = resId.Eval() + |} + |} - type AddonProfileConfig = - { - AciConnectorLinux: AciConnectorLinux option - HttpApplicationRouting: HttpApplicationRouting option - IngressApplicationGateway: IngressApplicationGateway option - KubeDashboard: KubeDashboard option - OmsAgent: OmsAgent option - } + type AddonProfileConfig = { + AciConnectorLinux: AciConnectorLinux option + HttpApplicationRouting: HttpApplicationRouting option + IngressApplicationGateway: IngressApplicationGateway option + KubeDashboard: KubeDashboard option + OmsAgent: OmsAgent option + } with - static member Default = - { - AciConnectorLinux = None - HttpApplicationRouting = None - IngressApplicationGateway = None - KubeDashboard = None - OmsAgent = None - } + static member Default = { + AciConnectorLinux = None + HttpApplicationRouting = None + IngressApplicationGateway = None + KubeDashboard = None + OmsAgent = None + } - let toArmJson (config: AddonProfileConfig) = - {| - aciConnectorLinux = - match config.AciConnectorLinux with - | None -> Unchecked.defaultof<_> - | Some aciConn -> aciConn.ToArmJson - httpApplicationRouting = - match config.HttpApplicationRouting with - | None -> Unchecked.defaultof<_> - | Some routing -> routing.ToArmJson - ingressApplicationGateway = - match config.IngressApplicationGateway with - | None -> Unchecked.defaultof<_> - | Some appGateway -> appGateway.ToArmJson - kubeDashboard = - match config.KubeDashboard with - | None -> Unchecked.defaultof<_> - | Some dashboard -> dashboard.ToArmJson - omsagent = - match config.OmsAgent with - | None -> Unchecked.defaultof<_> - | Some oms -> oms.ToArmJson - |} + let toArmJson (config: AddonProfileConfig) = {| + aciConnectorLinux = + match config.AciConnectorLinux with + | None -> Unchecked.defaultof<_> + | Some aciConn -> aciConn.ToArmJson + httpApplicationRouting = + match config.HttpApplicationRouting with + | None -> Unchecked.defaultof<_> + | Some routing -> routing.ToArmJson + ingressApplicationGateway = + match config.IngressApplicationGateway with + | None -> Unchecked.defaultof<_> + | Some appGateway -> appGateway.ToArmJson + kubeDashboard = + match config.KubeDashboard with + | None -> Unchecked.defaultof<_> + | Some dashboard -> dashboard.ToArmJson + omsagent = + match config.OmsAgent with + | None -> Unchecked.defaultof<_> + | Some oms -> oms.ToArmJson + |} type AgentPoolMode = | System | User /// Additional identity settings for the managed cluster, such as the identity for kubelet to pull container images. -type ManagedClusterIdentityProfile = - { - KubeletIdentity: ResourceId option - } +type ManagedClusterIdentityProfile = { + KubeletIdentity: ResourceId option +} with - member internal this.ToArmJson = - {| - kubeletIdentity = - match this.KubeletIdentity with - | Some kubeletIdentity -> - {| - resourceId = kubeletIdentity.Eval() - clientId = - ArmExpression - .reference(kubeletIdentity.Type, kubeletIdentity) - .Map(fun r -> r + ".clientId") - .Eval() - objectId = - ArmExpression - .reference(kubeletIdentity.Type, kubeletIdentity) - .Map(fun r -> r + ".principalId") - .Eval() - |} - | None -> Unchecked.defaultof<_> - |} + member internal this.ToArmJson = {| + kubeletIdentity = + match this.KubeletIdentity with + | Some kubeletIdentity -> {| + resourceId = kubeletIdentity.Eval() + clientId = + ArmExpression + .reference(kubeletIdentity.Type, kubeletIdentity) + .Map(fun r -> r + ".clientId") + .Eval() + objectId = + ArmExpression + .reference(kubeletIdentity.Type, kubeletIdentity) + .Map(fun r -> r + ".principalId") + .Eval() + |} + | None -> Unchecked.defaultof<_> + |} member internal this.Dependencies = [ this.KubeletIdentity ] |> List.choose id -type ManagedCluster = - { - Name: ResourceName - Location: Location - Dependencies: ResourceId Set - /// Dependencies that are expressed in ARM functions instead of a resource Id - DependencyExpressions: ArmExpression Set - AddOnProfiles: AddonProfiles.AddonProfileConfig option - AgentPoolProfiles: {| Name: ResourceName - Count: int - MaxPods: int option - Mode: AgentPoolMode - OsDiskSize: int - OsType: OS - VmSize: VMSize - VirtualNetworkName: ResourceName option - SubnetName: ResourceName option |} list - DnsPrefix: string - EnableRBAC: bool - Identity: ManagedIdentity - IdentityProfile: ManagedClusterIdentityProfile option - ApiServerAccessProfile: {| AuthorizedIPRanges: string list - EnablePrivateCluster: bool option |} option - LinuxProfile: {| AdminUserName: string - PublicKeys: string list |} option - NetworkProfile: {| NetworkPlugin: ContainerService.NetworkPlugin option - DnsServiceIP: System.Net.IPAddress option - DockerBridgeCidr: IPAddressCidr option - LoadBalancerSku: LoadBalancer.Sku option - ServiceCidr: IPAddressCidr option |} option - WindowsProfile: {| AdminUserName: string - AdminPassword: SecureParameter |} option - ServicePrincipalProfile: {| ClientId: string - ClientSecret: SecureParameter option |} - } +type ManagedCluster = { + Name: ResourceName + Location: Location + Dependencies: ResourceId Set + /// Dependencies that are expressed in ARM functions instead of a resource Id + DependencyExpressions: ArmExpression Set + AddOnProfiles: AddonProfiles.AddonProfileConfig option + AgentPoolProfiles: + {| + Name: ResourceName + Count: int + MaxPods: int option + Mode: AgentPoolMode + OsDiskSize: int + OsType: OS + VmSize: VMSize + VirtualNetworkName: ResourceName option + SubnetName: ResourceName option + |} list + DnsPrefix: string + EnableRBAC: bool + Identity: ManagedIdentity + IdentityProfile: ManagedClusterIdentityProfile option + ApiServerAccessProfile: + {| + AuthorizedIPRanges: string list + EnablePrivateCluster: bool option + |} option + LinuxProfile: + {| + AdminUserName: string + PublicKeys: string list + |} option + NetworkProfile: + {| + NetworkPlugin: ContainerService.NetworkPlugin option + DnsServiceIP: System.Net.IPAddress option + DockerBridgeCidr: IPAddressCidr option + LoadBalancerSku: LoadBalancer.Sku option + ServiceCidr: IPAddressCidr option + |} option + WindowsProfile: + {| + AdminUserName: string + AdminPassword: SecureParameter + |} option + ServicePrincipalProfile: {| + ClientId: string + ClientSecret: SecureParameter option + |} +} with interface IParameters with - member this.SecureParameters = - [ - yield! this.ServicePrincipalProfile.ClientSecret |> Option.mapList id - yield! this.WindowsProfile |> Option.mapList (fun wp -> wp.AdminPassword) - ] + member this.SecureParameters = [ + yield! this.ServicePrincipalProfile.ClientSecret |> Option.mapList id + yield! this.WindowsProfile |> Option.mapList (fun wp -> wp.AdminPassword) + ] interface IArmResource with member this.ResourceId = managedClusters.resourceId this.Name @@ -214,52 +213,51 @@ type ManagedCluster = |> Set.ofSeq |> Set.union this.Dependencies - {| managedClusters.Create(this.Name, this.Location) with - dependsOn = - [ - dependencies |> Seq.map (fun r -> r.Eval()) - this.DependencyExpressions |> Seq.map (fun r -> r.Eval()) - ] - |> Seq.concat - identity = // If using MSI but no identity was set, then enable the system identity like the CLI - if - this.ServicePrincipalProfile.ClientId = "msi" - && this.Identity.SystemAssigned = FeatureFlag.Disabled - && this.Identity.UserAssigned.Length = 0 - then - { - SystemAssigned = Enabled - UserAssigned = [] - } - .ToArmJson - else - this.Identity.ToArmJson - properties = - {| + {| + managedClusters.Create(this.Name, this.Location) with + dependsOn = + [ + dependencies |> Seq.map (fun r -> r.Eval()) + this.DependencyExpressions |> Seq.map (fun r -> r.Eval()) + ] + |> Seq.concat + identity = // If using MSI but no identity was set, then enable the system identity like the CLI + if + this.ServicePrincipalProfile.ClientId = "msi" + && this.Identity.SystemAssigned = FeatureFlag.Disabled + && this.Identity.UserAssigned.Length = 0 + then + { + SystemAssigned = Enabled + UserAssigned = [] + } + .ToArmJson + else + this.Identity.ToArmJson + properties = {| addonProfiles = this.AddOnProfiles |> Option.map AddonProfiles.toArmJson |> Option.defaultValue Unchecked.defaultof<_> agentPoolProfiles = this.AgentPoolProfiles - |> List.mapi (fun idx agent -> - {| - name = - if agent.Name = ResourceName.Empty then - $"nodepool{idx + 1}" - else - agent.Name.Value.ToLowerInvariant() - count = agent.Count - maxPods = agent.MaxPods |> Option.toNullable - mode = agent.Mode |> string - osDiskSizeGB = agent.OsDiskSize - osType = string agent.OsType - vmSize = agent.VmSize.ArmValue - vnetSubnetID = - match agent.VirtualNetworkName, agent.SubnetName with - | Some vnet, Some subnet -> subnets.resourceId(vnet, subnet).Eval() - | _ -> null - |}) + |> List.mapi (fun idx agent -> {| + name = + if agent.Name = ResourceName.Empty then + $"nodepool{idx + 1}" + else + agent.Name.Value.ToLowerInvariant() + count = agent.Count + maxPods = agent.MaxPods |> Option.toNullable + mode = agent.Mode |> string + osDiskSizeGB = agent.OsDiskSize + osType = string agent.OsType + vmSize = agent.VmSize.ArmValue + vnetSubnetID = + match agent.VirtualNetworkName, agent.SubnetName with + | Some vnet, Some subnet -> subnets.resourceId(vnet, subnet).Eval() + | _ -> null + |}) dnsPrefix = this.DnsPrefix enableRBAC = this.EnableRBAC identityProfile = @@ -268,61 +266,54 @@ type ManagedCluster = | None -> Unchecked.defaultof<_> apiServerAccessProfile = match this.ApiServerAccessProfile with - | Some apiServerProfile -> - {| - authorizedIPRanges = apiServerProfile.AuthorizedIPRanges - enablePrivateCluster = - apiServerProfile.EnablePrivateCluster |> Option.map box |> Option.toObj - |} + | Some apiServerProfile -> {| + authorizedIPRanges = apiServerProfile.AuthorizedIPRanges + enablePrivateCluster = + apiServerProfile.EnablePrivateCluster |> Option.map box |> Option.toObj + |} | None -> Unchecked.defaultof<_> linuxProfile = match this.LinuxProfile with - | Some linuxProfile -> - {| - adminUsername = linuxProfile.AdminUserName - ssh = - {| - publicKeys = - linuxProfile.PublicKeys |> List.map (fun k -> {| keyData = k |}) - |} + | Some linuxProfile -> {| + adminUsername = linuxProfile.AdminUserName + ssh = {| + publicKeys = linuxProfile.PublicKeys |> List.map (fun k -> {| keyData = k |}) |} + |} | None -> Unchecked.defaultof<_> networkProfile = match this.NetworkProfile with - | Some networkProfile -> - {| - dnsServiceIP = networkProfile.DnsServiceIP |> Option.map string |> Option.toObj - dockerBridgeCidr = - networkProfile.DockerBridgeCidr - |> Option.map IPAddressCidr.format - |> Option.toObj - loadBalancerSku = - networkProfile.LoadBalancerSku - |> Option.map (fun sku -> sku.ArmValue) - |> Option.toObj - networkPlugin = - networkProfile.NetworkPlugin - |> Option.map (fun plugin -> plugin.ArmValue) - |> Option.toObj - serviceCidr = - networkProfile.ServiceCidr |> Option.map IPAddressCidr.format |> Option.toObj - |} - | None -> Unchecked.defaultof<_> - servicePrincipalProfile = - {| - clientId = this.ServicePrincipalProfile.ClientId - secret = - this.ServicePrincipalProfile.ClientSecret - |> Option.map (fun clientSecret -> clientSecret.ArmExpression.Eval()) + | Some networkProfile -> {| + dnsServiceIP = networkProfile.DnsServiceIP |> Option.map string |> Option.toObj + dockerBridgeCidr = + networkProfile.DockerBridgeCidr + |> Option.map IPAddressCidr.format + |> Option.toObj + loadBalancerSku = + networkProfile.LoadBalancerSku + |> Option.map (fun sku -> sku.ArmValue) |> Option.toObj - |} + networkPlugin = + networkProfile.NetworkPlugin + |> Option.map (fun plugin -> plugin.ArmValue) + |> Option.toObj + serviceCidr = + networkProfile.ServiceCidr |> Option.map IPAddressCidr.format |> Option.toObj + |} + | None -> Unchecked.defaultof<_> + servicePrincipalProfile = {| + clientId = this.ServicePrincipalProfile.ClientId + secret = + this.ServicePrincipalProfile.ClientSecret + |> Option.map (fun clientSecret -> clientSecret.ArmExpression.Eval()) + |> Option.toObj + |} windowsProfile = match this.WindowsProfile with - | Some winProfile -> - {| - adminUsername = winProfile.AdminUserName - adminPassword = winProfile.AdminPassword.ArmExpression.Eval() - |} + | Some winProfile -> {| + adminUsername = winProfile.AdminUserName + adminPassword = winProfile.AdminPassword.ArmExpression.Eval() + |} | None -> Unchecked.defaultof<_> |} |} diff --git a/src/Farmer/Arm/DBforPostgreSQL.fs b/src/Farmer/Arm/DBforPostgreSQL.fs index 70308d351..cf2397da8 100644 --- a/src/Farmer/Arm/DBforPostgreSQL.fs +++ b/src/Farmer/Arm/DBforPostgreSQL.fs @@ -30,103 +30,92 @@ type PostgreSQLFamily = module Servers = - type Database = - { - Name: ResourceName - Server: ResourceName - Charset: string - Collation: string - } + type Database = { + Name: ResourceName + Server: ResourceName + Charset: string + Collation: string + } with interface IArmResource with member this.ResourceId = databases.resourceId (this.Server / this.Name) - member this.JsonModel = - {| databases.Create(this.Server / this.Name, dependsOn = [ servers.resourceId this.Server ]) with - properties = - {| - charset = this.Charset - collation = this.Collation - |} - |} - - type FirewallRule = - { - Name: ResourceName - Server: ResourceName - Start: IPAddress - End: IPAddress - Location: Location - } + member this.JsonModel = {| + databases.Create(this.Server / this.Name, dependsOn = [ servers.resourceId this.Server ]) with + properties = {| + charset = this.Charset + collation = this.Collation + |} + |} + + type FirewallRule = { + Name: ResourceName + Server: ResourceName + Start: IPAddress + End: IPAddress + Location: Location + } with interface IArmResource with member this.ResourceId = firewallRules.resourceId (this.Server / this.Name) - member this.JsonModel = - {| firewallRules.Create(this.Server / this.Name, this.Location, [ servers.resourceId this.Server ]) with - properties = - {| - startIpAddress = string this.Start - endIpAddress = string this.End - |} - |} - - type VnetRule = - { - Name: ResourceName - Server: ResourceName - VirtualNetworkSubnetId: ResourceId - Location: Location - } + member this.JsonModel = {| + firewallRules.Create(this.Server / this.Name, this.Location, [ servers.resourceId this.Server ]) with + properties = {| + startIpAddress = string this.Start + endIpAddress = string this.End + |} + |} + + type VnetRule = { + Name: ResourceName + Server: ResourceName + VirtualNetworkSubnetId: ResourceId + Location: Location + } with interface IArmResource with member this.ResourceId = virtualNetworkRules.resourceId (this.Server / this.Name) - member this.JsonModel = - {| virtualNetworkRules.Create( - this.Server / this.Name, - this.Location, - [ servers.resourceId this.Server ] - ) with - properties = - {| - virtualNetworkSubnetId = this.VirtualNetworkSubnetId.Eval() - |} - |} - -type Server = - { - Name: ResourceName - Location: Location - Credentials: {| Username: string - Password: SecureParameter |} - Version: Version - Capacity: int - StorageSize: int - Tier: Sku - Family: PostgreSQLFamily - GeoRedundantBackup: FeatureFlag - StorageAutoGrow: FeatureFlag - BackupRetention: int - Tags: Map - } - - member this.Sku = - {| - name = $"{this.Tier.Name}_{this.Family.AsArmValue}_{this.Capacity}" - tier = string this.Tier - capacity = this.Capacity - family = string this.Family - size = string this.StorageSize - |} + member this.JsonModel = {| + virtualNetworkRules.Create(this.Server / this.Name, this.Location, [ servers.resourceId this.Server ]) with + properties = {| + virtualNetworkSubnetId = this.VirtualNetworkSubnetId.Eval() + |} + |} - member this.GetStorageProfile() = - {| - storageMB = this.StorageSize - backupRetentionDays = this.BackupRetention - geoRedundantBackup = string this.GeoRedundantBackup - storageAutoGrow = string this.StorageAutoGrow - |} +type Server = { + Name: ResourceName + Location: Location + Credentials: {| + Username: string + Password: SecureParameter + |} + Version: Version + Capacity: int + StorageSize: int + Tier: Sku + Family: PostgreSQLFamily + GeoRedundantBackup: FeatureFlag + StorageAutoGrow: FeatureFlag + BackupRetention: int + Tags: Map +} with + + member this.Sku = {| + name = $"{this.Tier.Name}_{this.Family.AsArmValue}_{this.Capacity}" + tier = string this.Tier + capacity = this.Capacity + family = string this.Family + size = string this.StorageSize + |} + + member this.GetStorageProfile() = {| + storageMB = this.StorageSize + backupRetentionDays = this.BackupRetention + geoRedundantBackup = string this.GeoRedundantBackup + storageAutoGrow = string this.StorageAutoGrow + |} member this.GetProperties() = let version = @@ -149,8 +138,8 @@ type Server = interface IArmResource with member this.ResourceId = servers.resourceId this.Name - member this.JsonModel = - {| servers.Create(this.Name, this.Location, tags = (this.Tags |> Map.add "displayName" this.Name.Value)) with + member this.JsonModel = {| + servers.Create(this.Name, this.Location, tags = (this.Tags |> Map.add "displayName" this.Name.Value)) with sku = this.Sku properties = this.GetProperties() - |} + |} diff --git a/src/Farmer/Arm/Dashboard.fs b/src/Farmer/Arm/Dashboard.fs index 021584a0f..bbf7402ef 100644 --- a/src/Farmer/Arm/Dashboard.fs +++ b/src/Farmer/Arm/Dashboard.fs @@ -11,242 +11,211 @@ type DashboardMetadata = | CustomMetadata of obj | Cache24h -type LensAsset = - { - idInputName: string - ``type``: string - } +type LensAsset = { + idInputName: string + ``type``: string +} -type LensMetadata = - { - ``type``: string - inputs: obj list - settings: obj - filters: obj - asset: LensAsset - isAdapter: System.Nullable - defaultMenuItemId: string - } +type LensMetadata = { + ``type``: string + inputs: obj list + settings: obj + filters: obj + asset: LensAsset + isAdapter: System.Nullable + defaultMenuItemId: string +} -type LensPosition = - { - x: int - y: int - rowSpan: int - colSpan: int - } +type LensPosition = { + x: int + y: int + rowSpan: int + colSpan: int +} -type LensPart = - { - position: LensPosition - metadata: LensMetadata - } +type LensPart = { + position: LensPosition + metadata: LensMetadata +} -type MarkdownPartParameters = - { - title: string - subtitle: string - content: string - } +type MarkdownPartParameters = { + title: string + subtitle: string + content: string +} /// Generates a MarkdownPart -let generateMarkdownPart (markdownProperties: MarkdownPartParameters) = - { - ``type`` = "Extension[azure]/HubsExtension/PartType/MarkdownPart" - inputs = List.empty - settings = - {| - content = markdownProperties.content - title = markdownProperties.title - subtitle = markdownProperties.subtitle - |} - :> obj - filters = null - asset = Unchecked.defaultof - isAdapter = System.Nullable() - defaultMenuItemId = null - } +let generateMarkdownPart (markdownProperties: MarkdownPartParameters) = { + ``type`` = "Extension[azure]/HubsExtension/PartType/MarkdownPart" + inputs = List.empty + settings = + {| + content = markdownProperties.content + title = markdownProperties.title + subtitle = markdownProperties.subtitle + |} + :> obj + filters = null + asset = Unchecked.defaultof + isAdapter = System.Nullable() + defaultMenuItemId = null +} -type VideoPartParameters = - { - title: string - subtitle: string - url: string - } +type VideoPartParameters = { + title: string + subtitle: string + url: string +} /// Generates a VideoPart -let generateVideoPart (videoProperties: VideoPartParameters) = - { - ``type`` = "Extension[azure]/HubsExtension/PartType/VideoPart" - inputs = List.empty - settings = - {| - content = - {| - settings = - {| - title = videoProperties.title - subtitle = videoProperties.subtitle - src = videoProperties.url - autoplay = false - |} - |} +let generateVideoPart (videoProperties: VideoPartParameters) = { + ``type`` = "Extension[azure]/HubsExtension/PartType/VideoPart" + inputs = List.empty + settings = + {| + content = {| + settings = {| + title = videoProperties.title + subtitle = videoProperties.subtitle + src = videoProperties.url + autoplay = false + |} |} - :> obj - filters = null - asset = Unchecked.defaultof - isAdapter = System.Nullable() - defaultMenuItemId = null - } + |} + :> obj + filters = null + asset = Unchecked.defaultof + isAdapter = System.Nullable() + defaultMenuItemId = null +} /// Generates a virtualMachinePart -let generateVirtualMachinePart (vmId: ResourceId) = - { - ``type`` = "Extension/Microsoft_Azure_Compute/PartType/VirtualMachinePart" - inputs = - [ - {| - name = "id" - value = vmId.ArmExpression.Eval() - |} - :> obj - ] - settings = null - filters = null - asset = - { - idInputName = "id" - ``type`` = "VirtualMachine" - } - isAdapter = System.Nullable() - defaultMenuItemId = "overview" +let generateVirtualMachinePart (vmId: ResourceId) = { + ``type`` = "Extension/Microsoft_Azure_Compute/PartType/VirtualMachinePart" + inputs = [ + {| + name = "id" + value = vmId.ArmExpression.Eval() + |} + :> obj + ] + settings = null + filters = null + asset = { + idInputName = "id" + ``type`` = "VirtualMachine" } + isAdapter = System.Nullable() + defaultMenuItemId = "overview" +} /// Generates a webtest part -let generateWebtestResultPart (applicationInsightsName: string) = - { - ``type`` = "Extension/AppInsightsExtension/PartType/AllWebTestsResponseTimeFullGalleryAdapterPart" - inputs = - [ - {| - name = "ComponentId" - value = - {| - Name = applicationInsightsName - SubscriptionId = "[ subscription().subscriptionId ]" - ResourceGroup = "[ resourceGroup().name ]" - |} - |} - ] - settings = null - filters = null - asset = - { - idInputName = "ComponentId" - ``type`` = "ApplicationInsights" - } - isAdapter = System.Nullable(true) - defaultMenuItemId = null +let generateWebtestResultPart (applicationInsightsName: string) = { + ``type`` = "Extension/AppInsightsExtension/PartType/AllWebTestsResponseTimeFullGalleryAdapterPart" + inputs = [ + {| + name = "ComponentId" + value = {| + Name = applicationInsightsName + SubscriptionId = "[ subscription().subscriptionId ]" + ResourceGroup = "[ resourceGroup().name ]" + |} + |} + ] + settings = null + filters = null + asset = { + idInputName = "ComponentId" + ``type`` = "ApplicationInsights" } + isAdapter = System.Nullable(true) + defaultMenuItemId = null +} -type MetrixChartParameters = - { - resourceId: ResourceId - metrics: MetricsName list - interval: IsoDateTime - } +type MetrixChartParameters = { + resourceId: ResourceId + metrics: MetricsName list + interval: IsoDateTime +} /// Generates a MetricsChartPart for a resource given in parameters -let generateMetricsChartPart (chartProperties: MetrixChartParameters) = - { - ``type`` = "Extension/Microsoft_Azure_Monitoring/PartType/MetricsChartPart" - inputs = - [ - {| - name = "queryInputs" - value = - {| - id = chartProperties.resourceId.ArmExpression.Eval() - chartType = 0 - timespan = - {| - duration = - match chartProperties.interval with - | IsoDateTime dur -> dur - start = null - ``end`` = null - |} - metrics = - chartProperties.metrics - |> List.map (function - | MetricsName m -> - {| - name = m - resourceId = chartProperties.resourceId.ArmExpression.Eval() - |}) - |} +let generateMetricsChartPart (chartProperties: MetrixChartParameters) = { + ``type`` = "Extension/Microsoft_Azure_Monitoring/PartType/MetricsChartPart" + inputs = [ + {| + name = "queryInputs" + value = {| + id = chartProperties.resourceId.ArmExpression.Eval() + chartType = 0 + timespan = {| + duration = + match chartProperties.interval with + | IsoDateTime dur -> dur + start = null + ``end`` = null |} - ] - settings = null - filters = null - asset = Unchecked.defaultof - isAdapter = System.Nullable() - defaultMenuItemId = null - } + metrics = + chartProperties.metrics + |> List.map (function + | MetricsName m -> {| + name = m + resourceId = chartProperties.resourceId.ArmExpression.Eval() + |}) + |} + |} + ] + settings = null + filters = null + asset = Unchecked.defaultof + isAdapter = System.Nullable() + defaultMenuItemId = null +} -type MonitorChartParameters = - { - chartInputs: obj list - chartSettings: obj - filters: obj - } +type MonitorChartParameters = { + chartInputs: obj list + chartSettings: obj + filters: obj +} /// Generates a MonitorChartPart -let generateMonitorChartPart (chartProperties: MonitorChartParameters) = - { - ``type`` = "Extension/HubsExtension/PartType/MonitorChartPart" - inputs = - [ - box - <| {| - name = "sharedTimeRange" - isOptional = true - |} - box - <| {| - name = "options" - value = - {| - v2charts = true - charts = chartProperties.chartInputs - |} - |} - ] - settings = - {| - content = - {| - options = - {| - chart = chartProperties.chartSettings - |} - |} +let generateMonitorChartPart (chartProperties: MonitorChartParameters) = { + ``type`` = "Extension/HubsExtension/PartType/MonitorChartPart" + inputs = [ + box + <| {| + name = "sharedTimeRange" + isOptional = true + |} + box + <| {| + name = "options" + value = {| + v2charts = true + charts = chartProperties.chartInputs + |} + |} + ] + settings = {| + content = {| + options = {| + chart = chartProperties.chartSettings |} - filters = chartProperties.filters - asset = Unchecked.defaultof - isAdapter = System.Nullable() - defaultMenuItemId = null - } + |} + |} + filters = chartProperties.filters + asset = Unchecked.defaultof + isAdapter = System.Nullable() + defaultMenuItemId = null +} -type Dashboard = - { - Name: ResourceName - Title: string option - Location: Location - Metadata: DashboardMetadata - LensParts: LensPart list - Dependencies: Set - } +type Dashboard = { + Name: ResourceName + Title: string option + Location: Location + Metadata: DashboardMetadata + LensParts: LensPart list + Dependencies: Set +} with interface IArmResource with member this.ResourceId = dashboard.resourceId this.Name @@ -258,62 +227,51 @@ type Dashboard = | Some title -> title | None -> this.Name.Value - {| dashboard.Create(this.Name, this.Location, dependsOn = this.Dependencies) with - tags = {| ``hidden-title`` = dahsboardTitle |} - id = - ArmExpression - .create( - $"concat('/subscriptions/', subscription().subscriptionId, '/resourceGroups/', resourceGroup().name, '/providers/Microsoft.Portal/dashboards/', '{this.Name.Value}')" - ) - .Eval() - properties = - {| + {| + dashboard.Create(this.Name, this.Location, dependsOn = this.Dependencies) with + tags = {| ``hidden-title`` = dahsboardTitle |} + id = + ArmExpression + .create( + $"concat('/subscriptions/', subscription().subscriptionId, '/resourceGroups/', resourceGroup().name, '/providers/Microsoft.Portal/dashboards/', '{this.Name.Value}')" + ) + .Eval() + properties = {| metadata = match this.Metadata with - | EmptyMetadata -> {| |} :> obj + | EmptyMetadata -> {| |} :> obj | CustomMetadata metad -> metad - | Cache24h -> - {| - model = - {| - timeRange = - {| - ``type`` = - "MsPortalFx.Composition.Configuration.ValueTypes.TimeRange" - value = - {| - relative = {| duration = 24; timeUnit = 1 |} - |} + | Cache24h -> {| + model = {| + timeRange = {| + ``type`` = "MsPortalFx.Composition.Configuration.ValueTypes.TimeRange" + value = {| + relative = {| duration = 24; timeUnit = 1 |} + |} + |} + filterLocale = {| value = "en-us" |} + filters = {| + value = {| + MsPortalFx_TimeRange = {| + model = {| + format = "utc" + granularity = "auto" + relative = "24h" |} - filterLocale = {| value = "en-us" |} - filters = - {| - value = - {| - MsPortalFx_TimeRange = - {| - model = - {| - format = "utc" - granularity = "auto" - relative = "24h" - |} - displayCache = - {| - name = "UTC Time" - value = "Past 24 hours" - |} - |} - |} + displayCache = {| + name = "UTC Time" + value = "Past 24 hours" |} + |} |} + |} |} - lenses = - [ - {| - order = "0" - parts = this.LensParts - |} - ] + |} + lenses = [ + {| + order = "0" + parts = this.LensParts + |} + ] |} |} diff --git a/src/Farmer/Arm/DataLakeStore.fs b/src/Farmer/Arm/DataLakeStore.fs index 6b97ede17..e09deebf8 100644 --- a/src/Farmer/Arm/DataLakeStore.fs +++ b/src/Farmer/Arm/DataLakeStore.fs @@ -6,23 +6,21 @@ open Farmer.DataLake let accounts = ResourceType("Microsoft.DataLakeStore/accounts", "2016-11-01") -type Account = - { - Name: ResourceName - Location: Location - EncryptionState: FeatureFlag - Sku: Sku - Tags: Map - } +type Account = { + Name: ResourceName + Location: Location + EncryptionState: FeatureFlag + Sku: Sku + Tags: Map +} with interface IArmResource with member this.ResourceId = accounts.resourceId this.Name - member this.JsonModel = - {| accounts.Create(this.Name, this.Location, tags = this.Tags) with - properties = - {| - newTier = this.Sku.ToString() - encryptionState = this.EncryptionState.ToString() - |} - |} + member this.JsonModel = {| + accounts.Create(this.Name, this.Location, tags = this.Tags) with + properties = {| + newTier = this.Sku.ToString() + encryptionState = this.EncryptionState.ToString() + |} + |} diff --git a/src/Farmer/Arm/Databricks.fs b/src/Farmer/Arm/Databricks.fs index cbf3cedce..38a686469 100644 --- a/src/Farmer/Arm/Databricks.fs +++ b/src/Farmer/Arm/Databricks.fs @@ -8,91 +8,83 @@ open System let workspaces = ResourceType("Microsoft.Databricks/workspaces", "2018-04-01") -type VnetConfig = - { - Vnet: ResourceId - PublicSubnet: ResourceName - PrivateSubnet: ResourceName - } +type VnetConfig = { + Vnet: ResourceId + PublicSubnet: ResourceName + PrivateSubnet: ResourceName +} type KeyEncryption = | InfrastructureManaged | CustomerManaged of - {| Vault: ResourceId - Key: string - KeyVersion: Guid option |} + {| + Vault: ResourceId + Key: string + KeyVersion: Guid option + |} -type Workspace = - { - Name: ResourceName - Location: Location - ManagedResourceGroupId: ResourceName - Sku: Sku - EnablePublicIp: FeatureFlag - KeyEncryption: KeyEncryption option - VnetConfig: VnetConfig option - Tags: Map - Dependencies: ResourceId Set - } +type Workspace = { + Name: ResourceName + Location: Location + ManagedResourceGroupId: ResourceName + Sku: Sku + EnablePublicIp: FeatureFlag + KeyEncryption: KeyEncryption option + VnetConfig: VnetConfig option + Tags: Map + Dependencies: ResourceId Set +} with interface IArmResource with member this.ResourceId = workspaces.resourceId this.Name - member this.JsonModel = - {| workspaces.Create(this.Name, this.Location, this.Dependencies, this.Tags) with + member this.JsonModel = {| + workspaces.Create(this.Name, this.Location, this.Dependencies, this.Tags) with sku = {| name = this.Sku.ArmValue |} - properties = - {| - managedResourceGroupId = - ArmExpression - .create( - $"concat(subscription().id, '/resourceGroups/', '{this.ManagedResourceGroupId.Value}')" - ) - .Eval() - parameters = - Map - [ - "enableNoPublicIp", - box - {| - value = (not this.EnablePublicIp.AsBoolean) - |} - "prepareEncryption", - box - {| - value = Option.isSome this.KeyEncryption - |} - match this.VnetConfig with - | Some config -> - "customVirtualNetworkId", box {| value = config.Vnet.Eval() |} - "customPublicSubnetName", box {| value = config.PublicSubnet.Value |} - "customPrivateSubnetName", box {| value = config.PrivateSubnet.Value |} - | None -> () - match this.KeyEncryption with - | Some config -> - "encryption", - {| - value = - match config with - | CustomerManaged config -> - {| - keySource = "Microsoft.Keyvault" - keyName = config.Key - keyversion = - config.KeyVersion |> Option.map string |> Option.toObj - keyvaulturi = - $"https://{config.Vault.Name.Value}.vault.azure.net" - |} - | InfrastructureManaged -> - {| - keySource = "Default" - keyName = null - keyversion = null - keyvaulturi = null - |} - |} - |> box - | None -> () - ] - |} - |} + properties = {| + managedResourceGroupId = + ArmExpression + .create( + $"concat(subscription().id, '/resourceGroups/', '{this.ManagedResourceGroupId.Value}')" + ) + .Eval() + parameters = + Map [ + "enableNoPublicIp", + box {| + value = (not this.EnablePublicIp.AsBoolean) + |} + "prepareEncryption", + box {| + value = Option.isSome this.KeyEncryption + |} + match this.VnetConfig with + | Some config -> + "customVirtualNetworkId", box {| value = config.Vnet.Eval() |} + "customPublicSubnetName", box {| value = config.PublicSubnet.Value |} + "customPrivateSubnetName", box {| value = config.PrivateSubnet.Value |} + | None -> () + match this.KeyEncryption with + | Some config -> + "encryption", + {| + value = + match config with + | CustomerManaged config -> {| + keySource = "Microsoft.Keyvault" + keyName = config.Key + keyversion = config.KeyVersion |> Option.map string |> Option.toObj + keyvaulturi = $"https://{config.Vault.Name.Value}.vault.azure.net" + |} + | InfrastructureManaged -> {| + keySource = "Default" + keyName = null + keyversion = null + keyvaulturi = null + |} + |} + |> box + | None -> () + ] + |} + |} diff --git a/src/Farmer/Arm/DeploymentScript.fs b/src/Farmer/Arm/DeploymentScript.fs index 0bf6f8aa8..91147a656 100644 --- a/src/Farmer/Arm/DeploymentScript.fs +++ b/src/Farmer/Arm/DeploymentScript.fs @@ -23,31 +23,29 @@ type Cleanup = | OnSuccess | OnExpiration of TimeSpan -type DeploymentScript = - { - Name: ResourceName - Location: Location - Dependencies: ResourceId Set - Arguments: string list - CleanupPreference: Cleanup - Cli: CliVersion - EnvironmentVariables: Map - ForceUpdateTag: Guid option - Identity: UserAssignedIdentity - ScriptSource: ScriptSource - SupportingScriptUris: Uri list - Timeout: TimeSpan option - Tags: Map - } +type DeploymentScript = { + Name: ResourceName + Location: Location + Dependencies: ResourceId Set + Arguments: string list + CleanupPreference: Cleanup + Cli: CliVersion + EnvironmentVariables: Map + ForceUpdateTag: Guid option + Identity: UserAssignedIdentity + ScriptSource: ScriptSource + SupportingScriptUris: Uri list + Timeout: TimeSpan option + Tags: Map +} with interface IParameters with - member this.SecureParameters = - [ - for envVar in this.EnvironmentVariables do - match envVar.Value with - | SecureEnvValue p -> p - | _ -> () - ] + member this.SecureParameters = [ + for envVar in this.EnvironmentVariables do + match envVar.Value with + | SecureEnvValue p -> p + | _ -> () + ] interface IArmResource with member this.ResourceId = deploymentScripts.resourceId this.Name @@ -60,16 +58,16 @@ type DeploymentScript = let dependencies = this.Dependencies.Add this.Identity.ResourceId - {| deploymentScripts.Create(this.Name, this.Location, dependencies, this.Tags) with - kind = cliKind - identity = - { - SystemAssigned = Disabled - UserAssigned = [ this.Identity ] - } - .ToArmJson - properties = - {| + {| + deploymentScripts.Create(this.Name, this.Location, dependencies, this.Tags) with + kind = cliKind + identity = + { + SystemAssigned = Disabled + UserAssigned = [ this.Identity ] + } + .ToArmJson + properties = {| arguments = match this.Arguments with | [] -> null @@ -81,29 +79,25 @@ type DeploymentScript = | Cleanup.OnSuccess -> "OnSuccess" | Cleanup.OnExpiration _ -> "OnExpiration" | Cleanup.Always -> "Always" - environmentVariables = - [ - for key, value in Map.toSeq this.EnvironmentVariables do - match value with - | EnvValue v -> - {| - name = key - value = v - secureValue = null - |} - | SecureEnvExpression armExpression -> - {| - name = key - value = null - secureValue = armExpression.Eval() - |} - | SecureEnvValue v -> - {| - name = key - value = null - secureValue = v.ArmExpression.Eval() - |} - ] + environmentVariables = [ + for key, value in Map.toSeq this.EnvironmentVariables do + match value with + | EnvValue v -> {| + name = key + value = v + secureValue = null + |} + | SecureEnvExpression armExpression -> {| + name = key + value = null + secureValue = armExpression.Eval() + |} + | SecureEnvValue v -> {| + name = key + value = null + secureValue = v.ArmExpression.Eval() + |} + ] forceUpdateTag = this.ForceUpdateTag |> Option.toNullable scriptContent = match this.ScriptSource with diff --git a/src/Farmer/Arm/Devices.fs b/src/Farmer/Arm/Devices.fs index 70fdc7309..237787d39 100644 --- a/src/Farmer/Arm/Devices.fs +++ b/src/Farmer/Arm/Devices.fs @@ -23,97 +23,88 @@ type Sku = member this.Name = match this with | Free -> "F1" - | Paid (name, _) -> name + | Paid(name, _) -> name member this.Capacity = match this with | Free -> 1 - | Paid (_, capacity) -> capacity - -type DeliveryDetails = - {| Ttl: IsoDateTime option - LockDuration: IsoDateTime option - MaxDeliveryCount: int option |} - -let serialize (d: DeliveryDetails) = - {| - ttlAsIso8601 = d.Ttl |> Option.map (fun f -> f.Value) |> Option.toObj - lockDurationAsIso8601 = d.LockDuration |> Option.map (fun f -> f.Value) |> Option.toObj - maxDeliveryCount = d.MaxDeliveryCount |> Option.toNullable - |} - -type iotHubs = - { - Name: ResourceName - Location: Location - Sku: Sku - RetentionDays: int option - PartitionCount: int option - DefaultTtl: IsoDateTime option - MaxDeliveryCount: int option - Feedback: DeliveryDetails option - FileNotifications: DeliveryDetails option - Tags: Map - } + | Paid(_, capacity) -> capacity + +type DeliveryDetails = {| + Ttl: IsoDateTime option + LockDuration: IsoDateTime option + MaxDeliveryCount: int option +|} + +let serialize (d: DeliveryDetails) = {| + ttlAsIso8601 = d.Ttl |> Option.map (fun f -> f.Value) |> Option.toObj + lockDurationAsIso8601 = d.LockDuration |> Option.map (fun f -> f.Value) |> Option.toObj + maxDeliveryCount = d.MaxDeliveryCount |> Option.toNullable +|} + +type iotHubs = { + Name: ResourceName + Location: Location + Sku: Sku + RetentionDays: int option + PartitionCount: int option + DefaultTtl: IsoDateTime option + MaxDeliveryCount: int option + Feedback: DeliveryDetails option + FileNotifications: DeliveryDetails option + Tags: Map +} with interface IArmResource with member this.ResourceId = iotHubs.resourceId this.Name - member this.JsonModel = - {| iotHubs.Create(this.Name, this.Location, tags = this.Tags) with - properties = - {| - eventHubEndpoints = - match this.RetentionDays, this.PartitionCount with - | None, None -> null - | _ -> - box - {| - events = - {| - retentionTimeInDays = this.RetentionDays |> Option.toNullable - partitionCount = this.PartitionCount |> Option.toNullable - |} - |} - cloudToDevice = - match this with - | { - DefaultTtl = None - MaxDeliveryCount = None - Feedback = None - } -> null - | _ -> - box - {| - defaultTtlAsIso8601 = - this.DefaultTtl |> Option.map (fun v -> v.Value) |> Option.toObj - maxDeliveryCount = this.MaxDeliveryCount |> Option.toNullable - feedback = this.Feedback |> Option.map (serialize >> box) |> Option.toObj - |} - messagingEndpoints = - this.FileNotifications - |> Option.map (fun fileNotifications -> - box - {| - fileNotifications = fileNotifications |> serialize - |}) - |> Option.toObj - |} - sku = - {| - name = this.Sku.Name - capacity = this.Sku.Capacity - |} - |} - -type ProvisioningServices = - { - Name: ResourceName - Location: Location - IotHubName: ResourceName - IotHubKey: ArmExpression - Tags: Map - } + member this.JsonModel = {| + iotHubs.Create(this.Name, this.Location, tags = this.Tags) with + properties = {| + eventHubEndpoints = + match this.RetentionDays, this.PartitionCount with + | None, None -> null + | _ -> + box {| + events = {| + retentionTimeInDays = this.RetentionDays |> Option.toNullable + partitionCount = this.PartitionCount |> Option.toNullable + |} + |} + cloudToDevice = + match this with + | { + DefaultTtl = None + MaxDeliveryCount = None + Feedback = None + } -> null + | _ -> + box {| + defaultTtlAsIso8601 = this.DefaultTtl |> Option.map (fun v -> v.Value) |> Option.toObj + maxDeliveryCount = this.MaxDeliveryCount |> Option.toNullable + feedback = this.Feedback |> Option.map (serialize >> box) |> Option.toObj + |} + messagingEndpoints = + this.FileNotifications + |> Option.map (fun fileNotifications -> + box {| + fileNotifications = fileNotifications |> serialize + |}) + |> Option.toObj + |} + sku = {| + name = this.Sku.Name + capacity = this.Sku.Capacity + |} + |} + +type ProvisioningServices = { + Name: ResourceName + Location: Location + IotHubName: ResourceName + IotHubKey: ArmExpression + Tags: Map +} with member this.IotHubConnection = ArmExpression.create @@ -124,18 +115,16 @@ type ProvisioningServices = interface IArmResource with member this.ResourceId = provisioningServices.resourceId this.Name - member this.JsonModel = - {| provisioningServices.Create(this.Name, this.Location, [ iotHubs.resourceId this.IotHubName ], this.Tags) with + member this.JsonModel = {| + provisioningServices.Create(this.Name, this.Location, [ iotHubs.resourceId this.IotHubName ], this.Tags) with sku = {| name = "S1"; capacity = 1 |} - properties = - {| - iotHubs = - [ - {| - connectionString = this.IotHubConnection.Eval() - location = this.Location.ArmValue - name = this.IotHubPath - |} - ] - |} - |} + properties = {| + iotHubs = [ + {| + connectionString = this.IotHubConnection.Eval() + location = this.Location.ArmValue + name = this.IotHubPath + |} + ] + |} + |} diff --git a/src/Farmer/Arm/DiagnosticSetting.fs b/src/Farmer/Arm/DiagnosticSetting.fs index a09262def..4ae1829c0 100644 --- a/src/Farmer/Arm/DiagnosticSetting.fs +++ b/src/Farmer/Arm/DiagnosticSetting.fs @@ -7,25 +7,26 @@ open Farmer.DiagnosticSettings let diagnosticSettingsType (parent: ResourceType) = ResourceType(parent.Type + "/providers/diagnosticSettings", "2017-05-01-preview") -type SinkInformation = - { - StorageAccount: ResourceId option - EventHub: {| AuthorizationRuleId: ResourceId - EventHubName: ResourceName option |} option - LogAnalyticsWorkspace: (ResourceId * LogAnalyticsDestination) option - } +type SinkInformation = { + StorageAccount: ResourceId option + EventHub: + {| + AuthorizationRuleId: ResourceId + EventHubName: ResourceName option + |} option + LogAnalyticsWorkspace: (ResourceId * LogAnalyticsDestination) option +} -type DiagnosticSettings = - { - Name: ResourceName - Location: Location - MetricsSource: ResourceId - Sinks: SinkInformation - Metrics: MetricSetting Set - Logs: LogSetting Set - Dependencies: ResourceId Set - Tags: Map - } +type DiagnosticSettings = { + Name: ResourceName + Location: Location + MetricsSource: ResourceId + Sinks: SinkInformation + Metrics: MetricSetting Set + Logs: LogSetting Set + Dependencies: ResourceId Set + Tags: Map +} with interface IArmResource with member this.ResourceId = @@ -41,15 +42,15 @@ type DiagnosticSettings = ] |> List.reduce (/) - {| diagnosticSettingsType(this.MetricsSource.Type) - .Create(resourceName, this.Location, this.Dependencies, this.Tags) with - properties = - {| + {| + diagnosticSettingsType(this.MetricsSource.Type) + .Create(resourceName, this.Location, this.Dependencies, this.Tags) with + properties = {| LogAnalyticsDestinationType = match this.Sinks.LogAnalyticsWorkspace with - | Some (_, Dedicated) -> "Dedicated" + | Some(_, Dedicated) -> "Dedicated" | None - | Some (_, AzureDiagnostics) -> null + | Some(_, AzureDiagnostics) -> null eventHubName = this.Sinks.EventHub |> Option.bind (fun hub -> hub.EventHubName |> Option.map (fun r -> r.Value)) @@ -59,44 +60,40 @@ type DiagnosticSettings = |> Option.map (fun hub -> hub.AuthorizationRuleId.Eval()) |> Option.toObj storageAccountId = this.Sinks.StorageAccount |> Option.map (fun x -> x.Eval()) |> Option.toObj - metrics = - [| - for metric in this.Metrics do - {| - category = metric.Category - enabled = metric.Enabled - timeGrain = - metric.TimeGrain - |> Option.map (IsoDateTime.OfTimeSpan >> fun v -> v.Value) - |> Option.toObj - retentionPolicy = - metric.RetentionPolicy - |> Option.map (fun policy -> - box - {| - enabled = policy.Enabled - days = policy.RetentionPeriod - |}) - |> Option.toObj - |} - |] - logs = - [| - for log in this.Logs do - {| - category = log.Category.Value - enabled = log.Enabled - retentionPolicy = - log.RetentionPolicy - |> Option.map (fun policy -> - box - {| - enabled = policy.Enabled - days = policy.RetentionPeriod - |}) - |> Option.toObj - |} - |] + metrics = [| + for metric in this.Metrics do + {| + category = metric.Category + enabled = metric.Enabled + timeGrain = + metric.TimeGrain + |> Option.map (IsoDateTime.OfTimeSpan >> fun v -> v.Value) + |> Option.toObj + retentionPolicy = + metric.RetentionPolicy + |> Option.map (fun policy -> + box {| + enabled = policy.Enabled + days = policy.RetentionPeriod + |}) + |> Option.toObj + |} + |] + logs = [| + for log in this.Logs do + {| + category = log.Category.Value + enabled = log.Enabled + retentionPolicy = + log.RetentionPolicy + |> Option.map (fun policy -> + box {| + enabled = policy.Enabled + days = policy.RetentionPeriod + |}) + |> Option.toObj + |} + |] workspaceId = this.Sinks.LogAnalyticsWorkspace |> Option.map (fun (resource, _) -> resource.Eval()) diff --git a/src/Farmer/Arm/Disk.fs b/src/Farmer/Arm/Disk.fs index 10ed2755c..af1fdb039 100644 --- a/src/Farmer/Arm/Disk.fs +++ b/src/Farmer/Arm/Disk.fs @@ -10,48 +10,46 @@ type DiskCreation = | Import of SourceUri: Uri * StorageAccountId: ResourceId | Empty of Size: int -type Disk = - { - Name: ResourceName - Location: Location - Sku: Vm.DiskType option - Zones: string list - OsType: OS - CreationData: DiskCreation - Tags: Map - Dependencies: ResourceId Set - } +type Disk = { + Name: ResourceName + Location: Location + Sku: Vm.DiskType option + Zones: string list + OsType: OS + CreationData: DiskCreation + Tags: Map + Dependencies: ResourceId Set +} with interface IArmResource with member this.ResourceId = disks.resourceId this.Name - member this.JsonModel = - {| disks.Create(this.Name, this.Location, dependsOn = this.Dependencies, tags = this.Tags) with + member this.JsonModel = {| + disks.Create(this.Name, this.Location, dependsOn = this.Dependencies, tags = this.Tags) with sku = this.Sku |> Option.map (fun sku -> {| name = sku.ArmValue |} :> obj) |> Option.toObj zones = if this.Zones.IsEmpty then null else ResizeArray(this.Zones) - properties = - {| - creationData = - match this.CreationData with - | Empty _ -> {| createOption = "Empty" |} :> obj - | Import (sourceUri, storageAccountId) -> - {| - createOption = "Import" - sourceUri = sourceUri.AbsoluteUri - storageAccountId = storageAccountId.Eval() - |} - :> obj - diskSizeGB = - match this.CreationData with - | Empty size -> size / 1 :> obj - | _ -> null - osType = - this.OsType - |> function - | Linux -> "Linux" - | Windows -> "Windows" - |} - |} + properties = {| + creationData = + match this.CreationData with + | Empty _ -> {| createOption = "Empty" |} :> obj + | Import(sourceUri, storageAccountId) -> + {| + createOption = "Import" + sourceUri = sourceUri.AbsoluteUri + storageAccountId = storageAccountId.Eval() + |} + :> obj + diskSizeGB = + match this.CreationData with + | Empty size -> size / 1 :> obj + | _ -> null + osType = + this.OsType + |> function + | Linux -> "Linux" + | Windows -> "Windows" + |} + |} diff --git a/src/Farmer/Arm/Dns.fs b/src/Farmer/Arm/Dns.fs index c1cfc2295..30fc897d8 100644 --- a/src/Farmer/Arm/Dns.fs +++ b/src/Farmer/Arm/Dns.fs @@ -90,12 +90,11 @@ type DnsRecordType with | Public -> this.publicResourceType | Private -> this.privateResourceType -type DnsZone = - { - Name: ResourceName - Dependencies: Set - Properties: {| ZoneType: string |} - } +type DnsZone = { + Name: ResourceName + Dependencies: Set + Properties: {| ZoneType: string |} +} with member private this.zoneResource = match this.Properties.ZoneType with @@ -106,21 +105,20 @@ type DnsZone = interface IArmResource with member this.ResourceId = this.zoneResource.resourceId this.Name - member this.JsonModel = - {| this.zoneResource.Create(this.Name, Location.Global, this.Dependencies) with - properties = - {| - zoneType = this.Properties.ZoneType - |} - |} + member this.JsonModel = {| + this.zoneResource.Create(this.Name, Location.Global, this.Dependencies) with + properties = {| + zoneType = this.Properties.ZoneType + |} + |} module DnsRecords = let private sourceZoneNSRecordReference (zoneResourceId: ResourceId) : ArmExpression = - let sourceZoneResId = - { zoneResourceId with + let sourceZoneResId = { + zoneResourceId with Segments = [ ResourceName "@" ] Type = nsRecord - } + } ArmExpression .reference(nsRecord, sourceZoneResId) @@ -172,15 +170,14 @@ module DnsRecords = | Public -> "SOARecord" | Private -> "soaRecord" - type DnsRecord = - { - Name: ResourceName - Dependencies: Set - Zone: LinkedResource - ZoneType: DnsZoneType - Type: DnsRecordType - TTL: int - } + type DnsRecord = { + Name: ResourceName + Dependencies: Set + Zone: LinkedResource + ZoneType: DnsZoneType + Type: DnsRecordType + TTL: int + } with /// Includes the DnsZone if deployed in the same template (Managed). member private this.dependsOn = @@ -193,38 +190,36 @@ module DnsRecords = interface IArmResource with member this.ResourceId = this.RecordType.resourceId (this.Zone.Name, this.Name) - member this.JsonModel = - {| this.RecordType.Create(this.Zone.Name / this.Name, dependsOn = this.dependsOn) with + member this.JsonModel = {| + this.RecordType.Create(this.Zone.Name / this.Name, dependsOn = this.dependsOn) with properties = [ (ttlKey this.ZoneType), box this.TTL match this.Type with - | A (Some targetResource, _) - | CName (Some targetResource, _) - | AAAA (Some targetResource, _) -> + | A(Some targetResource, _) + | CName(Some targetResource, _) + | AAAA(Some targetResource, _) -> "targetResource", - box - {| - id = targetResource.ArmExpression.Eval() - |} + box {| + id = targetResource.ArmExpression.Eval() + |} | _ -> () match this.Type with - | CName (_, Some cnameRecord) -> + | CName(_, Some cnameRecord) -> (cnameRecordKey this.ZoneType), box {| cname = cnameRecord |} | MX records -> (mxRecordKey this.ZoneType), records - |> List.map (fun mx -> - {| - preference = mx.Preference - exchange = mx.Exchange - |}) + |> List.map (fun mx -> {| + preference = mx.Preference + exchange = mx.Exchange + |}) |> box - | NS (NsRecords.Records records) -> + | NS(NsRecords.Records records) -> "NSRecords", records |> List.map (fun ns -> {| nsdname = ns |}) |> box - | NS (NsRecords.SourceZone sourceZone) -> + | NS(NsRecords.SourceZone sourceZone) -> "NSRecords", (sourceZoneNSRecordReference sourceZone).Eval() |> box | TXT records -> (txtRecordKey this.ZoneType), @@ -232,49 +227,46 @@ module DnsRecords = | PTR records -> (ptrRecordKey this.ZoneType), records |> List.map (fun ptr -> {| ptrdname = ptr |}) |> box - | A (_, records) -> + | A(_, records) -> (aRecordKey this.ZoneType), records |> List.map (fun a -> {| ipv4Address = a |}) |> box - | AAAA (_, records) -> + | AAAA(_, records) -> (aaaRecordKey this.ZoneType), records |> List.map (fun aaaa -> {| ipv6Address = aaaa |}) |> box | SRV records -> let records = records - |> List.map (fun srv -> - {| - priority = srv.Priority |> Option.toNullable - weight = srv.Weight |> Option.toNullable - port = srv.Port |> Option.toNullable - target = Option.toObj srv.Target - |}) + |> List.map (fun srv -> {| + priority = srv.Priority |> Option.toNullable + weight = srv.Weight |> Option.toNullable + port = srv.Port |> Option.toNullable + target = Option.toObj srv.Target + |}) (srvRecordKey this.ZoneType), box records | SOA record -> - let record = - {| - host = Option.toObj record.Host - email = Option.toObj record.Email - serialNumber = record.SerialNumber |> Option.toNullable - refreshTime = record.RefreshTime |> Option.toNullable - retryTime = record.RetryTime |> Option.toNullable - expireTime = record.ExpireTime |> Option.toNullable - minimumTTL = record.MinimumTTL |> Option.toNullable - |} + let record = {| + host = Option.toObj record.Host + email = Option.toObj record.Email + serialNumber = record.SerialNumber |> Option.toNullable + refreshTime = record.RefreshTime |> Option.toNullable + retryTime = record.RetryTime |> Option.toNullable + expireTime = record.ExpireTime |> Option.toNullable + minimumTTL = record.MinimumTTL |> Option.toNullable + |} (soaRecordKey this.ZoneType), box record - | CName (_, None) -> () + | CName(_, None) -> () ] |> Map - |} + |} -type DnsResolver = - { - Name: ResourceName - Location: Location - VirtualNetworkId: LinkedResource - Dependencies: Set - Tags: Map - } +type DnsResolver = { + Name: ResourceName + Location: Location + VirtualNetworkId: LinkedResource + Dependencies: Set + Tags: Map +} with interface IArmResource with member this.ResourceId = dnsResolvers.resourceId this.Name @@ -283,27 +275,25 @@ type DnsResolver = let dependencies = this.Dependencies |> LinkedResource.addToSetIfManaged this.VirtualNetworkId - {| dnsResolvers.Create(this.Name, this.Location, dependencies, this.Tags) with - properties = - {| - virtualNetwork = - {| - id = this.VirtualNetworkId.ResourceId.Eval() - |} + {| + dnsResolvers.Create(this.Name, this.Location, dependencies, this.Tags) with + properties = {| + virtualNetwork = {| + id = this.VirtualNetworkId.ResourceId.Eval() + |} |} |} module DnsResolver = - type InboundEndpoint = - { - Name: ResourceName - Location: Location - DnsResolverId: LinkedResource - PrivateIpAllocations: AllocationMethod list - SubnetId: LinkedResource - Dependencies: Set - Tags: Map - } + type InboundEndpoint = { + Name: ResourceName + Location: Location + DnsResolverId: LinkedResource + PrivateIpAllocations: AllocationMethod list + SubnetId: LinkedResource + Dependencies: Set + Tags: Map + } with interface IArmResource with member this.ResourceId = @@ -313,48 +303,43 @@ module DnsResolver = let dependencies = this.Dependencies |> LinkedResource.addToSetIfManaged this.DnsResolverId - {| dnsResolverInboundEndpoints.Create( - this.DnsResolverId.Name / this.Name, - this.Location, - dependencies, - this.Tags - ) with - properties = - {| + {| + dnsResolverInboundEndpoints.Create( + this.DnsResolverId.Name / this.Name, + this.Location, + dependencies, + this.Tags + ) with + properties = {| ipConfigurations = this.PrivateIpAllocations |> List.map (fun ip -> match ip with - | DynamicPrivateIp -> - {| - privateIpAddress = null - privateIpAllocationMethod = "Dynamic" - subnet = - {| - id = this.SubnetId.ResourceId.Eval() - |} + | DynamicPrivateIp -> {| + privateIpAddress = null + privateIpAllocationMethod = "Dynamic" + subnet = {| + id = this.SubnetId.ResourceId.Eval() + |} + |} + | StaticPrivateIp addr -> {| + privateIpAddress = string addr + privateIpAllocationMethod = "Static" + subnet = {| + id = this.SubnetId.ResourceId.Eval() |} - | StaticPrivateIp addr -> - {| - privateIpAddress = string addr - privateIpAllocationMethod = "Static" - subnet = - {| - id = this.SubnetId.ResourceId.Eval() - |} - |}) + |}) |} |} - type OutboundEndpoint = - { - Name: ResourceName - Location: Location - DnsResolverId: LinkedResource - SubnetId: LinkedResource - Dependencies: Set - Tags: Map - } + type OutboundEndpoint = { + Name: ResourceName + Location: Location + DnsResolverId: LinkedResource + SubnetId: LinkedResource + Dependencies: Set + Tags: Map + } with interface IArmResource with member this.ResourceId = @@ -364,29 +349,27 @@ module DnsResolver = let dependencies = this.Dependencies |> LinkedResource.addToSetIfManaged this.DnsResolverId - {| dnsResolverOutboundEndpoints.Create( - this.DnsResolverId.Name / this.Name, - this.Location, - dependencies, - this.Tags - ) with - properties = - {| - subnet = - {| - id = this.SubnetId.ResourceId.Eval() - |} + {| + dnsResolverOutboundEndpoints.Create( + this.DnsResolverId.Name / this.Name, + this.Location, + dependencies, + this.Tags + ) with + properties = {| + subnet = {| + id = this.SubnetId.ResourceId.Eval() + |} |} |} -type DnsForwardingRuleset = - { - Name: ResourceName - Location: Location - DnsResolverOutboundEndpointIds: ResourceId Set - Dependencies: Set - Tags: Map - } +type DnsForwardingRuleset = { + Name: ResourceName + Location: Location + DnsResolverOutboundEndpointIds: ResourceId Set + Dependencies: Set + Tags: Map +} with interface IArmResource with member this.ResourceId = dnsForwardingRulesets.resourceId this.Name @@ -395,9 +378,9 @@ type DnsForwardingRuleset = let dependencies = this.Dependencies |> Set.union this.DnsResolverOutboundEndpointIds - {| dnsForwardingRulesets.Create(this.Name, this.Location, dependencies, this.Tags) with - properties = - {| + {| + dnsForwardingRulesets.Create(this.Name, this.Location, dependencies, this.Tags) with + properties = {| dnsResolverOutboundEndpoints = this.DnsResolverOutboundEndpointIds |> Set.map (fun endpointId -> {| id = endpointId.Eval() |}) @@ -405,62 +388,56 @@ type DnsForwardingRuleset = |} module DnsForwardingRuleset = - type ForwardingRule = - { - Name: ResourceName - ForwardingRulesetId: LinkedResource - DomainName: string - ForwardingRuleState: FeatureFlag option - TargetDnsServers: IPEndPoint list - } + type ForwardingRule = { + Name: ResourceName + ForwardingRulesetId: LinkedResource + DomainName: string + ForwardingRuleState: FeatureFlag option + TargetDnsServers: IPEndPoint list + } with interface IArmResource with member this.ResourceId = dnsForwardingRulesetForwardingRules.resourceId (this.ForwardingRulesetId.Name / this.Name) - member this.JsonModel = - {| dnsForwardingRulesetForwardingRules.Create( - this.ForwardingRulesetId.Name / this.Name, - dependsOn = [ this.ForwardingRulesetId.ResourceId ] - ) with - properties = - {| - domainName = this.DomainName - forwardingRuleState = - match this.ForwardingRuleState with - | Some state -> state.ArmValue - | None -> null - targetDnsServers = - this.TargetDnsServers - |> List.map (fun ipEndpoint -> - {| - ipAddress = string ipEndpoint.Address - port = ipEndpoint.Port - |}) - |} - |} + member this.JsonModel = {| + dnsForwardingRulesetForwardingRules.Create( + this.ForwardingRulesetId.Name / this.Name, + dependsOn = [ this.ForwardingRulesetId.ResourceId ] + ) with + properties = {| + domainName = this.DomainName + forwardingRuleState = + match this.ForwardingRuleState with + | Some state -> state.ArmValue + | None -> null + targetDnsServers = + this.TargetDnsServers + |> List.map (fun ipEndpoint -> {| + ipAddress = string ipEndpoint.Address + port = ipEndpoint.Port + |}) + |} + |} - type VirtualNetworkLink = - { - Name: ResourceName - ForwardingRulesetId: LinkedResource - VirtualNetworkId: LinkedResource - } + type VirtualNetworkLink = { + Name: ResourceName + ForwardingRulesetId: LinkedResource + VirtualNetworkId: LinkedResource + } with interface IArmResource with member this.ResourceId = dnsForwardingRulesetVirtualNetworkLinks.resourceId (this.ForwardingRulesetId.Name / this.Name) - member this.JsonModel = - {| dnsForwardingRulesetVirtualNetworkLinks.Create( - this.Name, - dependsOn = [ this.ForwardingRulesetId.ResourceId ] - ) with - properties = - {| - virtualNetwork = - {| - id = this.VirtualNetworkId.ResourceId.Eval() - |} + member this.JsonModel = {| + dnsForwardingRulesetVirtualNetworkLinks.Create( + this.Name, + dependsOn = [ this.ForwardingRulesetId.ResourceId ] + ) with + properties = {| + virtualNetwork = {| + id = this.VirtualNetworkId.ResourceId.Eval() |} - |} + |} + |} diff --git a/src/Farmer/Arm/DocumentDb.fs b/src/Farmer/Arm/DocumentDb.fs index 2bc97fd66..8df72aa0f 100644 --- a/src/Farmer/Arm/DocumentDb.fs +++ b/src/Farmer/Arm/DocumentDb.fs @@ -22,76 +22,74 @@ type DatabaseKind = module DatabaseAccounts = module SqlDatabases = - type Container = - { - Name: ResourceName - Account: ResourceName - Database: ResourceName - PartitionKey: {| Paths: string list - Kind: IndexKind |} - UniqueKeyPolicy: {| UniqueKeys: {| Paths: string list |} Set |} - IndexingPolicy: {| IncludedPaths: {| Path: string - Indexes: (IndexDataType * IndexKind) list |} list - ExcludedPaths: string list |} - } + type Container = { + Name: ResourceName + Account: ResourceName + Database: ResourceName + PartitionKey: {| + Paths: string list + Kind: IndexKind + |} + UniqueKeyPolicy: {| + UniqueKeys: {| Paths: string list |} Set + |} + IndexingPolicy: {| + IncludedPaths: + {| + Path: string + Indexes: (IndexDataType * IndexKind) list + |} list + ExcludedPaths: string list + |} + } with interface IArmResource with member this.ResourceId = containers.resourceId (this.Account / this.Database / this.Name) - member this.JsonModel = - {| containers.Create( - this.Account / this.Database / this.Name, - dependsOn = [ sqlDatabases.resourceId (this.Account, this.Database) ] - ) with - properties = - {| - resource = - {| - id = this.Name.Value - partitionKey = - {| - paths = this.PartitionKey.Paths - kind = string this.PartitionKey.Kind - |} - uniqueKeyPolicy = - {| - uniqueKeys = - this.UniqueKeyPolicy.UniqueKeys - |> Set.map (fun k -> {| paths = k.Paths |}) - |} - indexingPolicy = - {| - indexingMode = "consistent" - includedPaths = - this.IndexingPolicy.IncludedPaths - |> List.map (fun p -> - {| - path = p.Path - indexes = - p.Indexes - |> List.map (fun (dataType, kind) -> - {| - kind = string kind - dataType = dataType.ToString().ToLower() - precision = -1 - |}) - |}) - excludedPaths = - this.IndexingPolicy.ExcludedPaths - |> List.map (fun p -> {| path = p |}) - |} - |} + member this.JsonModel = {| + containers.Create( + this.Account / this.Database / this.Name, + dependsOn = [ sqlDatabases.resourceId (this.Account, this.Database) ] + ) with + properties = {| + resource = {| + id = this.Name.Value + partitionKey = {| + paths = this.PartitionKey.Paths + kind = string this.PartitionKey.Kind + |} + uniqueKeyPolicy = {| + uniqueKeys = + this.UniqueKeyPolicy.UniqueKeys |> Set.map (fun k -> {| paths = k.Paths |}) + |} + indexingPolicy = {| + indexingMode = "consistent" + includedPaths = + this.IndexingPolicy.IncludedPaths + |> List.map (fun p -> {| + path = p.Path + indexes = + p.Indexes + |> List.map (fun (dataType, kind) -> {| + kind = string kind + dataType = dataType.ToString().ToLower() + precision = -1 + |}) + |}) + excludedPaths = + this.IndexingPolicy.ExcludedPaths |> List.map (fun p -> {| path = p |}) + |} |} - |} + |} + |} - type SqlDatabase = - { - Name: ResourceName - Account: ResourceName - Throughput: Throughput - Kind: DatabaseKind - } + type SqlDatabase = { + Name: ResourceName + Account: ResourceName + Throughput: Throughput + Kind: DatabaseKind + } with interface IArmResource with member this.ResourceId = sqlDatabases.resourceId (this.Account / this.Name) @@ -102,36 +100,34 @@ module DatabaseAccounts = | Document -> sqlDatabases | Mongo -> mongoDatabases - {| resource.Create(this.Account / this.Name, dependsOn = [ databaseAccounts.resourceId this.Account ]) with - properties = - {| + {| + resource.Create(this.Account / this.Name, dependsOn = [ databaseAccounts.resourceId this.Account ]) with + properties = {| resource = {| id = this.Name.Value |} - options = - {| - throughput = - match this.Throughput with - | Provisioned t -> string t - | Serverless -> null - |} + options = {| + throughput = + match this.Throughput with + | Provisioned t -> string t + | Serverless -> null + |} |} |} -type DatabaseAccount = - { - Name: ResourceName - Location: Location - ConsistencyPolicy: ConsistencyPolicy - FailoverPolicy: FailoverPolicy - PublicNetworkAccess: FeatureFlag - FreeTier: bool - Serverless: FeatureFlag - Kind: DatabaseKind - Tags: Map - } +type DatabaseAccount = { + Name: ResourceName + Location: Location + ConsistencyPolicy: ConsistencyPolicy + FailoverPolicy: FailoverPolicy + PublicNetworkAccess: FeatureFlag + FreeTier: bool + Serverless: FeatureFlag + Kind: DatabaseKind + Tags: Map +} with member this.MaxStatelessPrefix = match this.ConsistencyPolicy with - | BoundedStaleness (staleness, _) -> Some staleness + | BoundedStaleness(staleness, _) -> Some staleness | Session | Eventual | ConsistentPrefix @@ -139,7 +135,7 @@ type DatabaseAccount = member this.MaxInterval = match this.ConsistencyPolicy with - | BoundedStaleness (_, interval) -> Some interval + | BoundedStaleness(_, interval) -> Some interval | Session | Eventual | ConsistentPrefix @@ -155,58 +151,55 @@ type DatabaseAccount = | MultiMaster _ -> Some true | _ -> None - member this.FailoverLocations = - [ - match this.FailoverPolicy with - | AutoFailover secondary - | MultiMaster secondary -> - {| - LocationName = this.Location.ArmValue - FailoverPriority = 0 - |} + member this.FailoverLocations = [ + match this.FailoverPolicy with + | AutoFailover secondary + | MultiMaster secondary -> + {| + LocationName = this.Location.ArmValue + FailoverPriority = 0 + |} - {| - LocationName = secondary.ArmValue - FailoverPriority = 1 - |} - | NoFailover -> () - ] + {| + LocationName = secondary.ArmValue + FailoverPriority = 1 + |} + | NoFailover -> () + ] interface IArmResource with member this.ResourceId = databaseAccounts.resourceId this.Name - member this.JsonModel = - {| databaseAccounts.Create(this.Name, this.Location, tags = this.Tags) with + member this.JsonModel = {| + databaseAccounts.Create(this.Name, this.Location, tags = this.Tags) with kind = match this.Kind with | Document -> "GlobalDocumentDB" | Mongo -> "MongoDB" properties = {| - consistencyPolicy = - {| - defaultConsistencyLevel = - match this.ConsistencyPolicy with - | BoundedStaleness _ -> "BoundedStaleness" - | Session - | Eventual - | ConsistentPrefix - | Strong as policy -> string policy - maxStalenessPrefix = this.MaxStatelessPrefix |> Option.toNullable - maxIntervalInSeconds = this.MaxInterval |> Option.toNullable - |} + consistencyPolicy = {| + defaultConsistencyLevel = + match this.ConsistencyPolicy with + | BoundedStaleness _ -> "BoundedStaleness" + | Session + | Eventual + | ConsistentPrefix + | Strong as policy -> string policy + maxStalenessPrefix = this.MaxStatelessPrefix |> Option.toNullable + maxIntervalInSeconds = this.MaxInterval |> Option.toNullable + |} databaseAccountOfferType = "Standard" enableAutomaticFailover = this.EnableAutomaticFailover |> Option.toNullable enableMultipleWriteLocations = this.EnableMultipleWriteLocations |> Option.toNullable locations = match this.FailoverLocations, this.Serverless with | [], Enabled -> - box - [ - {| - locationName = this.Location.ArmValue - |} - ] + box [ + {| + locationName = this.Location.ArmValue + |} + ] | [], Disabled -> null | locations, _ -> box locations publicNetworkAccess = string this.PublicNetworkAccess @@ -218,4 +211,4 @@ type DatabaseAccount = null |} |> box - |} + |} diff --git a/src/Farmer/Arm/EventGrid.fs b/src/Farmer/Arm/EventGrid.fs index 1d82ed3c7..e755b77b9 100644 --- a/src/Farmer/Arm/EventGrid.fs +++ b/src/Farmer/Arm/EventGrid.fs @@ -15,11 +15,11 @@ type TopicType = member this.Value = match this with - | TopicType (_, s) -> s + | TopicType(_, s) -> s member this.ResourceType = match this with - | TopicType (r, _) -> r + | TopicType(r, _) -> r module Topics = let EventHubsNamespace = @@ -38,17 +38,15 @@ module Topics = let AppServicePlan = TopicType(serverFarms, "Microsoft.Web.ServerFarms") let SignalR = TopicType(signalR, "Microsoft.SignalRService.SignalR") -type ServiceBusQueueEndpointType = - { - Bus: ResourceName - Queue: ResourceName - } +type ServiceBusQueueEndpointType = { + Bus: ResourceName + Queue: ResourceName +} -type ServiceBusTopicEndpointType = - { - Bus: ResourceName - Topic: ResourceName - } +type ServiceBusTopicEndpointType = { + Bus: ResourceName + Topic: ResourceName +} type ServiceBusEndpointType = | Queue of Queue: ServiceBusQueueEndpointType @@ -60,14 +58,13 @@ type EndpointType = | StorageQueue of queue: ResourceName | ServiceBus of bus: ServiceBusEndpointType -type Topic = - { - Name: ResourceName - Location: Location - Source: ResourceName - TopicType: TopicType - Tags: Map - } +type Topic = { + Name: ResourceName + Location: Location + Source: ResourceName + TopicType: TopicType + Tags: Map +} with interface IArmResource with member this.ResourceId = systemTopics.resourceId this.Name @@ -75,22 +72,21 @@ type Topic = member this.JsonModel = let sourceResourceId = this.TopicType.ResourceType.resourceId this.Source - {| systemTopics.Create(this.Name, this.Location, [ sourceResourceId ], this.Tags) with - properties = - {| + {| + systemTopics.Create(this.Name, this.Location, [ sourceResourceId ], this.Tags) with + properties = {| source = sourceResourceId.Eval() topicType = this.TopicType.Value |} |} -type Subscription<'T> = - { - Name: ResourceName - Topic: ResourceName - Destination: ResourceName - DestinationEndpoint: EndpointType - Events: EventGridEvent<'T> list - } +type Subscription<'T> = { + Name: ResourceName + Topic: ResourceName + Destination: ResourceName + DestinationEndpoint: EndpointType + Events: EventGridEvent<'T> list +} with interface IArmResource with member this.ResourceId = eventSubscriptions.resourceId (this.Topic / this.Name) @@ -102,19 +98,18 @@ type Subscription<'T> = | StorageQueue queue -> Some(Storage.queues.resourceId (this.Destination, ResourceName "default", queue)) | WebHook _ -> None - | ServiceBus (Queue { Queue = queue; Bus = bus }) -> Some(ServiceBus.queues.resourceId (bus, queue)) - | ServiceBus (Topic { Topic = topic; Bus = bus }) -> Some(ServiceBus.topics.resourceId (bus, topic)) - - {| eventSubscriptions.Create( - this.Topic / this.Name, - dependsOn = - [ - systemTopics.resourceId this.Topic - yield! Option.toList destinationResourceId - ] - ) with - properties = - {| + | ServiceBus(Queue { Queue = queue; Bus = bus }) -> Some(ServiceBus.queues.resourceId (bus, queue)) + | ServiceBus(Topic { Topic = topic; Bus = bus }) -> Some(ServiceBus.topics.resourceId (bus, topic)) + + {| + eventSubscriptions.Create( + this.Topic / this.Name, + dependsOn = [ + systemTopics.resourceId this.Topic + yield! Option.toList destinationResourceId + ] + ) with + properties = {| destination = match this.DestinationEndpoint with | WebHook uri -> @@ -123,49 +118,38 @@ type Subscription<'T> = properties = {| endpointUrl = uri.ToString() |} |} |> box - | EventHub hubName -> - {| - endpointType = "EventHub" - properties = - {| - resourceId = - Namespaces.eventHubs.resourceId(this.Destination, hubName).Eval() - |} + | EventHub hubName -> {| + endpointType = "EventHub" + properties = {| + resourceId = Namespaces.eventHubs.resourceId(this.Destination, hubName).Eval() |} - | StorageQueue queueName -> - {| - endpointType = "StorageQueue" - properties = - {| - resourceId = (storageAccounts.resourceId this.Destination).Eval() - queueName = queueName.Value - |} + |} + | StorageQueue queueName -> {| + endpointType = "StorageQueue" + properties = {| + resourceId = (storageAccounts.resourceId this.Destination).Eval() + queueName = queueName.Value |} - | ServiceBus (Queue { Queue = queue; Bus = bus }) -> - {| - endpointType = "ServiceBusQueue" - properties = - {| - resourceId = (ServiceBus.queues.resourceId (bus, queue)).Eval() - queueName = queue.Value - |} + |} + | ServiceBus(Queue { Queue = queue; Bus = bus }) -> {| + endpointType = "ServiceBusQueue" + properties = {| + resourceId = (ServiceBus.queues.resourceId (bus, queue)).Eval() + queueName = queue.Value |} - | ServiceBus (Topic { Topic = topic; Bus = bus }) -> - {| - endpointType = "ServiceBusTopic" - properties = - {| - resourceId = (ServiceBus.topics.resourceId (bus, topic)).Eval() - queueName = topic.Value - |} + |} + | ServiceBus(Topic { Topic = topic; Bus = bus }) -> {| + endpointType = "ServiceBusTopic" + properties = {| + resourceId = (ServiceBus.topics.resourceId (bus, topic)).Eval() + queueName = topic.Value |} - filter = - {| - includedEventTypes = - [ - for event in this.Events do - event.Value - ] - |} + |} + filter = {| + includedEventTypes = [ + for event in this.Events do + event.Value + ] + |} |} |} diff --git a/src/Farmer/Arm/EventHub.fs b/src/Farmer/Arm/EventHub.fs index a625f3a42..043df27dc 100644 --- a/src/Farmer/Arm/EventHub.fs +++ b/src/Farmer/Arm/EventHub.fs @@ -8,15 +8,14 @@ let namespaces = ResourceType("Microsoft.EventHub/namespaces", "2017-04-01") type CaptureDestination = StorageAccount of ResourceName * containerName: string -type Namespace = - { - Name: ResourceName - Location: Location - Sku: {| Name: EventHubSku; Capacity: int |} - ZoneRedundant: bool option - AutoInflateSettings: InflateSetting option - Tags: Map - } +type Namespace = { + Name: ResourceName + Location: Location + Sku: {| Name: EventHubSku; Capacity: int |} + ZoneRedundant: bool option + AutoInflateSettings: InflateSetting option + Tags: Map +} with member this.MaxThroughputUnits = this.AutoInflateSettings @@ -27,26 +26,24 @@ type Namespace = interface IArmResource with member this.ResourceId = namespaces.resourceId this.Name - member this.JsonModel = - {| namespaces.Create(this.Name, this.Location, tags = this.Tags) with - sku = - {| - name = string this.Sku.Name - tier = string this.Sku.Name - capacity = this.Sku.Capacity - |} - properties = - {| - zoneRedundant = this.ZoneRedundant |> Option.toNullable - isAutoInflateEnabled = - this.AutoInflateSettings - |> Option.map (function - | AutoInflate _ -> true - | ManualInflate -> false) - |> Option.toNullable - maximumThroughputUnits = this.MaxThroughputUnits |> Option.toNullable - |} - |} + member this.JsonModel = {| + namespaces.Create(this.Name, this.Location, tags = this.Tags) with + sku = {| + name = string this.Sku.Name + tier = string this.Sku.Name + capacity = this.Sku.Capacity + |} + properties = {| + zoneRedundant = this.ZoneRedundant |> Option.toNullable + isAutoInflateEnabled = + this.AutoInflateSettings + |> Option.map (function + | AutoInflate _ -> true + | ManualInflate -> false) + |> Option.toNullable + maximumThroughputUnits = this.MaxThroughputUnits |> Option.toNullable + |} + |} module Namespaces = let eventHubs = @@ -55,48 +52,43 @@ module Namespaces = let authorizationRules = ResourceType("Microsoft.EventHub/namespaces/AuthorizationRules", "2017-04-01") - type EventHub = - { - Name: ResourceName - Location: Location - MessageRetentionDays: int option - Partitions: int - Dependencies: ResourceId Set - CaptureDestination: CaptureDestination option - Tags: Map - } + type EventHub = { + Name: ResourceName + Location: Location + MessageRetentionDays: int option + Partitions: int + Dependencies: ResourceId Set + CaptureDestination: CaptureDestination option + Tags: Map + } with interface IArmResource with member this.ResourceId = eventHubs.resourceId this.Name - member this.JsonModel = - {| eventHubs.Create(this.Name, this.Location, this.Dependencies, this.Tags) with - properties = - {| - messageRetentionInDays = this.MessageRetentionDays |> Option.toNullable - partitionCount = this.Partitions - status = "Active" - captureDescription = - match this.CaptureDestination with - | Some (StorageAccount (name, container)) -> - {| - enabled = true - encoding = "Avro" - destination = - {| - name = "EventHubArchive.AzureBlockBlob" - properties = - {| - storageAccountResourceId = - storageAccounts.resourceId(name).Eval() - blobContainer = container - |} - |} + member this.JsonModel = {| + eventHubs.Create(this.Name, this.Location, this.Dependencies, this.Tags) with + properties = {| + messageRetentionInDays = this.MessageRetentionDays |> Option.toNullable + partitionCount = this.Partitions + status = "Active" + captureDescription = + match this.CaptureDestination with + | Some(StorageAccount(name, container)) -> + {| + enabled = true + encoding = "Avro" + destination = {| + name = "EventHubArchive.AzureBlockBlob" + properties = {| + storageAccountResourceId = storageAccounts.resourceId(name).Eval() + blobContainer = container + |} |} - |> box - | None -> null - |} - |} + |} + |> box + | None -> null + |} + |} module EventHubs = let authorizationRules = @@ -105,13 +97,12 @@ module Namespaces = let consumerGroups = ResourceType("Microsoft.EventHub/namespaces/eventhubs/consumergroups", "2017-04-01") - type ConsumerGroup = - { - ConsumerGroupName: ResourceName - EventHub: ResourceName - Location: Location - Dependencies: ResourceId list - } + type ConsumerGroup = { + ConsumerGroupName: ResourceName + EventHub: ResourceName + Location: Location + Dependencies: ResourceId list + } with interface IArmResource with member this.ResourceId = @@ -120,21 +111,19 @@ module Namespaces = member this.JsonModel = consumerGroups.Create(this.EventHub / this.ConsumerGroupName, this.Location, this.Dependencies) - type AuthorizationRule = - { - Name: ResourceName - Location: Location - Dependencies: ResourceId list - Rights: AuthorizationRuleRight Set - } + type AuthorizationRule = { + Name: ResourceName + Location: Location + Dependencies: ResourceId list + Rights: AuthorizationRuleRight Set + } with interface IArmResource with member this.ResourceId = authorizationRules.resourceId this.Name - member this.JsonModel = - {| authorizationRules.Create(this.Name, this.Location, this.Dependencies) with - properties = - {| - rights = this.Rights |> Set.map string |> Set.toList - |} - |} + member this.JsonModel = {| + authorizationRules.Create(this.Name, this.Location, this.Dependencies) with + properties = {| + rights = this.Rights |> Set.map string |> Set.toList + |} + |} diff --git a/src/Farmer/Arm/Gallery.fs b/src/Farmer/Arm/Gallery.fs index 9736c6fad..87f6f71f2 100644 --- a/src/Farmer/Arm/Gallery.fs +++ b/src/Farmer/Arm/Gallery.fs @@ -12,13 +12,12 @@ let galleryImages = ResourceType("Microsoft.Compute/galleries/images", "2022-03- let imageVersions = ResourceType("Microsoft.Compute/galleries/images/versions", "2022-03-03") -type CommunityGalleryInfo = - { - Eula: string - PublicNamePrefix: string - PublisherContact: string - PublisherUri: Uri - } +type CommunityGalleryInfo = { + Eula: string + PublicNamePrefix: string + PublisherContact: string + PublisherUri: Uri +} type SharingProfile = | Community of CommunityGalleryInfo @@ -27,157 +26,144 @@ type SharingProfile = type SoftDeletePolicy = { IsSoftDeleteEnabled: bool } -type Gallery = - { - Name: GalleryName - Location: Location - Description: string option - SharingProfile: SharingProfile option - SoftDeletePolicy: SoftDeletePolicy option - Tags: Map - Dependencies: Set - } +type Gallery = { + Name: GalleryName + Location: Location + Description: string option + SharingProfile: SharingProfile option + SoftDeletePolicy: SoftDeletePolicy option + Tags: Map + Dependencies: Set +} with interface IArmResource with member this.ResourceId = galleries.resourceId this.Name.ResourceName - member this.JsonModel = - {| galleries.Create(this.Name.ResourceName, this.Location, dependsOn = this.Dependencies, tags = this.Tags) with - properties = - {| - description = this.Description - sharingProfile = - match this.SharingProfile with - | None -> null - | Some sharingProfile -> - match sharingProfile with - | Community info -> - {| - permissions = "Community" - communityGalleryInfo = - {| - eula = info.Eula - publicNamePrefix = info.PublicNamePrefix - publisherContact = info.PublisherContact - publisherUri = info.PublisherUri.AbsoluteUri - |} - |} - :> obj - | Groups -> {| permissions = "Groups" |} :> obj - | Private -> {| permissions = "Private" |} :> obj - softDeletePolicy = - match this.SoftDeletePolicy with - | Some policy -> + member this.JsonModel = {| + galleries.Create(this.Name.ResourceName, this.Location, dependsOn = this.Dependencies, tags = this.Tags) with + properties = {| + description = this.Description + sharingProfile = + match this.SharingProfile with + | None -> null + | Some sharingProfile -> + match sharingProfile with + | Community info -> {| - isSoftDeleteEnabled = policy.IsSoftDeleteEnabled + permissions = "Community" + communityGalleryInfo = {| + eula = info.Eula + publicNamePrefix = info.PublicNamePrefix + publisherContact = info.PublisherContact + publisherUri = info.PublisherUri.AbsoluteUri + |} |} :> obj - | None -> null - |} - |} - -type GalleryImageIdentifier = - { - Offer: string - Publisher: string - Sku: string - } - -type ImagePurchasePlan = - { - PlanName: string - PlanProduct: string - PlanPublisher: string - } - -type RecommendedMachineConfiguration = - { - MemoryMin: int - MemoryMax: int - VCpuMin: int - VCpuMax: int + | Groups -> {| permissions = "Groups" |} :> obj + | Private -> {| permissions = "Private" |} :> obj + softDeletePolicy = + match this.SoftDeletePolicy with + | Some policy -> + {| + isSoftDeleteEnabled = policy.IsSoftDeleteEnabled + |} + :> obj + | None -> null + |} + |} + +type GalleryImageIdentifier = { + Offer: string + Publisher: string + Sku: string +} + +type ImagePurchasePlan = { + PlanName: string + PlanProduct: string + PlanPublisher: string +} + +type RecommendedMachineConfiguration = { + MemoryMin: int + MemoryMax: int + VCpuMin: int + VCpuMax: int +} with + + static member Default = { + MemoryMin = 1 + MemoryMax = 32 + VCpuMin = 1 + VCpuMax = 16 } - static member Default = - { - MemoryMin = 1 - MemoryMax = 32 - VCpuMin = 1 - VCpuMax = 16 - } - -type GalleryImage = - { - Name: ResourceName - GalleryName: GalleryName - Location: Location - Architecture: Architecture option - Description: string - Eula: string option - HyperVGeneration: HyperVGeneration - Identifier: GalleryImageIdentifier - OsState: OsState - OsType: OS - PrivacyStatementUri: Uri option - PurchasePlan: ImagePurchasePlan option - Recommended: RecommendedMachineConfiguration - ReleaseNoteUri: Uri option - Tags: Map - Dependencies: Set - } +type GalleryImage = { + Name: ResourceName + GalleryName: GalleryName + Location: Location + Architecture: Architecture option + Description: string + Eula: string option + HyperVGeneration: HyperVGeneration + Identifier: GalleryImageIdentifier + OsState: OsState + OsType: OS + PrivacyStatementUri: Uri option + PurchasePlan: ImagePurchasePlan option + Recommended: RecommendedMachineConfiguration + ReleaseNoteUri: Uri option + Tags: Map + Dependencies: Set +} with interface IArmResource with member this.ResourceId = galleryImages.resourceId (this.GalleryName.ResourceName, this.Name) - member this.JsonModel = - {| galleryImages.Create( - this.GalleryName.ResourceName / this.Name, - this.Location, - dependsOn = this.Dependencies, - tags = this.Tags - ) with - properties = - {| - architecture = this.Architecture |> Option.map (fun arch -> arch.ArmValue) |> Option.toObj - description = this.Description - eula = this.Eula |> Option.toObj - hyperVGeneration = this.HyperVGeneration.ArmValue - identifier = - {| - offer = this.Identifier.Offer - publisher = this.Identifier.Publisher - sku = this.Identifier.Sku - |} - osState = this.OsState.ArmValue - osType = string this.OsType - privacyStatementUri = - this.PrivacyStatementUri - |> Option.map (fun uri -> uri.AbsoluteUri) - |> Option.toObj - purchasePlan = - match this.PurchasePlan with - | None -> null - | Some plan -> - {| - name = plan.PlanName - product = plan.PlanProduct - publisher = plan.PlanPublisher - |} - :> obj - recommended = + member this.JsonModel = {| + galleryImages.Create( + this.GalleryName.ResourceName / this.Name, + this.Location, + dependsOn = this.Dependencies, + tags = this.Tags + ) with + properties = {| + architecture = this.Architecture |> Option.map (fun arch -> arch.ArmValue) |> Option.toObj + description = this.Description + eula = this.Eula |> Option.toObj + hyperVGeneration = this.HyperVGeneration.ArmValue + identifier = {| + offer = this.Identifier.Offer + publisher = this.Identifier.Publisher + sku = this.Identifier.Sku + |} + osState = this.OsState.ArmValue + osType = string this.OsType + privacyStatementUri = + this.PrivacyStatementUri + |> Option.map (fun uri -> uri.AbsoluteUri) + |> Option.toObj + purchasePlan = + match this.PurchasePlan with + | None -> null + | Some plan -> {| - memory = - {| - min = this.Recommended.MemoryMin - max = this.Recommended.MemoryMax - |} - vCPUs = - {| - min = this.Recommended.VCpuMin - max = this.Recommended.VCpuMax - |} + name = plan.PlanName + product = plan.PlanProduct + publisher = plan.PlanPublisher |} - releaseNoteUri = this.ReleaseNoteUri |> Option.map (fun uri -> uri.AbsoluteUri) |> Option.toObj + :> obj + recommended = {| + memory = {| + min = this.Recommended.MemoryMin + max = this.Recommended.MemoryMax + |} + vCPUs = {| + min = this.Recommended.VCpuMin + max = this.Recommended.VCpuMax + |} |} - |} + releaseNoteUri = this.ReleaseNoteUri |> Option.map (fun uri -> uri.AbsoluteUri) |> Option.toObj + |} + |} diff --git a/src/Farmer/Arm/ImageTemplate.fs b/src/Farmer/Arm/ImageTemplate.fs index 1944741da..3b8c912c6 100644 --- a/src/Farmer/Arm/ImageTemplate.fs +++ b/src/Farmer/Arm/ImageTemplate.fs @@ -10,12 +10,11 @@ let imageTemplates = let images = ResourceType("Microsoft.Compute/images", "2022-08-01") -type PlatformImageSource = - { - ImageIdentifier: GalleryImageIdentifier - PlanInfo: ImagePurchasePlan option - Version: string - } +type PlatformImageSource = { + ImageIdentifier: GalleryImageIdentifier + PlanInfo: ImagePurchasePlan option + Version: string +} with member this.JsonModel = {| @@ -41,10 +40,9 @@ type PlatformImageSource = |} :> obj -type ManagedImageSource = - { - ImageId: ResourceId - } +type ManagedImageSource = { + ImageId: ResourceId +} with member this.JsonModel = {| @@ -53,10 +51,9 @@ type ManagedImageSource = |} :> obj -type SharedImageVersionSource = - { - ImageId: ResourceId - } +type SharedImageVersionSource = { + ImageId: ResourceId +} with member this.JsonModel = {| @@ -77,13 +74,12 @@ type ImageBuilderSource = | Managed src -> src.JsonModel | SharedVersion src -> src.JsonModel -type FileCustomizer = - { - Name: string - Destination: string - SourceUri: Uri - Sha256Checksum: string option - } +type FileCustomizer = { + Name: string + Destination: string + SourceUri: Uri + Sha256Checksum: string option +} with member this.JsonModel = {| @@ -95,12 +91,11 @@ type FileCustomizer = |} :> obj -type ShellScriptCustomizer = - { - Name: string - ScriptUri: Uri - Sha256Checksum: string option - } +type ShellScriptCustomizer = { + Name: string + ScriptUri: Uri + Sha256Checksum: string option +} with member this.JsonModel = {| @@ -111,11 +106,10 @@ type ShellScriptCustomizer = |} :> obj -type ShellCustomizer = - { - Name: string - Inline: string list - } +type ShellCustomizer = { + Name: string + Inline: string list +} with member this.JsonModel = {| @@ -125,15 +119,14 @@ type ShellCustomizer = |} :> obj -type PowerShellScriptCustomizer = - { - Name: string - ScriptUri: Uri - Sha256Checksum: string option - RunAsSystem: bool - RunAsElevated: bool - ValidExitCodes: int list - } +type PowerShellScriptCustomizer = { + Name: string + ScriptUri: Uri + Sha256Checksum: string option + RunAsSystem: bool + RunAsElevated: bool + ValidExitCodes: int list +} with member this.JsonModel = {| @@ -151,14 +144,13 @@ type PowerShellScriptCustomizer = |} :> obj -type PowerShellCustomizer = - { - Name: string - Inline: string list - RunAsSystem: bool - RunAsElevated: bool - ValidExitCodes: int list - } +type PowerShellCustomizer = { + Name: string + Inline: string list + RunAsSystem: bool + RunAsElevated: bool + ValidExitCodes: int list +} with member this.JsonModel = {| @@ -175,12 +167,11 @@ type PowerShellCustomizer = |} :> obj -type WindowsRestartCustomizer = - { - RestartCheckCommand: string option - RestartCommand: string option - RestartTimeout: string option // 5m for 5 minutes, 2h for two hours - } +type WindowsRestartCustomizer = { + RestartCheckCommand: string option + RestartCommand: string option + RestartTimeout: string option // 5m for 5 minutes, 2h for two hours +} with member this.JsonModel = {| @@ -191,12 +182,11 @@ type WindowsRestartCustomizer = |} :> obj -type WindowsUpdateCustomizer = - { - Filters: string list - SearchCriteria: string option // defaults to "BrowseOnly=0 and IsInstalled=0" (Recommended) - UpdateLimit: int option // defaults to limit of 1000 updates - } +type WindowsUpdateCustomizer = { + Filters: string list + SearchCriteria: string option // defaults to "BrowseOnly=0 and IsInstalled=0" (Recommended) + UpdateLimit: int option // defaults to limit of 1000 updates +} with member this.JsonModel = {| @@ -231,28 +221,25 @@ type Customizer = | WindowsRestart customizer -> customizer.JsonModel | WindowsUpdate customizer -> customizer.JsonModel -type ManagedImageDistributor = - { - ImageId: ResourceId - RunOutputName: string - Location: string - ArtifactTags: Map - } - -type SharedImageDistributor = - { - GalleryImageId: ResourceId - RunOutputName: string - ReplicationRegions: Location list - ExcludeFromLatest: bool option - ArtifactTags: Map - } - -type VhdDistributor = - { - RunOutputName: string - ArtifactTags: Map - } +type ManagedImageDistributor = { + ImageId: ResourceId + RunOutputName: string + Location: string + ArtifactTags: Map +} + +type SharedImageDistributor = { + GalleryImageId: ResourceId + RunOutputName: string + ReplicationRegions: Location list + ExcludeFromLatest: bool option + ArtifactTags: Map +} + +type VhdDistributor = { + RunOutputName: string + ArtifactTags: Map +} [] type Distibutor = @@ -274,19 +261,18 @@ type Distibutor = distributor.ArtifactTags :> obj |} :> obj - | SharedImage distributor -> - {| - ``type`` = "SharedImage" - galleryImageId = distributor.GalleryImageId.Eval() - replicationRegions = distributor.ReplicationRegions |> List.map (fun location -> location.ArmValue) - runOutputName = distributor.RunOutputName - excludeFromLatest = distributor.ExcludeFromLatest |> Option.map box |> Option.toObj - artifactTags = - if distributor.ArtifactTags.IsEmpty then - null - else - distributor.ArtifactTags :> obj - |} + | SharedImage distributor -> {| + ``type`` = "SharedImage" + galleryImageId = distributor.GalleryImageId.Eval() + replicationRegions = distributor.ReplicationRegions |> List.map (fun location -> location.ArmValue) + runOutputName = distributor.RunOutputName + excludeFromLatest = distributor.ExcludeFromLatest |> Option.map box |> Option.toObj + artifactTags = + if distributor.ArtifactTags.IsEmpty then + null + else + distributor.ArtifactTags :> obj + |} | VHD distributor -> {| ``type`` = "VHD" @@ -299,18 +285,17 @@ type Distibutor = |} :> obj -type ImageBuilder = - { - Name: ResourceName - Location: Location - Identity: Identity.ManagedIdentity - BuildTimeoutInMinutes: int option - Source: ImageBuilderSource - Customize: Customizer list - Distribute: Distibutor list - Tags: Map - Dependencies: ResourceId Set - } +type ImageBuilder = { + Name: ResourceName + Location: Location + Identity: Identity.ManagedIdentity + BuildTimeoutInMinutes: int option + Source: ImageBuilderSource + Customize: Customizer list + Distribute: Distibutor list + Tags: Map + Dependencies: ResourceId Set +} with interface IArmResource with member this.ResourceId = imageTemplates.resourceId this.Name @@ -323,10 +308,10 @@ type ImageBuilder = } |> Set.ofSeq - {| imageTemplates.Create(this.Name, this.Location, dependsOn = dependencies, tags = this.Tags) with - identity = this.Identity.ToArmJson - properties = - {| + {| + imageTemplates.Create(this.Name, this.Location, dependsOn = dependencies, tags = this.Tags) with + identity = this.Identity.ToArmJson + properties = {| buildTimeoutInMinutes = this.BuildTimeoutInMinutes |> Option.map box |> Option.toObj source = this.Source.JsonModel customize = this.Customize |> List.map (fun customizer -> customizer.JsonModel) diff --git a/src/Farmer/Arm/Insights.fs b/src/Farmer/Arm/Insights.fs index 75ba72787..c2ef32bb7 100644 --- a/src/Farmer/Arm/Insights.fs +++ b/src/Farmer/Arm/Insights.fs @@ -21,17 +21,16 @@ type InstanceKind = | Classic -> components | Workspace _ -> componentsWorkspace -type Components = - { - Name: ResourceName - Location: Location - LinkedWebsite: ResourceName option - DisableIpMasking: bool - SamplingPercentage: int - InstanceKind: InstanceKind - Tags: Map - Dependencies: ResourceId Set - } +type Components = { + Name: ResourceName + Location: Location + LinkedWebsite: ResourceName option + DisableIpMasking: bool + SamplingPercentage: int + InstanceKind: InstanceKind + Tags: Map + Dependencies: ResourceId Set +} with interface IArmResource with member this.ResourceId = components.resourceId this.Name @@ -46,10 +45,10 @@ type Components = ) | None -> this.Tags - {| this.InstanceKind.ResourceType.Create(this.Name, this.Location, this.Dependencies, tags) with - kind = "web" - properties = - {| + {| + this.InstanceKind.ResourceType.Create(this.Name, this.Location, this.Dependencies, tags) with + kind = "web" + properties = {| name = this.Name.Value Application_Type = "web" ApplicationId = diff --git a/src/Farmer/Arm/KeyVault.fs b/src/Farmer/Arm/KeyVault.fs index 51c1fb0cb..1f270e180 100644 --- a/src/Farmer/Arm/KeyVault.fs +++ b/src/Farmer/Arm/KeyVault.fs @@ -14,18 +14,17 @@ let vaults = ResourceType("Microsoft.KeyVault/vaults", "2022-07-01") let keys = ResourceType("Microsoft.keyVault/vaults/keys", "2022-07-01") module Vaults = - type Secret = - { - Name: ResourceName - Location: Location - Value: SecretValue - ContentType: string option - Enabled: bool option - ActivationDate: DateTime option - ExpirationDate: DateTime option - Dependencies: ResourceId Set - Tags: Map - } + type Secret = { + Name: ResourceName + Location: Location + Value: SecretValue + ContentType: string option + Enabled: bool option + ActivationDate: DateTime option + ExpirationDate: DateTime option + Dependencies: ResourceId Set + Tags: Map + } with static member ``1970`` = DateTime(1970, 1, 1, 0, 0, 0) @@ -43,43 +42,40 @@ module Vaults = interface IArmResource with member this.ResourceId = secrets.resourceId this.Name - member this.JsonModel = - {| secrets.Create(this.Name, this.Location, this.Dependencies, this.Tags) with - properties = - {| - value = this.Value.Value - contentType = this.ContentType |> Option.toObj - attributes = - {| - enabled = this.Enabled |> Option.toNullable - nbf = - this.ActivationDate - |> Option.map Secret.TotalSecondsSince1970 - |> Option.toNullable - exp = - this.ExpirationDate - |> Option.map Secret.TotalSecondsSince1970 - |> Option.toNullable - |} + member this.JsonModel = {| + secrets.Create(this.Name, this.Location, this.Dependencies, this.Tags) with + properties = {| + value = this.Value.Value + contentType = this.ContentType |> Option.toObj + attributes = {| + enabled = this.Enabled |> Option.toNullable + nbf = + this.ActivationDate + |> Option.map Secret.TotalSecondsSince1970 + |> Option.toNullable + exp = + this.ExpirationDate + |> Option.map Secret.TotalSecondsSince1970 + |> Option.toNullable |} - |} + |} + |} let private armValue armValue (a: 'a option) = a |> Option.map armValue |> Option.defaultValue Unchecked.defaultof<_> - type Key = - { - VaultName: ResourceName - KeyName: ResourceName - Location: Location - Enabled: bool option - ActivationDate: DateTime option - ExpirationDate: DateTime option - KeyOps: KeyOperation list - KTY: KeyType - Dependencies: ResourceId Set - Tags: Map - } + type Key = { + VaultName: ResourceName + KeyName: ResourceName + Location: Location + Enabled: bool option + ActivationDate: DateTime option + ExpirationDate: DateTime option + KeyOps: KeyOperation list + KTY: KeyType + Dependencies: ResourceId Set + Tags: Map + } with member this.Name = this.VaultName / this.KeyName member this.ResourceId = keys.resourceId this.Name @@ -87,69 +83,72 @@ module Vaults = interface IArmResource with member this.ResourceId = this.ResourceId - member this.JsonModel = - {| keys.Create(this.Name, this.Location, this.Dependencies, this.Tags) with - properties = - {| - attributes = - {| - enabled = this.Enabled |> Option.toNullable - exp = - this.ExpirationDate - |> Option.map (fun exp -> DateTimeOffset(exp).ToUnixTimeSeconds()) - nbf = - this.ActivationDate - |> Option.map (fun nbf -> DateTimeOffset(nbf).ToUnixTimeSeconds()) - |} - curveName = - match this.KTY with - | EC curveName - | ECHSM curveName -> curveName |> KeyCurveName.ArmValue - | _ -> null - kty = this.KTY |> KeyType.ArmValue - keyOps = - if this.KeyOps.IsEmpty then - Unchecked.defaultof<_> - else - this.KeyOps |> List.map KeyOperation.ArmValue - keySize = - match this.KTY with - | RSA (RsaKeyLength keySize) -> box keySize - | RSAHSM (RsaKeyLength keySize) -> box keySize - | _ -> null + member this.JsonModel = {| + keys.Create(this.Name, this.Location, this.Dependencies, this.Tags) with + properties = {| + attributes = {| + enabled = this.Enabled |> Option.toNullable + exp = + this.ExpirationDate + |> Option.map (fun exp -> DateTimeOffset(exp).ToUnixTimeSeconds()) + nbf = + this.ActivationDate + |> Option.map (fun nbf -> DateTimeOffset(nbf).ToUnixTimeSeconds()) |} - |} + curveName = + match this.KTY with + | EC curveName + | ECHSM curveName -> curveName |> KeyCurveName.ArmValue + | _ -> null + kty = this.KTY |> KeyType.ArmValue + keyOps = + if this.KeyOps.IsEmpty then + Unchecked.defaultof<_> + else + this.KeyOps |> List.map KeyOperation.ArmValue + keySize = + match this.KTY with + | RSA(RsaKeyLength keySize) -> box keySize + | RSAHSM(RsaKeyLength keySize) -> box keySize + | _ -> null + |} + |} type CreateMode = | Recover | Default -type Vault = - { - Name: ResourceName - Location: Location - TenantId: string - Sku: KeyVault.Sku - Uri: Uri option - Deployment: FeatureFlag option - DiskEncryption: FeatureFlag option - RbacAuthorization: FeatureFlag option - TemplateDeployment: FeatureFlag option - SoftDelete: SoftDeletionMode option - CreateMode: CreateMode option - AccessPolicies: {| ObjectId: ArmExpression - ApplicationId: Guid option - Permissions: {| Keys: Key Set - Secrets: Secret Set - Certificates: Certificate Set - Storage: Storage Set |} |} list - DefaultAction: DefaultAction option - Bypass: Bypass option - IpRules: string list - VnetRules: string list - DisablePublicNetworkAccess: FeatureFlag option - Tags: Map - } +type Vault = { + Name: ResourceName + Location: Location + TenantId: string + Sku: KeyVault.Sku + Uri: Uri option + Deployment: FeatureFlag option + DiskEncryption: FeatureFlag option + RbacAuthorization: FeatureFlag option + TemplateDeployment: FeatureFlag option + SoftDelete: SoftDeletionMode option + CreateMode: CreateMode option + AccessPolicies: + {| + ObjectId: ArmExpression + ApplicationId: Guid option + Permissions: + {| + Keys: Key Set + Secrets: Secret Set + Certificates: Certificate Set + Storage: Storage Set + |} + |} list + DefaultAction: DefaultAction option + Bypass: Bypass option + IpRules: string list + VnetRules: string list + DisablePublicNetworkAccess: FeatureFlag option + Tags: Map +} with member this.PurgeProtection = match this.SoftDelete with @@ -166,75 +165,75 @@ type Vault = interface IArmResource with member this.ResourceId = vaults.resourceId this.Name - member this.JsonModel = - {| vaults.Create(this.Name, this.Location, this.Dependencies, this.Tags) with - properties = - {| - tenantId = this.TenantId - sku = - {| - name = this.Sku.ArmValue - family = "A" - |} - enabledForDeployment = this.Deployment |> Option.map (fun f -> f.AsBoolean) |> Option.toNullable - enabledForDiskEncryption = - this.DiskEncryption |> Option.map (fun f -> f.AsBoolean) |> Option.toNullable - enabledForTemplateDeployment = - this.TemplateDeployment - |> Option.map (fun f -> f.AsBoolean) - |> Option.toNullable - enableRbacAuthorization = - this.RbacAuthorization |> Option.map (fun f -> f.AsBoolean) |> Option.toNullable - enableSoftDelete = - match this.SoftDelete with - | None -> Nullable() - | Some SoftDeleteWithPurgeProtection - | Some SoftDeletionOnly -> Nullable true - createMode = this.CreateMode |> Option.map (fun m -> m.ToString().ToLower()) |> Option.toObj - enablePurgeProtection = this.PurgeProtection |> Option.toNullable - vaultUri = this.Uri |> Option.map string |> Option.toObj - accessPolicies = - [| - for policy in this.AccessPolicies do - {| - objectId = ArmExpression.Eval policy.ObjectId - tenantId = this.TenantId - applicationId = policy.ApplicationId |> Option.map string |> Option.toObj - permissions = - {| - keys = this.ToStringArray policy.Permissions.Keys - storage = this.ToStringArray policy.Permissions.Storage - certificates = this.ToStringArray policy.Permissions.Certificates - secrets = this.ToStringArray policy.Permissions.Secrets - |} - |} - |] - networkAcls = + member this.JsonModel = {| + vaults.Create(this.Name, this.Location, this.Dependencies, this.Tags) with + properties = {| + tenantId = this.TenantId + sku = {| + name = this.Sku.ArmValue + family = "A" + |} + enabledForDeployment = this.Deployment |> Option.map (fun f -> f.AsBoolean) |> Option.toNullable + enabledForDiskEncryption = + this.DiskEncryption |> Option.map (fun f -> f.AsBoolean) |> Option.toNullable + enabledForTemplateDeployment = + this.TemplateDeployment + |> Option.map (fun f -> f.AsBoolean) + |> Option.toNullable + enableRbacAuthorization = + this.RbacAuthorization |> Option.map (fun f -> f.AsBoolean) |> Option.toNullable + enableSoftDelete = + match this.SoftDelete with + | None -> Nullable() + | Some SoftDeleteWithPurgeProtection + | Some SoftDeletionOnly -> Nullable true + createMode = this.CreateMode |> Option.map (fun m -> m.ToString().ToLower()) |> Option.toObj + enablePurgeProtection = this.PurgeProtection |> Option.toNullable + vaultUri = this.Uri |> Option.map string |> Option.toObj + accessPolicies = [| + for policy in this.AccessPolicies do {| - defaultAction = this.DefaultAction |> Option.map string |> Option.toObj - bypass = this.Bypass |> Option.map string |> Option.toObj - ipRules = this.IpRules - virtualNetworkRules = this.VnetRules |> List.map (fun rule -> {| id = rule |}) + objectId = ArmExpression.Eval policy.ObjectId + tenantId = this.TenantId + applicationId = policy.ApplicationId |> Option.map string |> Option.toObj + permissions = {| + keys = this.ToStringArray policy.Permissions.Keys + storage = this.ToStringArray policy.Permissions.Storage + certificates = this.ToStringArray policy.Permissions.Certificates + secrets = this.ToStringArray policy.Permissions.Secrets + |} |} - publicNetworkAccess = - match this.DisablePublicNetworkAccess with - | Some FeatureFlag.Enabled -> "Disabled" - | Some FeatureFlag.Disabled -> "Enabled" - | None -> null + |] + networkAcls = {| + defaultAction = this.DefaultAction |> Option.map string |> Option.toObj + bypass = this.Bypass |> Option.map string |> Option.toObj + ipRules = this.IpRules + virtualNetworkRules = this.VnetRules |> List.map (fun rule -> {| id = rule |}) |} - |} + publicNetworkAccess = + match this.DisablePublicNetworkAccess with + | Some FeatureFlag.Enabled -> "Disabled" + | Some FeatureFlag.Disabled -> "Enabled" + | None -> null + |} + |} -type VaultAddPolicies = - { - KeyVault: LinkedResource - TenantId: string option - AccessPolicies: {| ObjectId: ArmExpression - ApplicationId: Guid option - Permissions: {| Keys: Key Set - Secrets: Secret Set - Certificates: Certificate Set - Storage: Storage Set |} |} list - } +type VaultAddPolicies = { + KeyVault: LinkedResource + TenantId: string option + AccessPolicies: + {| + ObjectId: ArmExpression + ApplicationId: Guid option + Permissions: + {| + Keys: Key Set + Secrets: Secret Set + Certificates: Certificate Set + Storage: Storage Set + |} + |} list +} with member private _.ToStringArray s = s |> Set.map (fun s -> s.ToString().ToLower()) |> Set.toArray @@ -249,24 +248,22 @@ type VaultAddPolicies = | Managed kvResId -> [ kvResId ] | _ -> [] - {| accessPolicies.Create(this.KeyVault.Name / (ResourceName "add"), dependsOn = dependencies) with - properties = - {| - accessPolicies = - [| - for policy in this.AccessPolicies do - {| - objectId = ArmExpression.Eval policy.ObjectId - tenantId = this.TenantId |> Option.defaultValue "[subscription().tenantId]" - applicationId = policy.ApplicationId |> Option.map string |> Option.toObj - permissions = - {| - keys = this.ToStringArray policy.Permissions.Keys - storage = this.ToStringArray policy.Permissions.Storage - certificates = this.ToStringArray policy.Permissions.Certificates - secrets = this.ToStringArray policy.Permissions.Secrets - |} + {| + accessPolicies.Create(this.KeyVault.Name / (ResourceName "add"), dependsOn = dependencies) with + properties = {| + accessPolicies = [| + for policy in this.AccessPolicies do + {| + objectId = ArmExpression.Eval policy.ObjectId + tenantId = this.TenantId |> Option.defaultValue "[subscription().tenantId]" + applicationId = policy.ApplicationId |> Option.map string |> Option.toObj + permissions = {| + keys = this.ToStringArray policy.Permissions.Keys + storage = this.ToStringArray policy.Permissions.Storage + certificates = this.ToStringArray policy.Permissions.Certificates + secrets = this.ToStringArray policy.Permissions.Secrets |} - |] + |} + |] |} |} diff --git a/src/Farmer/Arm/LoadBalancer.fs b/src/Farmer/Arm/LoadBalancer.fs index abc94f752..da1e10537 100644 --- a/src/Farmer/Arm/LoadBalancer.fs +++ b/src/Farmer/Arm/LoadBalancer.fs @@ -14,148 +14,148 @@ let loadBalancerBackendAddressPools = let loadBalancerProbes = ResourceType("Microsoft.Network/loadBalancers/probes", "2020-11-01") -type LoadBalancer = - { - Name: ResourceName - Location: Location - Sku: LoadBalancerSku - FrontendIpConfigs: {| Name: ResourceName - PrivateIpAllocationMethod: PrivateIpAddress.AllocationMethod - PublicIp: ResourceId option - Subnet: ResourceId option |} list - BackendAddressPools: ResourceName list - LoadBalancingRules: {| Name: ResourceName - FrontendIpConfiguration: ResourceName - BackendAddressPool: ResourceName - Probe: ResourceName option - FrontendPort: uint16 - BackendPort: uint16 - Protocol: TransmissionProtocol option // default "All" - IdleTimeoutMinutes: int option // default 4 minutes - LoadDistribution: LoadDistributionPolicy - EnableTcpReset: bool option // default false - DisableOutboundSnat: bool option |} list // default true - Probes: {| Name: ResourceName - Protocol: LoadBalancerProbeProtocol - Port: uint16 - RequestPath: string - IntervalInSeconds: int - NumberOfProbes: int |} list - Dependencies: Set - Tags: Map - } +type LoadBalancer = { + Name: ResourceName + Location: Location + Sku: LoadBalancerSku + FrontendIpConfigs: + {| + Name: ResourceName + PrivateIpAllocationMethod: PrivateIpAddress.AllocationMethod + PublicIp: ResourceId option + Subnet: ResourceId option + |} list + BackendAddressPools: ResourceName list + LoadBalancingRules: + {| + Name: ResourceName + FrontendIpConfiguration: ResourceName + BackendAddressPool: ResourceName + Probe: ResourceName option + FrontendPort: uint16 + BackendPort: uint16 + Protocol: TransmissionProtocol option // default "All" + IdleTimeoutMinutes: int option // default 4 minutes + LoadDistribution: LoadDistributionPolicy + EnableTcpReset: bool option // default false + DisableOutboundSnat: bool option + |} list // default true + Probes: + {| + Name: ResourceName + Protocol: LoadBalancerProbeProtocol + Port: uint16 + RequestPath: string + IntervalInSeconds: int + NumberOfProbes: int + |} list + Dependencies: Set + Tags: Map +} with interface IArmResource with member this.ResourceId = loadBalancers.resourceId this.Name - member this.JsonModel = - {| loadBalancers.Create(this.Name, this.Location, this.Dependencies, this.Tags) with - sku = - {| - name = this.Sku.Name.ArmValue - tier = this.Sku.Tier.ArmValue - |} - properties = - {| - frontendIpConfigurations = - this.FrontendIpConfigs - |> List.map (fun frontend -> - let allocationMethod, ip = - match frontend.PrivateIpAllocationMethod with - | PrivateIpAddress.DynamicPrivateIp -> "Dynamic", null - | PrivateIpAddress.StaticPrivateIp ip -> "Static", string ip + member this.JsonModel = {| + loadBalancers.Create(this.Name, this.Location, this.Dependencies, this.Tags) with + sku = {| + name = this.Sku.Name.ArmValue + tier = this.Sku.Tier.ArmValue + |} + properties = {| + frontendIpConfigurations = + this.FrontendIpConfigs + |> List.map (fun frontend -> + let allocationMethod, ip = + match frontend.PrivateIpAllocationMethod with + | PrivateIpAddress.DynamicPrivateIp -> "Dynamic", null + | PrivateIpAddress.StaticPrivateIp ip -> "Static", string ip - {| - name = frontend.Name.Value - properties = - {| - privateIPAllocationMethod = allocationMethod - privateIPAddress = ip - publicIPAddress = - frontend.PublicIp - |> Option.map (fun pip -> {| id = pip.Eval() |}) - |> Option.defaultValue Unchecked.defaultof<_> - subnet = - frontend.Subnet - |> Option.map (fun subnetId -> {| id = subnetId.Eval() |}) - |> Option.defaultValue Unchecked.defaultof<_> - |} - |}) - backendAddressPools = - this.BackendAddressPools |> List.map (fun backend -> {| name = backend.Value |}) - loadBalancingRules = - this.LoadBalancingRules - |> List.map (fun rule -> - {| - name = rule.Name.Value - properties = - {| - frontendIPConfiguration = - {| - id = - loadBalancerFrontendIPConfigurations - .resourceId(this.Name, rule.FrontendIpConfiguration) - .Eval() - |} - frontendPort = rule.FrontendPort - backendPort = rule.BackendPort - protocol = - rule.Protocol - |> Option.map (function - | TransmissionProtocol.TCP -> "Tcp" - | TransmissionProtocol.UDP -> "Udp") - |> Option.defaultValue "All" - idleTimeoutInMinutes = rule.IdleTimeoutMinutes |> Option.defaultValue 4 - enableTcpReset = rule.EnableTcpReset |> Option.defaultValue false - disableOutboundSnat = rule.DisableOutboundSnat |> Option.defaultValue true - loadDistribution = rule.LoadDistribution.ArmValue - backendAddressPool = - {| - id = - loadBalancerBackendAddressPools - .resourceId(this.Name, rule.BackendAddressPool) - .Eval() - |} - probe = - rule.Probe - |> Option.map (fun probe -> - {| - id = loadBalancerProbes.resourceId(this.Name, probe).Eval() - |}) - |> Option.defaultValue Unchecked.defaultof<_> - |} - |}) - probes = - this.Probes - |> List.map (fun probe -> - {| - name = probe.Name.Value - properties = - {| - protocol = probe.Protocol.ArmValue - port = probe.Port - requestPath = probe.RequestPath - intervalInSeconds = probe.IntervalInSeconds - numberOfProbes = probe.NumberOfProbes - |} - |}) - inboundNatRules = [] - outboundNatRules = [] - inboundNatPools = [] - |} - |} + {| + name = frontend.Name.Value + properties = {| + privateIPAllocationMethod = allocationMethod + privateIPAddress = ip + publicIPAddress = + frontend.PublicIp + |> Option.map (fun pip -> {| id = pip.Eval() |}) + |> Option.defaultValue Unchecked.defaultof<_> + subnet = + frontend.Subnet + |> Option.map (fun subnetId -> {| id = subnetId.Eval() |}) + |> Option.defaultValue Unchecked.defaultof<_> + |} + |}) + backendAddressPools = + this.BackendAddressPools |> List.map (fun backend -> {| name = backend.Value |}) + loadBalancingRules = + this.LoadBalancingRules + |> List.map (fun rule -> {| + name = rule.Name.Value + properties = {| + frontendIPConfiguration = {| + id = + loadBalancerFrontendIPConfigurations + .resourceId(this.Name, rule.FrontendIpConfiguration) + .Eval() + |} + frontendPort = rule.FrontendPort + backendPort = rule.BackendPort + protocol = + rule.Protocol + |> Option.map (function + | TransmissionProtocol.TCP -> "Tcp" + | TransmissionProtocol.UDP -> "Udp") + |> Option.defaultValue "All" + idleTimeoutInMinutes = rule.IdleTimeoutMinutes |> Option.defaultValue 4 + enableTcpReset = rule.EnableTcpReset |> Option.defaultValue false + disableOutboundSnat = rule.DisableOutboundSnat |> Option.defaultValue true + loadDistribution = rule.LoadDistribution.ArmValue + backendAddressPool = {| + id = + loadBalancerBackendAddressPools + .resourceId(this.Name, rule.BackendAddressPool) + .Eval() + |} + probe = + rule.Probe + |> Option.map (fun probe -> {| + id = loadBalancerProbes.resourceId(this.Name, probe).Eval() + |}) + |> Option.defaultValue Unchecked.defaultof<_> + |} + |}) + probes = + this.Probes + |> List.map (fun probe -> {| + name = probe.Name.Value + properties = {| + protocol = probe.Protocol.ArmValue + port = probe.Port + requestPath = probe.RequestPath + intervalInSeconds = probe.IntervalInSeconds + numberOfProbes = probe.NumberOfProbes + |} + |}) + inboundNatRules = [] + outboundNatRules = [] + inboundNatPools = [] + |} + |} -type BackendAddressPool = - { - /// Name of the backend address pool - Name: ResourceName - /// Name of the load balancer where this pool will be added. - LoadBalancer: ResourceName - /// Addresses of backend services. - LoadBalancerBackendAddresses: {| Name: ResourceName - VirtualNetwork: LinkedResource option - IpAddress: System.Net.IPAddress |} list - } +type BackendAddressPool = { + /// Name of the backend address pool + Name: ResourceName + /// Name of the load balancer where this pool will be added. + LoadBalancer: ResourceName + /// Addresses of backend services. + LoadBalancerBackendAddresses: + {| + Name: ResourceName + VirtualNetwork: LinkedResource option + IpAddress: System.Net.IPAddress + |} list +} with interface IArmResource with member this.ResourceId = @@ -168,29 +168,27 @@ type BackendAddressPool = for addr in this.LoadBalancerBackendAddresses do match addr.VirtualNetwork with - | Some (Managed vnetId) -> yield vnetId + | Some(Managed vnetId) -> yield vnetId | _ -> () } |> Set.ofSeq - {| loadBalancerBackendAddressPools.Create(this.Name, dependsOn = dependencies) with - name = $"{this.LoadBalancer.Value}/{this.Name.Value}" - properties = - {| + {| + loadBalancerBackendAddressPools.Create(this.Name, dependsOn = dependencies) with + name = $"{this.LoadBalancer.Value}/{this.Name.Value}" + properties = {| loadBalancerBackendAddresses = this.LoadBalancerBackendAddresses - |> List.map (fun addr -> - {| - name = addr.Name.Value - properties = - {| - ipAddress = string addr.IpAddress - virtualNetwork = - match addr.VirtualNetwork with - | Some (Managed vnetId) -> {| id = vnetId.Eval() |} - | Some (Unmanaged vnetId) -> {| id = vnetId.Eval() |} - | None -> Unchecked.defaultof<_> - |} - |}) + |> List.map (fun addr -> {| + name = addr.Name.Value + properties = {| + ipAddress = string addr.IpAddress + virtualNetwork = + match addr.VirtualNetwork with + | Some(Managed vnetId) -> {| id = vnetId.Eval() |} + | Some(Unmanaged vnetId) -> {| id = vnetId.Eval() |} + | None -> Unchecked.defaultof<_> + |} + |}) |} |} diff --git a/src/Farmer/Arm/LogAnalytics.fs b/src/Farmer/Arm/LogAnalytics.fs index 4638f86fe..3b2117b2e 100644 --- a/src/Farmer/Arm/LogAnalytics.fs +++ b/src/Farmer/Arm/LogAnalytics.fs @@ -6,36 +6,33 @@ open Farmer let workspaces = ResourceType("Microsoft.OperationalInsights/workspaces", "2020-03-01-preview") -type Workspace = - { - Name: ResourceName - Location: Location - RetentionPeriod: int option - IngestionSupport: FeatureFlag option - QuerySupport: FeatureFlag option - DailyCap: int option - Tags: Map - } +type Workspace = { + Name: ResourceName + Location: Location + RetentionPeriod: int option + IngestionSupport: FeatureFlag option + QuerySupport: FeatureFlag option + DailyCap: int option + Tags: Map +} with interface IArmResource with member this.ResourceId = workspaces.resourceId this.Name - member this.JsonModel = - {| workspaces.Create(this.Name, this.Location, tags = this.Tags) with - properties = - {| - sku = {| name = "PerGB2018" |} - retentionInDays = this.RetentionPeriod |> Option.toNullable - workspaceCapping = - match this.DailyCap with - | None -> null - | Some cap -> {| dailyQuotaGb = cap |} |> box - publicNetworkAccessForIngestion = - this.IngestionSupport |> Option.map (fun f -> f.ArmValue) |> Option.toObj - publicNetworkAccessForQuery = - this.QuerySupport |> Option.map (fun f -> f.ArmValue) |> Option.toObj - |} - |} + member this.JsonModel = {| + workspaces.Create(this.Name, this.Location, tags = this.Tags) with + properties = {| + sku = {| name = "PerGB2018" |} + retentionInDays = this.RetentionPeriod |> Option.toNullable + workspaceCapping = + match this.DailyCap with + | None -> null + | Some cap -> {| dailyQuotaGb = cap |} |> box + publicNetworkAccessForIngestion = + this.IngestionSupport |> Option.map (fun f -> f.ArmValue) |> Option.toObj + publicNetworkAccessForQuery = this.QuerySupport |> Option.map (fun f -> f.ArmValue) |> Option.toObj + |} + |} type LogAnalytics = static member getCustomerId resourceId = diff --git a/src/Farmer/Arm/LogicApps.fs b/src/Farmer/Arm/LogicApps.fs index e6a64e58c..947a2c7ef 100644 --- a/src/Farmer/Arm/LogicApps.fs +++ b/src/Farmer/Arm/LogicApps.fs @@ -6,18 +6,17 @@ open System.Text.Json let workflows = ResourceType("Microsoft.Logic/workflows", "2019-05-01") -type LogicApp = - { - Name: ResourceName - Location: Location - Definition: JsonDocument - Tags: Map - } +type LogicApp = { + Name: ResourceName + Location: Location + Definition: JsonDocument + Tags: Map +} with interface IArmResource with member this.ResourceId = workflows.resourceId this.Name - member this.JsonModel = - {| workflows.Create(this.Name, this.Location, tags = this.Tags) with + member this.JsonModel = {| + workflows.Create(this.Name, this.Location, tags = this.Tags) with properties = this.Definition - |} + |} diff --git a/src/Farmer/Arm/ManagedIdentity.fs b/src/Farmer/Arm/ManagedIdentity.fs index afe1d7232..832fa63b2 100644 --- a/src/Farmer/Arm/ManagedIdentity.fs +++ b/src/Farmer/Arm/ManagedIdentity.fs @@ -7,12 +7,11 @@ open Farmer.Identity let userAssignedIdentities = ResourceType("Microsoft.ManagedIdentity/userAssignedIdentities", "2018-11-30") -type UserAssignedIdentity = - { - Name: ResourceName - Location: Location - Tags: Map - } +type UserAssignedIdentity = { + Name: ResourceName + Location: Location + Tags: Map +} with interface IArmResource with member this.ResourceId = userAssignedIdentities.resourceId this.Name @@ -26,41 +25,37 @@ let toArmJson = | { SystemAssigned = Disabled UserAssigned = [] - } -> - {| - ``type`` = "None" - userAssignedIdentities = null - |} + } -> {| + ``type`` = "None" + userAssignedIdentities = null + |} | { SystemAssigned = Enabled UserAssigned = [] - } -> - {| - ``type`` = "SystemAssigned" - userAssignedIdentities = null - |} + } -> {| + ``type`` = "SystemAssigned" + userAssignedIdentities = null + |} | { SystemAssigned = Disabled UserAssigned = identities - } -> - {| - ``type`` = "UserAssigned" - userAssignedIdentities = - identities - |> List.map (fun identity -> identity.ResourceId.Eval(), obj ()) - |> dict - |} + } -> {| + ``type`` = "UserAssigned" + userAssignedIdentities = + identities + |> List.map (fun identity -> identity.ResourceId.Eval(), obj ()) + |> dict + |} | { SystemAssigned = Enabled UserAssigned = identities - } -> - {| - ``type`` = "SystemAssigned, UserAssigned" - userAssignedIdentities = - identities - |> List.map (fun identity -> identity.ResourceId.Eval(), obj ()) - |> dict - |} + } -> {| + ``type`` = "SystemAssigned, UserAssigned" + userAssignedIdentities = + identities + |> List.map (fun identity -> identity.ResourceId.Eval(), obj ()) + |> dict + |} type ManagedIdentity with diff --git a/src/Farmer/Arm/Maps.fs b/src/Farmer/Arm/Maps.fs index 89c4196e0..de41ab7c5 100644 --- a/src/Farmer/Arm/Maps.fs +++ b/src/Farmer/Arm/Maps.fs @@ -6,24 +6,22 @@ open Farmer.Maps let accounts = ResourceType("Microsoft.Maps/accounts", "2018-05-01") -type Maps = - { - Name: ResourceName - Location: Location - Sku: Sku - Tags: Map - } +type Maps = { + Name: ResourceName + Location: Location + Sku: Sku + Tags: Map +} with interface IArmResource with member this.ResourceId = accounts.resourceId this.Name - member this.JsonModel = - {| accounts.Create(this.Name, this.Location, tags = this.Tags) with - sku = - {| - name = - match this.Sku with - | S0 -> "S0" - | S1 -> "S1" - |} - |} + member this.JsonModel = {| + accounts.Create(this.Name, this.Location, tags = this.Tags) with + sku = {| + name = + match this.Sku with + | S0 -> "S0" + | S1 -> "S1" + |} + |} diff --git a/src/Farmer/Arm/Network.fs b/src/Farmer/Arm/Network.fs index 6e5e30983..a2b0e99f6 100644 --- a/src/Farmer/Arm/Network.fs +++ b/src/Farmer/Arm/Network.fs @@ -61,17 +61,17 @@ type SubnetReference = member this.ResourceId: ResourceId = match this with - | ViaManagedVNet (vnetId, subnet) -> - { vnetId 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 + | ViaManagedVNet(id, _) + | Direct(Managed id) -> Some id | _ -> None static member create(vnetRef: LinkedResource, subnetName: ResourceName) = @@ -82,11 +82,11 @@ type SubnetReference = | Managed vnetId -> ViaManagedVNet(vnetId, subnetName) | Unmanaged vnetId -> Direct( - Unmanaged - { vnetId with + Unmanaged { + vnetId with Type = subnets Segments = [ subnetName ] - } + } ) static member create(subnetRef: LinkedResource) = @@ -95,92 +95,85 @@ type SubnetReference = Direct subnetRef -type Route = - { - Name: ResourceName - AddressPrefix: IPAddressCidr - NextHopType: Route.HopType - HasBgpOverride: FeatureFlag - } - - member internal this.JsonModelProperties = - {| - addressPrefix = IPAddressCidr.format this.AddressPrefix - nextHopType = this.NextHopType.ArmValue - nextHopIpAddress = - match this.NextHopType with - | VirtualAppliance ip -> - ip - |> Option.map (fun x -> x.ToString()) - |> Option.defaultValue Unchecked.defaultof<_> - | _ -> Unchecked.defaultof<_> - hasBgpOverride = this.HasBgpOverride.AsBoolean - |} +type Route = { + Name: ResourceName + AddressPrefix: IPAddressCidr + NextHopType: Route.HopType + HasBgpOverride: FeatureFlag +} with + + member internal this.JsonModelProperties = {| + addressPrefix = IPAddressCidr.format this.AddressPrefix + nextHopType = this.NextHopType.ArmValue + nextHopIpAddress = + match this.NextHopType with + | VirtualAppliance ip -> + ip + |> Option.map (fun x -> x.ToString()) + |> Option.defaultValue Unchecked.defaultof<_> + | _ -> Unchecked.defaultof<_> + hasBgpOverride = this.HasBgpOverride.AsBoolean + |} interface IArmResource with member this.ResourceId = routes.resourceId this.Name - member this.JsonModel = - {| routes.Create(this.Name) with + member this.JsonModel = {| + routes.Create(this.Name) with properties = this.JsonModelProperties - |} - -type RouteTable = - { - Name: ResourceName - Location: Location - Tags: Map - DisableBGPRoutePropagation: FeatureFlag - Routes: Route list - } - - member internal this.JsonModelProperties = - {| - disableBgpRoutePropagation = this.DisableBGPRoutePropagation.AsBoolean - routes = - this.Routes - |> Seq.map (fun x -> - {| - name = x.Name.Value - properties = x.JsonModelProperties - |}) |} +type RouteTable = { + Name: ResourceName + Location: Location + Tags: Map + DisableBGPRoutePropagation: FeatureFlag + Routes: Route list +} with + + member internal this.JsonModelProperties = {| + disableBgpRoutePropagation = this.DisableBGPRoutePropagation.AsBoolean + routes = + this.Routes + |> Seq.map (fun x -> {| + name = x.Name.Value + properties = x.JsonModelProperties + |}) + |} + interface IArmResource with member this.ResourceId = routeTables.resourceId this.Name - member this.JsonModel = - {| routeTables.Create(this.Name, this.Location, tags = this.Tags) with + member this.JsonModel = {| + routeTables.Create(this.Name, this.Location, tags = this.Tags) with properties = this.JsonModelProperties - |} + |} -type PublicIpAddress = - { - Name: ResourceName - AvailabilityZone: string option - Location: Location - Sku: PublicIpAddress.Sku - AllocationMethod: PublicIpAddress.AllocationMethod - DomainNameLabel: string option - Tags: Map - } +type PublicIpAddress = { + Name: ResourceName + AvailabilityZone: string option + Location: Location + Sku: PublicIpAddress.Sku + AllocationMethod: PublicIpAddress.AllocationMethod + DomainNameLabel: string option + Tags: Map +} with interface IArmResource with member this.ResourceId = publicIPAddresses.resourceId this.Name - member this.JsonModel = - {| publicIPAddresses.Create(this.Name, this.Location, tags = this.Tags) with + member this.JsonModel = {| + publicIPAddresses.Create(this.Name, this.Location, tags = this.Tags) with sku = {| name = this.Sku.ArmValue |} - properties = - {| - publicIPAllocationMethod = this.AllocationMethod.ArmValue - dnsSettings = - match this.DomainNameLabel with - | Some label -> box {| domainNameLabel = label.ToLower() |} - | None -> null - |} + properties = {| + publicIPAllocationMethod = this.AllocationMethod.ArmValue + dnsSettings = + match this.DomainNameLabel with + | Some label -> box {| domainNameLabel = label.ToLower() |} + | None -> null + |} zones = this.AvailabilityZone |> Option.map ResizeArray |> Option.toObj - |} + |} /// If using the IPs in the frontend of a cross-region laod balancer, public IPs and prefixes must be in /// the Global tier, otherwise regional IPs are sufficient. @@ -194,113 +187,103 @@ type PublicIpPrefixTier = | Regional -> "Regional" /// Public IP Prefix creates a block of contiguous public IP addresses that can be assigned to resources. -type PublicIpPrefix = - { - Name: ResourceName - Location: Location - PrefixLength: int - Tier: PublicIpPrefixTier - Tags: Map - } +type PublicIpPrefix = { + Name: ResourceName + Location: Location + PrefixLength: int + Tier: PublicIpPrefixTier + Tags: Map +} with interface IArmResource with member this.ResourceId = publicIPPrefixes.resourceId this.Name - member this.JsonModel = - {| publicIPPrefixes.Create(this.Name, this.Location, tags = this.Tags) with - sku = - {| - name = "Standard" - tier = this.Tier.ArmValue - |} - properties = - {| - prefixLength = this.PrefixLength - publicIPAddressVersion = "IPv4" - |} - |} - -type SubnetDelegation = - { - Name: ResourceName - ServiceName: string - } - -type Subnet = - { - Name: ResourceName - Prefix: string - VirtualNetwork: LinkedResource option - NetworkSecurityGroup: LinkedResource option - Delegations: SubnetDelegation list - NatGateway: LinkedResource option - ServiceEndpoints: (Network.EndpointServiceType * Location list) list - AssociatedServiceEndpointPolicies: ResourceId list - PrivateEndpointNetworkPolicies: FeatureFlag option - PrivateLinkServiceNetworkPolicies: FeatureFlag option - } - - member internal this.JsonModelProperties = - {| - addressPrefix = this.Prefix - natGateway = - this.NatGateway - |> Option.map LinkedResource.AsIdObject - |> Option.defaultValue Unchecked.defaultof<_> - networkSecurityGroup = - this.NetworkSecurityGroup - |> Option.map (fun nsg -> - {| - id = nsg.ResourceId.ArmExpression.Eval() - |}) - |> Option.defaultValue Unchecked.defaultof<_> - delegations = - this.Delegations - |> List.map (fun delegation -> - {| - name = delegation.Name.Value - properties = - {| - serviceName = delegation.ServiceName - |} - |}) - serviceEndpoints = - if this.ServiceEndpoints.IsEmpty then - Unchecked.defaultof<_> - else - this.ServiceEndpoints - |> List.map (fun (Network.EndpointServiceType (serviceEndpoint), locations) -> - {| - service = serviceEndpoint - locations = locations |> List.map (fun location -> location.ArmValue) - |}) - serviceEndpointPolicies = - if this.AssociatedServiceEndpointPolicies.IsEmpty then - Unchecked.defaultof<_> - else - this.AssociatedServiceEndpointPolicies - |> List.map (fun policyId -> {| id = policyId.ArmExpression.Eval() |}) - privateEndpointNetworkPolicies = - this.PrivateEndpointNetworkPolicies - |> Option.map (fun x -> x.ArmValue) - |> Option.defaultValue Unchecked.defaultof<_> - privateLinkServiceNetworkPolicies = - this.PrivateLinkServiceNetworkPolicies - |> Option.map (fun x -> x.ArmValue) - |> Option.defaultValue Unchecked.defaultof<_> + member this.JsonModel = {| + publicIPPrefixes.Create(this.Name, this.Location, tags = this.Tags) with + sku = {| + name = "Standard" + tier = this.Tier.ArmValue + |} + properties = {| + prefixLength = this.PrefixLength + publicIPAddressVersion = "IPv4" + |} |} +type SubnetDelegation = { + Name: ResourceName + ServiceName: string +} + +type Subnet = { + Name: ResourceName + Prefix: string + VirtualNetwork: LinkedResource option + NetworkSecurityGroup: LinkedResource option + Delegations: SubnetDelegation list + NatGateway: LinkedResource option + ServiceEndpoints: (Network.EndpointServiceType * Location list) list + AssociatedServiceEndpointPolicies: ResourceId list + PrivateEndpointNetworkPolicies: FeatureFlag option + PrivateLinkServiceNetworkPolicies: FeatureFlag option +} with + + member internal this.JsonModelProperties = {| + addressPrefix = this.Prefix + natGateway = + this.NatGateway + |> Option.map LinkedResource.AsIdObject + |> Option.defaultValue Unchecked.defaultof<_> + networkSecurityGroup = + this.NetworkSecurityGroup + |> Option.map (fun nsg -> {| + id = nsg.ResourceId.ArmExpression.Eval() + |}) + |> Option.defaultValue Unchecked.defaultof<_> + delegations = + this.Delegations + |> List.map (fun delegation -> {| + name = delegation.Name.Value + properties = {| + serviceName = delegation.ServiceName + |} + |}) + serviceEndpoints = + if this.ServiceEndpoints.IsEmpty then + Unchecked.defaultof<_> + else + this.ServiceEndpoints + |> List.map (fun (Network.EndpointServiceType(serviceEndpoint), locations) -> {| + service = serviceEndpoint + locations = locations |> List.map (fun location -> location.ArmValue) + |}) + serviceEndpointPolicies = + if this.AssociatedServiceEndpointPolicies.IsEmpty then + Unchecked.defaultof<_> + else + this.AssociatedServiceEndpointPolicies + |> List.map (fun policyId -> {| id = policyId.ArmExpression.Eval() |}) + privateEndpointNetworkPolicies = + this.PrivateEndpointNetworkPolicies + |> Option.map (fun x -> x.ArmValue) + |> Option.defaultValue Unchecked.defaultof<_> + privateLinkServiceNetworkPolicies = + this.PrivateLinkServiceNetworkPolicies + |> Option.map (fun x -> x.ArmValue) + |> Option.defaultValue Unchecked.defaultof<_> + |} + interface IArmResource with member this.JsonModel = match this.VirtualNetwork with - | Some (Managed vnet) -> - {| subnets.Create(vnet.Name / this.Name, dependsOn = [ vnet ]) with + | Some(Managed vnet) -> {| + subnets.Create(vnet.Name / this.Name, dependsOn = [ vnet ]) with properties = this.JsonModelProperties - |} - | Some (Unmanaged vnet) -> - {| subnets.Create(vnet.Name / this.Name) with + |} + | Some(Unmanaged vnet) -> {| + subnets.Create(vnet.Name / this.Name) with properties = this.JsonModelProperties - |} + |} | None -> raiseFarmer "Subnet record must be linked to a virtual network to properly assign the resourceId." member this.ResourceId = @@ -309,14 +292,13 @@ type Subnet = | None -> raiseFarmer "Subnet record must be linked to a virtual network to properly assign the resourceId." -type VirtualNetwork = - { - Name: ResourceName - Location: Location - AddressSpacePrefixes: string list - Subnets: Subnet list - Tags: Map - } +type VirtualNetwork = { + Name: ResourceName + Location: Location + AddressSpacePrefixes: string list + Subnets: Subnet list + Tags: Map +} with interface IArmResource with member this.ResourceId = virtualNetworks.resourceId this.Name @@ -326,29 +308,27 @@ type VirtualNetwork = seq { for subnet in this.Subnets do match subnet.NetworkSecurityGroup with - | Some (Managed id) -> id + | Some(Managed id) -> id | _ -> () match subnet.NatGateway with - | Some (Managed id) -> id + | Some(Managed id) -> id | _ -> () } |> Set - {| virtualNetworks.Create(this.Name, this.Location, dependsOn = dependencies, tags = this.Tags) with - properties = - {| - addressSpace = - {| - addressPrefixes = this.AddressSpacePrefixes - |} + {| + virtualNetworks.Create(this.Name, this.Location, dependsOn = dependencies, tags = this.Tags) with + properties = {| + addressSpace = {| + addressPrefixes = this.AddressSpacePrefixes + |} subnets = this.Subnets - |> List.map (fun subnet -> - {| - name = subnet.Name.Value - properties = subnet.JsonModelProperties - |}) + |> List.map (fun subnet -> {| + name = subnet.Name.Value + properties = subnet.JsonModelProperties + |}) |} |} @@ -357,151 +337,138 @@ type VPNClientProtocol = | SSTP | OpenVPN -type VpnClientConfiguration = - { - ClientAddressPools: IPAddressCidr list - ClientRootCertificates: {| Name: string - PublicCertData: string |} list - ClientRevokedCertificates: {| Name: string; Thumbprint: string |} list - ClientProtocols: VPNClientProtocol list - } - -type VirtualNetworkGateway = - { - Name: ResourceName - Location: Location - IpConfigs: {| Name: ResourceName - PrivateIpAllocationMethod: PrivateIpAddress.AllocationMethod - PublicIpName: ResourceName |} list - VirtualNetwork: ResourceName - GatewayType: GatewayType - VpnType: VpnType - EnableBgp: bool - - VpnClientConfiguration: VpnClientConfiguration option - - Tags: Map - } +type VpnClientConfiguration = { + ClientAddressPools: IPAddressCidr list + ClientRootCertificates: + {| + Name: string + PublicCertData: string + |} list + ClientRevokedCertificates: {| Name: string; Thumbprint: string |} list + ClientProtocols: VPNClientProtocol list +} + +type VirtualNetworkGateway = { + Name: ResourceName + Location: Location + IpConfigs: + {| + Name: ResourceName + PrivateIpAllocationMethod: PrivateIpAddress.AllocationMethod + PublicIpName: ResourceName + |} list + VirtualNetwork: ResourceName + GatewayType: GatewayType + VpnType: VpnType + EnableBgp: bool + + VpnClientConfiguration: VpnClientConfiguration option + + Tags: Map +} with interface IArmResource with member this.ResourceId = virtualNetworkGateways.resourceId this.Name member this.JsonModel = - let dependsOn = - [ - virtualNetworks.resourceId this.VirtualNetwork - for config in this.IpConfigs do - publicIPAddresses.resourceId config.PublicIpName - ] - - {| virtualNetworkGateways.Create(this.Name, this.Location, dependsOn, this.Tags) with - properties = - {| + let dependsOn = [ + virtualNetworks.resourceId this.VirtualNetwork + for config in this.IpConfigs do + publicIPAddresses.resourceId config.PublicIpName + ] + + {| + virtualNetworkGateways.Create(this.Name, this.Location, dependsOn, this.Tags) with + properties = {| ipConfigurations = this.IpConfigs - |> List.mapi (fun index ipConfig -> - {| - name = $"ipconfig{index + 1}" - properties = - let allocationMethod, ip = - match ipConfig.PrivateIpAllocationMethod with - | DynamicPrivateIp -> "Dynamic", null - | StaticPrivateIp ip -> "Static", string ip - - {| - privateIpAllocationMethod = allocationMethod - privateIpAddress = ip - publicIPAddress = - {| - id = publicIPAddresses.resourceId(ipConfig.PublicIpName).Eval() - |} - subnet = - {| - id = - subnets - .resourceId( - this.VirtualNetwork, - ResourceName "GatewaySubnet" - ) - .Eval() - |} + |> List.mapi (fun index ipConfig -> {| + name = $"ipconfig{index + 1}" + properties = + let allocationMethod, ip = + match ipConfig.PrivateIpAllocationMethod with + | DynamicPrivateIp -> "Dynamic", null + | StaticPrivateIp ip -> "Static", string ip + + {| + privateIpAllocationMethod = allocationMethod + privateIpAddress = ip + publicIPAddress = {| + id = publicIPAddresses.resourceId(ipConfig.PublicIpName).Eval() + |} + subnet = {| + id = + subnets + .resourceId(this.VirtualNetwork, ResourceName "GatewaySubnet") + .Eval() |} - |}) + |} + |}) sku = match this.GatewayType with - | GatewayType.ExpressRoute sku -> - {| - name = sku.ArmValue - tier = sku.ArmValue - |} - | GatewayType.Vpn sku -> - {| - name = sku.ArmValue - tier = sku.ArmValue - |} + | GatewayType.ExpressRoute sku -> {| + name = sku.ArmValue + tier = sku.ArmValue + |} + | GatewayType.Vpn sku -> {| + name = sku.ArmValue + tier = sku.ArmValue + |} gatewayType = this.GatewayType.ArmValue vpnType = this.VpnType.ArmValue enableBgp = this.EnableBgp vpnClientConfiguration = match this.VpnClientConfiguration with | Some vpnClientConfig -> - box - {| - vpnClientAddressPool = + box {| + vpnClientAddressPool = {| + addressPrefixes = [ + for prefix in vpnClientConfig.ClientAddressPools do + IPAddressCidr.format prefix + ] + |} + vpnClientProtocols = [ + for protocol in vpnClientConfig.ClientProtocols do + match protocol with + | SSTP -> "SSTP" + | IkeV2 -> "IkeV2" + | OpenVPN -> "OpenVPN" + ] + vpnClientRootCertificates = [ + for cert in vpnClientConfig.ClientRootCertificates do {| - addressPrefixes = - [ - for prefix in vpnClientConfig.ClientAddressPools do - IPAddressCidr.format prefix - ] + name = cert.Name + properties = {| + publicCertData = cert.PublicCertData + |} |} - vpnClientProtocols = - [ - for protocol in vpnClientConfig.ClientProtocols do - match protocol with - | SSTP -> "SSTP" - | IkeV2 -> "IkeV2" - | OpenVPN -> "OpenVPN" - ] - vpnClientRootCertificates = - [ - for cert in vpnClientConfig.ClientRootCertificates do - {| - name = cert.Name - properties = - {| - publicCertData = cert.PublicCertData - |} - |} - ] - vpnClientRevokedCertificates = - [ - for cert in vpnClientConfig.ClientRevokedCertificates do - {| - name = cert.Name - properties = {| thumbprint = cert.Thumbprint |} - |} - ] - radiusServers = [] - vpnClientIpsecPolicies = [] - |} + ] + vpnClientRevokedCertificates = [ + for cert in vpnClientConfig.ClientRevokedCertificates do + {| + name = cert.Name + properties = {| thumbprint = cert.Thumbprint |} + |} + ] + radiusServers = [] + vpnClientIpsecPolicies = [] + |} | None -> null activeActive = this.IpConfigs |> List.length > 1 |} |} -type Connection = - { - Name: ResourceName - Location: Location - ConnectionType: ConnectionType - VirtualNetworkGateway1: ResourceName - VirtualNetworkGateway2: ResourceName option - LocalNetworkGateway: ResourceName option - PeerId: string option - AuthorizationKey: string option - Tags: Map - } +type Connection = { + Name: ResourceName + Location: Location + ConnectionType: ConnectionType + VirtualNetworkGateway1: ResourceName + VirtualNetworkGateway2: ResourceName option + LocalNetworkGateway: ResourceName option + PeerId: string option + AuthorizationKey: string option + Tags: Map +} with member private this.VNetGateway1ResourceId = virtualNetworkGateways.resourceId this.VirtualNetworkGateway1 @@ -524,15 +491,14 @@ type Connection = ] |> List.choose id - {| connections.Create(this.Name, this.Location, dependsOn, this.Tags) with - properties = - {| + {| + connections.Create(this.Name, this.Location, dependsOn, this.Tags) with + properties = {| authorizationKey = this.AuthorizationKey |> Option.toObj connectionType = this.ConnectionType.ArmValue - virtualNetworkGateway1 = - {| - id = this.VNetGateway1ResourceId.Eval() - |} + virtualNetworkGateway1 = {| + id = this.VNetGateway1ResourceId.Eval() + |} virtualNetworkGateway2 = match this.VNetGateway2ResourceId with | Some vng2 -> box {| id = vng2.Eval() |} @@ -549,14 +515,13 @@ type Connection = |} /// IP configuration for a network interface. -type IpConfiguration = - { - SubnetName: ResourceName - PublicIpAddress: LinkedResource option - LoadBalancerBackendAddressPools: LinkedResource list - PrivateIpAllocation: PrivateIpAddress.AllocationMethod option - Primary: bool option - } +type IpConfiguration = { + SubnetName: ResourceName + PublicIpAddress: LinkedResource option + LoadBalancerBackendAddressPools: LinkedResource list + PrivateIpAllocation: PrivateIpAddress.AllocationMethod option + Primary: bool option +} module NetworkInterface = open Vm @@ -577,219 +542,207 @@ module NetworkInterface = | Standard_B8ms -> AcceleratedNetworkingUnsupported // failwithf "Accelerated networking unsupported for specified VM size. Using '%s'." state.Size.ArmValue | _ -> AcceleratedNetworkingSupported -type NetworkInterface = - { - Name: ResourceName - Location: Location - EnableAcceleratedNetworking: bool option - EnableIpForwarding: bool option - IpConfigs: IpConfiguration list - VirtualNetwork: LinkedResource - NetworkSecurityGroup: ResourceId option - Primary: bool option - Tags: Map - } +type NetworkInterface = { + Name: ResourceName + Location: Location + EnableAcceleratedNetworking: bool option + EnableIpForwarding: bool option + IpConfigs: IpConfiguration list + VirtualNetwork: LinkedResource + NetworkSecurityGroup: ResourceId option + Primary: bool option + Tags: Map +} with interface IArmResource with member this.ResourceId = networkInterfaces.resourceId this.Name member this.JsonModel = - let dependsOn = - [ - match this.VirtualNetwork with - | Managed resId -> resId + let dependsOn = [ + match this.VirtualNetwork with + | Managed resId -> resId + | _ -> () + for config in this.IpConfigs do + match config.PublicIpAddress with + | Some ipName -> ipName.ResourceId | _ -> () - for config in this.IpConfigs do - match config.PublicIpAddress with - | Some ipName -> ipName.ResourceId - | _ -> () - for linkedResource in config.LoadBalancerBackendAddressPools do - match linkedResource with - | Managed resId -> resId - | _ -> () - if this.NetworkSecurityGroup.IsSome then - this.NetworkSecurityGroup.Value - ] + for linkedResource in config.LoadBalancerBackendAddressPools do + match linkedResource with + | Managed resId -> resId + | _ -> () + if this.NetworkSecurityGroup.IsSome then + this.NetworkSecurityGroup.Value + ] + + let props = {| + primary = this.Primary |> Option.map box |> Option.toObj + enableAcceleratedNetworking = this.EnableAcceleratedNetworking |> Option.map box |> Option.toObj + enableIPForwarding = this.EnableIpForwarding |> Option.map box |> Option.toObj + ipConfigurations = + this.IpConfigs + |> List.mapi (fun index ipConfig -> {| + name = $"ipconfig{index + 1}" + properties = + let allocationMethod, ip = + match ipConfig.PrivateIpAllocation with + | Some(StaticPrivateIp ip) -> "Static", string ip + | _ -> "Dynamic", null - let props = - {| - primary = this.Primary |> Option.map box |> Option.toObj - enableAcceleratedNetworking = this.EnableAcceleratedNetworking |> Option.map box |> Option.toObj - enableIPForwarding = this.EnableIpForwarding |> Option.map box |> Option.toObj - ipConfigurations = - this.IpConfigs - |> List.mapi (fun index ipConfig -> {| - name = $"ipconfig{index + 1}" - properties = - let allocationMethod, ip = - match ipConfig.PrivateIpAllocation with - | Some (StaticPrivateIp ip) -> "Static", string ip - | _ -> "Dynamic", null - - {| - loadBalancerBackendAddressPools = - match ipConfig.LoadBalancerBackendAddressPools with - | [] -> null // Don't emit the field if there are none set. - | backendPools -> - backendPools - |> List.map (fun lr -> lr.ResourceId |> ResourceId.AsIdObject) - |> box - primary = ipConfig.Primary |> Option.map box |> Option.toObj - privateIPAllocationMethod = allocationMethod - privateIPAddress = ip - publicIPAddress = - ipConfig.PublicIpAddress - |> Option.map (fun pip -> - {| - id = pip.ResourceId.ArmExpression.Eval() - |}) - |> Option.defaultValue Unchecked.defaultof<_> - subnet = - {| - id = - { this.VirtualNetwork.ResourceId with - Type = subnets - Segments = [ ipConfig.SubnetName ] - } - .Eval() - |} - |} - |}) - |} + loadBalancerBackendAddressPools = + match ipConfig.LoadBalancerBackendAddressPools with + | [] -> null // Don't emit the field if there are none set. + | backendPools -> + backendPools + |> List.map (fun lr -> lr.ResourceId |> ResourceId.AsIdObject) + |> box + primary = ipConfig.Primary |> Option.map box |> Option.toObj + privateIPAllocationMethod = allocationMethod + privateIPAddress = ip + publicIPAddress = + ipConfig.PublicIpAddress + |> Option.map (fun pip -> {| + id = pip.ResourceId.ArmExpression.Eval() + |}) + |> Option.defaultValue Unchecked.defaultof<_> + subnet = {| + id = + { + this.VirtualNetwork.ResourceId with + Type = subnets + Segments = [ ipConfig.SubnetName ] + } + .Eval() + |} + |} + |}) + |} match this.NetworkSecurityGroup with - | None -> - {| networkInterfaces.Create(this.Name, this.Location, dependsOn, this.Tags) with + | None -> {| + networkInterfaces.Create(this.Name, this.Location, dependsOn, this.Tags) with properties = props - |} - | Some nsg -> - {| networkInterfaces.Create(this.Name, this.Location, dependsOn, this.Tags) with - properties = - {| props with + |} + | Some nsg -> {| + networkInterfaces.Create(this.Name, this.Location, dependsOn, this.Tags) with + properties = {| + props with networkSecurityGroup = {| id = nsg.Eval() |} - |} - |} + |} + |} -type NetworkProfile = - { - Name: ResourceName - Location: Location - Dependencies: ResourceId Set - ContainerNetworkInterfaceConfigurations: {| IpConfigs: {| Name: ResourceName - SubnetName: ResourceName |} list |} list - VirtualNetwork: ResourceId - Tags: Map - } +type NetworkProfile = { + Name: ResourceName + Location: Location + Dependencies: ResourceId Set + ContainerNetworkInterfaceConfigurations: + {| + IpConfigs: + {| + Name: ResourceName + SubnetName: ResourceName + |} list + |} list + VirtualNetwork: ResourceId + Tags: Map +} with interface IArmResource with member this.ResourceId = networkProfiles.resourceId this.Name - member this.JsonModel = - {| networkProfiles.Create(this.Name, this.Location, this.Dependencies, this.Tags) with - properties = - {| - containerNetworkInterfaceConfigurations = - this.ContainerNetworkInterfaceConfigurations - |> List.mapi (fun index containerIfConfig -> - {| - name = $"eth{index}" - properties = - {| - ipConfigurations = - containerIfConfig.IpConfigs - |> List.mapi (fun index ipConfig -> - {| - name = (ipConfig.Name.IfEmpty $"ipconfig{index + 1}").Value - properties = - {| - subnet = - {| - id = - { subnets.resourceId ( - this.VirtualNetwork.Name, - ipConfig.SubnetName - ) with - ResourceGroup = - this.VirtualNetwork.ResourceGroup - } - .Eval() - |} - |} - |}) + member this.JsonModel = {| + networkProfiles.Create(this.Name, this.Location, this.Dependencies, this.Tags) with + properties = {| + containerNetworkInterfaceConfigurations = + this.ContainerNetworkInterfaceConfigurations + |> List.mapi (fun index containerIfConfig -> {| + name = $"eth{index}" + properties = {| + ipConfigurations = + containerIfConfig.IpConfigs + |> List.mapi (fun index ipConfig -> {| + name = (ipConfig.Name.IfEmpty $"ipconfig{index + 1}").Value + properties = {| + subnet = {| + id = + { + subnets.resourceId ( + this.VirtualNetwork.Name, + ipConfig.SubnetName + ) with + ResourceGroup = this.VirtualNetwork.ResourceGroup + } + .Eval() + |} |} - |}) - |} - |} + |}) + |} + |}) + |} + |} -type ExpressRouteCircuit = - { - Name: ResourceName - Location: Location - Tier: Tier - Family: Family - ServiceProviderName: string - PeeringLocation: string - Bandwidth: int - GlobalReachEnabled: bool - Peerings: {| PeeringType: PeeringType - AzureASN: int - PeerASN: int64 - PrimaryPeerAddressPrefix: IPAddressCidr - SecondaryPeerAddressPrefix: IPAddressCidr - SharedKey: string option - VlanId: int |} list - Tags: Map - } +type ExpressRouteCircuit = { + Name: ResourceName + Location: Location + Tier: Tier + Family: Family + ServiceProviderName: string + PeeringLocation: string + Bandwidth: int + GlobalReachEnabled: bool + Peerings: + {| + PeeringType: PeeringType + AzureASN: int + PeerASN: int64 + PrimaryPeerAddressPrefix: IPAddressCidr + SecondaryPeerAddressPrefix: IPAddressCidr + SharedKey: string option + VlanId: int + |} list + Tags: Map +} with interface IArmResource with member this.ResourceId = expressRouteCircuits.resourceId this.Name - member this.JsonModel = - {| expressRouteCircuits.Create(this.Name, this.Location, tags = this.Tags) with - sku = - {| - name = $"{this.Tier}_{this.Family}" - tier = string this.Tier - family = string this.Family - |} - properties = - {| - peerings = - [ - for peer in this.Peerings do - {| - name = peer.PeeringType.Value - properties = - {| - peeringType = peer.PeeringType.Value - azureASN = peer.AzureASN - peerASN = peer.PeerASN - primaryPeerAddressPrefix = - IPAddressCidr.format peer.PrimaryPeerAddressPrefix - secondaryPeerAddressPrefix = - IPAddressCidr.format peer.SecondaryPeerAddressPrefix - vlanId = peer.VlanId - sharedKey = peer.SharedKey - |} - |} - ] - serviceProviderProperties = + member this.JsonModel = {| + expressRouteCircuits.Create(this.Name, this.Location, tags = this.Tags) with + sku = {| + name = $"{this.Tier}_{this.Family}" + tier = string this.Tier + family = string this.Family + |} + properties = {| + peerings = [ + for peer in this.Peerings do {| - serviceProviderName = this.ServiceProviderName - peeringLocation = this.PeeringLocation - bandwidthInMbps = this.Bandwidth + name = peer.PeeringType.Value + properties = {| + peeringType = peer.PeeringType.Value + azureASN = peer.AzureASN + peerASN = peer.PeerASN + primaryPeerAddressPrefix = IPAddressCidr.format peer.PrimaryPeerAddressPrefix + secondaryPeerAddressPrefix = IPAddressCidr.format peer.SecondaryPeerAddressPrefix + vlanId = peer.VlanId + sharedKey = peer.SharedKey + |} |} - globalReachEnabled = this.GlobalReachEnabled + ] + serviceProviderProperties = {| + serviceProviderName = this.ServiceProviderName + peeringLocation = this.PeeringLocation + bandwidthInMbps = this.Bandwidth |} - |} + globalReachEnabled = this.GlobalReachEnabled + |} + |} -type ExpressRouteCircuitAuthorization = - { - Name: ResourceName - Circuit: LinkedResource - } +type ExpressRouteCircuitAuthorization = { + Name: ResourceName + Circuit: LinkedResource +} with interface IArmResource with member this.ResourceId = @@ -802,15 +755,14 @@ type ExpressRouteCircuitAuthorization = ) -type PrivateEndpoint = - { - Name: ResourceName - Location: Location - Subnet: SubnetReference - Resource: LinkedResource - CustomNetworkInterfaceName: string option - GroupIds: string list - } +type PrivateEndpoint = { + Name: ResourceName + Location: Location + Subnet: SubnetReference + Resource: LinkedResource + CustomNetworkInterfaceName: string option + GroupIds: string list +} with static member create location (resourceId: ResourceId) groupIds = Set.toSeq @@ -832,30 +784,27 @@ type PrivateEndpoint = member this.ResourceId = privateEndpoints.resourceId this.Name member this.JsonModel = - let dependencies = - [ - yield! this.Subnet.Dependency |> Option.toList - match this.Resource with - | Managed x -> x - | _ -> () - ] - - {| privateEndpoints.Create(this.Name, this.Location, dependencies) with - properties = - {| + let dependencies = [ + yield! this.Subnet.Dependency |> Option.toList + match this.Resource with + | Managed x -> x + | _ -> () + ] + + {| + privateEndpoints.Create(this.Name, this.Location, dependencies) with + properties = {| subnet = {| id = this.Subnet.ResourceId.Eval() |} customNetworkInterfaceName = this.CustomNetworkInterfaceName |> Option.toObj - privateLinkServiceConnections = - [ - {| - name = this.Name.Value - properties = - {| - privateLinkServiceId = this.Resource.ResourceId.Eval() - groupIds = this.GroupIds - |} + privateLinkServiceConnections = [ + {| + name = this.Name.Value + properties = {| + privateLinkServiceId = this.Resource.ResourceId.Eval() + groupIds = this.GroupIds |} - ] + |} + ] |} |} @@ -870,15 +819,14 @@ type PeerAccess = | ForwardOnly | AccessAndForward -type NetworkPeering = - { - Location: Location - OwningVNet: LinkedResource - RemoteVNet: LinkedResource - RemoteAccess: PeerAccess - GatewayTransit: GatewayTransit - DependsOn: ResourceId Set - } +type NetworkPeering = { + Location: Location + OwningVNet: LinkedResource + RemoteVNet: LinkedResource + RemoteAccess: PeerAccess + GatewayTransit: GatewayTransit + DependsOn: ResourceId Set +} with member this.Name = this.OwningVNet.Name / $"peering-%s{this.RemoteVNet.Name.Value}" @@ -886,20 +834,19 @@ type NetworkPeering = member this.ResourceId = virtualNetworkPeering.resourceId this.Name member this.JsonModel = - let deps = - [ - match this.OwningVNet with - | Managed id -> id - | _ -> () - match this.RemoteVNet with - | Managed id -> id - | _ -> () - yield! this.DependsOn - ] - - {| virtualNetworkPeering.Create(this.Name, this.Location, deps) with - properties = - {| + let deps = [ + match this.OwningVNet with + | Managed id -> id + | _ -> () + match this.RemoteVNet with + | Managed id -> id + | _ -> () + yield! this.DependsOn + ] + + {| + virtualNetworkPeering.Create(this.Name, this.Location, deps) with + properties = {| allowVirtualNetworkAccess = match this.RemoteAccess with | AccessOnly @@ -919,25 +866,23 @@ type NetworkPeering = match this.GatewayTransit with | UseRemoteGateway -> true | _ -> false - remoteVirtualNetwork = - {| - id = - match this.RemoteVNet with - | Managed id - | Unmanaged id -> id.ArmExpression.Eval() - |} + remoteVirtualNetwork = {| + id = + match this.RemoteVNet with + | Managed id + | Unmanaged id -> id.ArmExpression.Eval() + |} |} |} -type NatGateway = - { - Name: ResourceName - Location: Location - IdleTimeout: int - PublicIpAddresses: LinkedResource list - PublicIpPrefixes: LinkedResource list - Tags: Map - } +type NatGateway = { + Name: ResourceName + Location: Location + IdleTimeout: int + PublicIpAddresses: LinkedResource list + PublicIpPrefixes: LinkedResource list + Tags: Map +} with interface IArmResource with member this.ResourceId = natGateways.resourceId this.Name @@ -952,10 +897,10 @@ type NatGateway = } |> Set.ofSeq - {| natGateways.Create(this.Name, this.Location, dependsOn = dependencies, tags = this.Tags) with - sku = {| name = "Standard" |} - properties = - {| + {| + natGateways.Create(this.Name, this.Location, dependsOn = dependencies, tags = this.Tags) with + sku = {| name = "Standard" |} + properties = {| idleTimeoutInMinutes = this.IdleTimeout publicIpAddresses = this.PublicIpAddresses |> List.map LinkedResource.AsIdObject publicIpPrefixes = this.PublicIpPrefixes |> List.map LinkedResource.AsIdObject diff --git a/src/Farmer/Arm/NetworkSecurityGroup.fs b/src/Farmer/Arm/NetworkSecurityGroup.fs index bd451685f..03d593767 100644 --- a/src/Farmer/Arm/NetworkSecurityGroup.fs +++ b/src/Farmer/Arm/NetworkSecurityGroup.fs @@ -21,11 +21,9 @@ let (|SingleEndpoint|ManyEndpoints|) endpoints = | Tag _ -> true | _ -> false) |> function - | Some (Tag tag) -> SingleEndpoint(Tag tag) + | Some(Tag tag) -> SingleEndpoint(Tag tag) | None - | Some (AnyEndpoint - | Network _ - | Host _) -> ManyEndpoints(List.ofSeq endpoints) + | Some(AnyEndpoint | Network _ | Host _) -> ManyEndpoints(List.ofSeq endpoints) let private (|SinglePort|ManyPorts|) (ports: _ Set) = if ports.Contains AnyPort then @@ -54,37 +52,35 @@ module private EndpointWriter = | SinglePort _ -> [] | ManyPorts ports -> [ for port in ports -> port.ArmValue ] -type SecurityRule = - { - Name: ResourceName - Description: string option - SecurityGroup: ResourceName - Protocol: NetworkProtocol - SourcePorts: Port Set - DestinationPorts: Port Set - SourceAddresses: Endpoint list - DestinationAddresses: Endpoint list - Access: Operation - Direction: TrafficDirection - Priority: int - } - - member this.PropertiesModel = - {| - description = this.Description |> Option.toObj - protocol = this.Protocol.ArmValue - sourcePortRange = this.SourcePorts |> EndpointWriter.toRange - sourcePortRanges = this.SourcePorts |> EndpointWriter.toRanges - destinationPortRange = this.DestinationPorts |> EndpointWriter.toRange - destinationPortRanges = this.DestinationPorts |> EndpointWriter.toRanges - sourceAddressPrefix = this.SourceAddresses |> EndpointWriter.toPrefix - sourceAddressPrefixes = this.SourceAddresses |> EndpointWriter.toPrefixes - destinationAddressPrefix = this.DestinationAddresses |> EndpointWriter.toPrefix - destinationAddressPrefixes = this.DestinationAddresses |> EndpointWriter.toPrefixes - access = this.Access.ArmValue - priority = this.Priority - direction = this.Direction.ArmValue - |} +type SecurityRule = { + Name: ResourceName + Description: string option + SecurityGroup: ResourceName + Protocol: NetworkProtocol + SourcePorts: Port Set + DestinationPorts: Port Set + SourceAddresses: Endpoint list + DestinationAddresses: Endpoint list + Access: Operation + Direction: TrafficDirection + Priority: int +} with + + member this.PropertiesModel = {| + description = this.Description |> Option.toObj + protocol = this.Protocol.ArmValue + sourcePortRange = this.SourcePorts |> EndpointWriter.toRange + sourcePortRanges = this.SourcePorts |> EndpointWriter.toRanges + destinationPortRange = this.DestinationPorts |> EndpointWriter.toRange + destinationPortRanges = this.DestinationPorts |> EndpointWriter.toRanges + sourceAddressPrefix = this.SourceAddresses |> EndpointWriter.toPrefix + sourceAddressPrefixes = this.SourceAddresses |> EndpointWriter.toPrefixes + destinationAddressPrefix = this.DestinationAddresses |> EndpointWriter.toPrefix + destinationAddressPrefixes = this.DestinationAddresses |> EndpointWriter.toPrefixes + access = this.Access.ArmValue + priority = this.Priority + direction = this.Direction.ArmValue + |} interface IArmResource with member this.ResourceId = securityRules.resourceId (this.SecurityGroup / this.Name) @@ -92,32 +88,30 @@ type SecurityRule = member this.JsonModel = let dependsOn = [ networkSecurityGroups.resourceId this.SecurityGroup ] - {| securityRules.Create(this.SecurityGroup / this.Name, dependsOn = dependsOn) with - properties = this.PropertiesModel + {| + securityRules.Create(this.SecurityGroup / this.Name, dependsOn = dependsOn) with + properties = this.PropertiesModel |} -type NetworkSecurityGroup = - { - Name: ResourceName - Location: Location - SecurityRules: SecurityRule list - Tags: Map - } +type NetworkSecurityGroup = { + Name: ResourceName + Location: Location + SecurityRules: SecurityRule list + Tags: Map +} with interface IArmResource with member this.ResourceId = networkSecurityGroups.resourceId this.Name - member this.JsonModel = - {| networkSecurityGroups.Create(this.Name, this.Location, tags = this.Tags) with - properties = - {| - securityRules = - this.SecurityRules - |> List.map (fun rule -> - {| - name = rule.Name.Value - ``type`` = securityRules.Type - properties = rule.PropertiesModel - |}) - |} - |} + member this.JsonModel = {| + networkSecurityGroups.Create(this.Name, this.Location, tags = this.Tags) with + properties = {| + securityRules = + this.SecurityRules + |> List.map (fun rule -> {| + name = rule.Name.Value + ``type`` = securityRules.Type + properties = rule.PropertiesModel + |}) + |} + |} diff --git a/src/Farmer/Arm/OperationsManagement.fs b/src/Farmer/Arm/OperationsManagement.fs index 5eb423ffe..bb330c59a 100644 --- a/src/Farmer/Arm/OperationsManagement.fs +++ b/src/Farmer/Arm/OperationsManagement.fs @@ -6,35 +6,36 @@ open Farmer let oms = ResourceType("Microsoft.OperationsManagement/solutions", "2015-11-01-preview") -type OMS = - { - Name: ResourceName - Location: Location - Plan: {| Name: string - Product: string - Publisher: string |} - Properties: {| ContainedResources: ResourceId list - ReferencedResources: ResourceId list - WorkspaceResourceId: ResourceId |} - Tags: Map - } +type OMS = { + Name: ResourceName + Location: Location + Plan: {| + Name: string + Product: string + Publisher: string + |} + Properties: {| + ContainedResources: ResourceId list + ReferencedResources: ResourceId list + WorkspaceResourceId: ResourceId + |} + Tags: Map +} with interface IArmResource with member this.ResourceId = oms.resourceId this.Name - member this.JsonModel = - {| oms.Create(this.Name, this.Location, [ this.Properties.WorkspaceResourceId ], tags = this.Tags) with - plan = - {| - name = this.Plan.Name - publisher = this.Plan.Publisher - product = this.Plan.Product - promotionCode = "" - |} - properties = - {| - workspaceResourceId = this.Properties.WorkspaceResourceId.Eval() - containedResources = this.Properties.ContainedResources |> List.map (fun cr -> cr.Eval()) - referencedResources = this.Properties.ReferencedResources |> List.map (fun rr -> rr.Eval()) - |} - |} + member this.JsonModel = {| + oms.Create(this.Name, this.Location, [ this.Properties.WorkspaceResourceId ], tags = this.Tags) with + plan = {| + name = this.Plan.Name + publisher = this.Plan.Publisher + product = this.Plan.Product + promotionCode = "" + |} + properties = {| + workspaceResourceId = this.Properties.WorkspaceResourceId.Eval() + containedResources = this.Properties.ContainedResources |> List.map (fun cr -> cr.Eval()) + referencedResources = this.Properties.ReferencedResources |> List.map (fun rr -> rr.Eval()) + |} + |} diff --git a/src/Farmer/Arm/PrivateLinkService.fs b/src/Farmer/Arm/PrivateLinkService.fs index ff737295d..36147a9a3 100644 --- a/src/Farmer/Arm/PrivateLinkService.fs +++ b/src/Farmer/Arm/PrivateLinkService.fs @@ -7,70 +7,68 @@ open Farmer let privateLinkServices = ResourceType("Microsoft.Network/privateLinkServices", "2021-08-01") -type PrivateLinkService = - { - Name: ResourceName - Location: Location - Dependencies: ResourceId Set - AutoApprovedSubscriptions: Guid list - EnableProxyProtocol: bool - LoadBalancerFrontendIpConfigIds: ResourceId list - IpConfigs: {| Name: ResourceName - PrivateIpAllocationMethod: AllocationMethod - PrivateIpAddressVersion: AddressFamily - Primary: bool - SubnetId: ResourceId |} list - VisibleToSubscriptions: Guid list - Tags: Map - } +type PrivateLinkService = { + Name: ResourceName + Location: Location + Dependencies: ResourceId Set + AutoApprovedSubscriptions: Guid list + EnableProxyProtocol: bool + LoadBalancerFrontendIpConfigIds: ResourceId list + IpConfigs: + {| + Name: ResourceName + PrivateIpAllocationMethod: AllocationMethod + PrivateIpAddressVersion: AddressFamily + Primary: bool + SubnetId: ResourceId + |} list + VisibleToSubscriptions: Guid list + Tags: Map +} with interface IArmResource with member this.ResourceId = privateLinkServices.resourceId this.Name - member this.JsonModel = - {| privateLinkServices.Create(this.Name, this.Location, this.Dependencies, this.Tags) with - properties = - {| - autoApproval = - match this.AutoApprovedSubscriptions with - | [] -> Unchecked.defaultof<_> - | approvedSubscriptions -> - {| - subscriptions = approvedSubscriptions - |} - enableProxyProtocol = this.EnableProxyProtocol - loadBalancerFrontendIpConfigurations = - this.LoadBalancerFrontendIpConfigIds - |> List.map (fun frontend -> {| id = frontend.Eval() |}) - ipConfigurations = - this.IpConfigs - |> List.map (fun ipconfig -> - {| - name = ipconfig.Name.Value - properties = - {| - primary = ipconfig.Primary - privateIPAllocationMethod = - match ipconfig.PrivateIpAllocationMethod with - | DynamicPrivateIp -> "Dynamic" - | StaticPrivateIp _ -> "Static" - privateIPAddress = - match ipconfig.PrivateIpAllocationMethod with - | DynamicPrivateIp -> null - | StaticPrivateIp ip -> string ip - privateIPAddressVersion = - match ipconfig.PrivateIpAddressVersion with - | AddressFamily.InterNetwork -> "IPv4" - | AddressFamily.InterNetworkV6 -> "IPv6" - | _ -> - raiseFarmer - "Unsupported PrivateIpAddressVersion - should be InterNetwork (IPv4) or InterNetworkV6 (IPv6)." - subnet = {| id = ipconfig.SubnetId.Eval() |} - |} - |}) - visibility = - match this.AutoApprovedSubscriptions @ this.VisibleToSubscriptions |> List.distinct with - | [] -> Unchecked.defaultof<_> - | visibleTo -> {| subscriptions = visibleTo |} - |} - |} + member this.JsonModel = {| + privateLinkServices.Create(this.Name, this.Location, this.Dependencies, this.Tags) with + properties = {| + autoApproval = + match this.AutoApprovedSubscriptions with + | [] -> Unchecked.defaultof<_> + | approvedSubscriptions -> {| + subscriptions = approvedSubscriptions + |} + enableProxyProtocol = this.EnableProxyProtocol + loadBalancerFrontendIpConfigurations = + this.LoadBalancerFrontendIpConfigIds + |> List.map (fun frontend -> {| id = frontend.Eval() |}) + ipConfigurations = + this.IpConfigs + |> List.map (fun ipconfig -> {| + name = ipconfig.Name.Value + properties = {| + primary = ipconfig.Primary + privateIPAllocationMethod = + match ipconfig.PrivateIpAllocationMethod with + | DynamicPrivateIp -> "Dynamic" + | StaticPrivateIp _ -> "Static" + privateIPAddress = + match ipconfig.PrivateIpAllocationMethod with + | DynamicPrivateIp -> null + | StaticPrivateIp ip -> string ip + privateIPAddressVersion = + match ipconfig.PrivateIpAddressVersion with + | AddressFamily.InterNetwork -> "IPv4" + | AddressFamily.InterNetworkV6 -> "IPv6" + | _ -> + raiseFarmer + "Unsupported PrivateIpAddressVersion - should be InterNetwork (IPv4) or InterNetworkV6 (IPv6)." + subnet = {| id = ipconfig.SubnetId.Eval() |} + |} + |}) + visibility = + match this.AutoApprovedSubscriptions @ this.VisibleToSubscriptions |> List.distinct with + | [] -> Unchecked.defaultof<_> + | visibleTo -> {| subscriptions = visibleTo |} + |} + |} diff --git a/src/Farmer/Arm/ResourceGroup.fs b/src/Farmer/Arm/ResourceGroup.fs index 37f1f740a..0ef66511a 100644 --- a/src/Farmer/Arm/ResourceGroup.fs +++ b/src/Farmer/Arm/ResourceGroup.fs @@ -22,41 +22,38 @@ type ParameterValue = /// Gets the key for this parameter in the nested deployment's 'parameters' dictionary. member this.Key = match this with - | ParameterValue (name, _) -> name - | KeyVaultReference (name, _, _) -> name + | ParameterValue(name, _) -> name + | KeyVaultReference(name, _, _) -> name /// A parameter key value pair contains parameters objects of arbitrary structure to be /// serialized as JSON for template parameters. member internal this.ParamValue: IDictionary = match this with - | ParameterValue (_, value) -> dict [ "value", box value ] - | KeyVaultReference (_, kvResId, secretName) -> - dict - [ - "reference", - dict - [ - "keyVault", dict [ "id", kvResId.Eval() ] |> box - "secretName", secretName |> box - ] - |> box + | ParameterValue(_, value) -> dict [ "value", box value ] + | KeyVaultReference(_, kvResId, secretName) -> + dict [ + "reference", + dict [ + "keyVault", dict [ "id", kvResId.Eval() ] |> box + "secretName", secretName |> box ] + |> box + ] /// Represents all configuration information to generate an ARM template. -type ResourceGroupDeployment = - { - TargetResourceGroup: ResourceName - DeploymentName: ResourceName - Dependencies: ResourceId Set - Outputs: Map - Location: Location - Resources: IArmResource list - /// Parameters provided to the deployment - ParameterValues: ParameterValue list - SubscriptionId: System.Guid option - Mode: DeploymentMode - Tags: Map - } +type ResourceGroupDeployment = { + TargetResourceGroup: ResourceName + DeploymentName: ResourceName + Dependencies: ResourceId Set + Outputs: Map + Location: Location + Resources: IArmResource list + /// Parameters provided to the deployment + ParameterValues: ParameterValue list + SubscriptionId: System.Guid option + Mode: DeploymentMode + Tags: Map +} with member this.ResourceId = resourceGroupDeployment.resourceId this.DeploymentName @@ -79,12 +76,11 @@ type ResourceGroupDeployment = List.distinct (this.TargetResourceGroup.Value :: nestedRgs) - member this.Template = - { - Parameters = this.Parameters - Outputs = this.Outputs |> Map.toList - Resources = this.Resources - } + member this.Template = { + Parameters = this.Parameters + Outputs = this.Outputs |> Map.toList + Resources = this.Resources + } /// Parameters to be emitted by the outer deployment to be passed to this deployment interface IParameters with /// Secure parameters that are not provided as input on this deployment @@ -97,53 +93,48 @@ type ResourceGroupDeployment = interface IArmResource with member this.ResourceId = this.ResourceId - member this.JsonModel = - {| resourceGroupDeployment.Create( - this.DeploymentName, - this.Location, - dependsOn = this.Dependencies, - tags = this.Tags - ) with + member this.JsonModel = {| + resourceGroupDeployment.Create( + this.DeploymentName, + this.Location, + dependsOn = this.Dependencies, + tags = this.Tags + ) with location = null // location is not supported for nested resource groups resourceGroup = this.TargetResourceGroup.Value subscriptionId = this.SubscriptionId |> Option.map string |> Option.toObj - properties = - {| - template = TemplateGeneration.processTemplate this.Template - parameters = - let parameters = Dictionary() - - for secureParam in this.Parameters do - parameters.Add( - secureParam.Key, - dict [ "value", $"[parameters('%s{secureParam.Value}')]" ] - ) - // Let input params be used to satisfy parameters emitted for resources in the template - for inputParam in this.ParameterValues do - parameters.[inputParam.Key] <- inputParam.ParamValue - - parameters - mode = - match this.Mode with - | Incremental -> "Incremental" - | Complete -> "Complete" - expressionEvaluationOptions = {| scope = "Inner" |} - |} - |} + properties = {| + template = TemplateGeneration.processTemplate this.Template + parameters = + let parameters = Dictionary() + + for secureParam in this.Parameters do + parameters.Add(secureParam.Key, dict [ "value", $"[parameters('%s{secureParam.Value}')]" ]) + // Let input params be used to satisfy parameters emitted for resources in the template + for inputParam in this.ParameterValues do + parameters.[inputParam.Key] <- inputParam.ParamValue + + parameters + mode = + match this.Mode with + | Incremental -> "Incremental" + | Complete -> "Complete" + expressionEvaluationOptions = {| scope = "Inner" |} + |} + |} /// Resource Group as a subscription level resource - only for use in deployments targeting a subscription. -type ResourceGroup = - { - Name: ResourceName - Dependencies: ResourceId Set - Location: Location - Tags: Map - } +type ResourceGroup = { + Name: ResourceName + Dependencies: ResourceId Set + Location: Location + Tags: Map +} with interface IArmResource with - member this.JsonModel = - {| resourceGroups.Create(this.Name, this.Location, this.Dependencies, this.Tags) with - properties = {| |} - |} + member this.JsonModel = {| + resourceGroups.Create(this.Name, this.Location, this.Dependencies, this.Tags) with + properties = {| |} + |} member this.ResourceId = resourceGroups.resourceId this.Name diff --git a/src/Farmer/Arm/RoleAssignment.fs b/src/Farmer/Arm/RoleAssignment.fs index 4e9ad0dae..459e72666 100644 --- a/src/Farmer/Arm/RoleAssignment.fs +++ b/src/Farmer/Arm/RoleAssignment.fs @@ -38,21 +38,20 @@ type AssignmentScope = | SpecificResource of ResourceId | UnmanagedResource of ResourceId -type RoleAssignment = - { - /// It's recommended to use a deterministic GUID for the role name. - Name: ResourceName - /// The role to assign, such as Roles.Contributor - RoleDefinitionId: RoleId - /// The principal ID of the user or service identity that should be granted this role. - PrincipalId: PrincipalId - /// The type of principal being assigned - should be set to ServicePrincipal for managed identities to avoid - /// the role assignment being created before Active Directory can replicate the principal. - PrincipalType: PrincipalType - /// Resource this role applies to. If this is set to a specific resource, it will automatically be set as a dependency for you. - Scope: AssignmentScope - Dependencies: ResourceId Set - } +type RoleAssignment = { + /// It's recommended to use a deterministic GUID for the role name. + Name: ResourceName + /// The role to assign, such as Roles.Contributor + RoleDefinitionId: RoleId + /// The principal ID of the user or service identity that should be granted this role. + PrincipalId: PrincipalId + /// The type of principal being assigned - should be set to ServicePrincipal for managed identities to avoid + /// the role assignment being created before Active Directory can replicate the principal. + PrincipalType: PrincipalType + /// Resource this role applies to. If this is set to a specific resource, it will automatically be set as a dependency for you. + Scope: AssignmentScope + Dependencies: ResourceId Set +} with interface IArmResource with member this.ResourceId = roleAssignments.resourceId this.Name @@ -60,22 +59,21 @@ type RoleAssignment = member this.JsonModel = let dependencies = this.Dependencies - + Set - [ - match this.Scope with - | SpecificResource resourceId -> resourceId - | UnmanagedResource _ - | ResourceGroup -> () - ] + + Set [ + match this.Scope with + | SpecificResource resourceId -> resourceId + | UnmanagedResource _ + | ResourceGroup -> () + ] - {| roleAssignments.Create(this.Name, dependsOn = dependencies) with - scope = - match this with - | { Scope = ResourceGroup } -> null - | { Scope = UnmanagedResource resourceId } -> resourceId.ArmExpression.Eval() - | { Scope = SpecificResource resourceId } -> resourceId.Eval() - properties = - {| + {| + roleAssignments.Create(this.Name, dependsOn = dependencies) with + scope = + match this with + | { Scope = ResourceGroup } -> null + | { Scope = UnmanagedResource resourceId } -> resourceId.ArmExpression.Eval() + | { Scope = SpecificResource resourceId } -> resourceId.Eval() + properties = {| roleDefinitionId = this.RoleDefinitionId.ArmValue.Eval() principalId = this.PrincipalId.ArmExpression.Eval() principalType = this.PrincipalType.ArmValue diff --git a/src/Farmer/Arm/Search.fs b/src/Farmer/Arm/Search.fs index b1688b1b5..630ccc198 100644 --- a/src/Farmer/Arm/Search.fs +++ b/src/Farmer/Arm/Search.fs @@ -6,15 +6,14 @@ open Farmer.Search let searchServices = ResourceType("Microsoft.Search/searchServices", "2015-08-19") -type SearchService = - { - Name: ResourceName - Location: Location - Sku: Sku - ReplicaCount: int - PartitionCount: int - Tags: Map - } +type SearchService = { + Name: ResourceName + Location: Location + Sku: Sku + ReplicaCount: int + PartitionCount: int + Tags: Map +} with member this.HostingMode = match this.Sku with @@ -24,24 +23,22 @@ type SearchService = interface IArmResource with member this.ResourceId = searchServices.resourceId this.Name - member this.JsonModel = - {| searchServices.Create(this.Name, this.Location, tags = this.Tags) with - sku = - {| - name = - match this.Sku with - | Free -> "free" - | Basic -> "basic" - | Standard -> "standard" - | Standard2 -> "standard2" - | Standard3 _ -> "standard3" - | StorageOptimisedL1 -> "storage_optimized_l1" - | StorageOptimisedL2 -> "storage_optimized_l2" - |} - properties = - {| - replicaCount = this.ReplicaCount - partitionCount = this.PartitionCount - hostingMode = this.HostingMode - |} - |} + member this.JsonModel = {| + searchServices.Create(this.Name, this.Location, tags = this.Tags) with + sku = {| + name = + match this.Sku with + | Free -> "free" + | Basic -> "basic" + | Standard -> "standard" + | Standard2 -> "standard2" + | Standard3 _ -> "standard3" + | StorageOptimisedL1 -> "storage_optimized_l1" + | StorageOptimisedL2 -> "storage_optimized_l2" + |} + properties = {| + replicaCount = this.ReplicaCount + partitionCount = this.PartitionCount + hostingMode = this.HostingMode + |} + |} diff --git a/src/Farmer/Arm/ServiceBus.fs b/src/Farmer/Arm/ServiceBus.fs index 3b4dfcb94..005417e56 100644 --- a/src/Farmer/Arm/ServiceBus.fs +++ b/src/Farmer/Arm/ServiceBus.fs @@ -30,199 +30,184 @@ module Namespaces = module Topics = let rules = ResourceType("Rules", "2022-01-01-preview") - type Subscription = - { - Name: ResourceName - Topic: LinkedResource - LockDuration: IsoDateTime option - DuplicateDetectionHistoryTimeWindow: IsoDateTime option - DefaultMessageTimeToLive: IsoDateTime option - ForwardTo: ResourceName option - MaxDeliveryCount: int option - Session: bool option - DeadLetteringOnMessageExpiration: bool option - Rules: Rule list - DependsOn: Set - } - - member private this.ResourceName = - this.Topic.Name / this.Topic.ResourceId.Segments.[0] / this.Name - - interface IArmResource with - member this.ResourceId = subscriptions.resourceId (this.ResourceName) - - member this.JsonModel = - {| subscriptions.Create( - this.ResourceName, - dependsOn = LinkedResource.addToSetIfManaged this.Topic this.DependsOn - ) with - properties = - {| - defaultMessageTimeToLive = tryGetIso this.DefaultMessageTimeToLive - requiresDuplicateDetection = - match this.DuplicateDetectionHistoryTimeWindow with - | Some _ -> Nullable true - | None -> Nullable() - duplicateDetectionHistoryTimeWindow = tryGetIso this.DuplicateDetectionHistoryTimeWindow - deadLetteringOnMessageExpiration = - this.DeadLetteringOnMessageExpiration |> Option.toNullable - forwardTo = this.ForwardTo |> Option.map (fun n -> n.Value) |> Option.toObj - maxDeliveryCount = this.MaxDeliveryCount |> Option.toNullable - requiresSession = this.Session |> Option.toNullable - lockDuration = tryGetIso this.LockDuration - |} - resources = - [ - for rule in this.Rules do - {| rules.Create( - rule.Name, - dependsOn = [ ResourceId.create (ResourceType("", ""), this.Name) ] - ) with - properties = - match rule with - | SqlFilter (_, expression) -> - {| - filterType = "SqlFilter" - sqlFilter = box {| sqlExpression = expression |} - correlationFilter = null - |} - | CorrelationFilter (_, correlationId, properties) -> - {| - filterType = "CorrelationFilter" - correlationFilter = - box - {| - correlationId = correlationId |> Option.toObj - properties = properties - |} - sqlFilter = null - |} - |} - ] - |} - - type Queue = - { + type Subscription = { Name: ResourceName - Namespace: LinkedResource + Topic: LinkedResource LockDuration: IsoDateTime option DuplicateDetectionHistoryTimeWindow: IsoDateTime option - Session: bool option - DeadLetteringOnMessageExpiration: bool option DefaultMessageTimeToLive: IsoDateTime option ForwardTo: ResourceName option MaxDeliveryCount: int option - MaxSizeInMegabytes: int option - EnablePartitioning: bool option - } + Session: bool option + DeadLetteringOnMessageExpiration: bool option + Rules: Rule list + DependsOn: Set + } with - member private this.ResourceName = this.Namespace.Name / this.Name + member private this.ResourceName = + this.Topic.Name / this.Topic.ResourceId.Segments.[0] / this.Name - interface IArmResource with - member this.ResourceId = queues.resourceId (this.ResourceName) + interface IArmResource with + member this.ResourceId = subscriptions.resourceId (this.ResourceName) - member this.JsonModel = - {| queues.Create( - this.ResourceName, - dependsOn = (LinkedResource.addToSetIfManaged this.Namespace Set.empty) - ) with - properties = - {| - lockDuration = tryGetIso this.LockDuration + member this.JsonModel = {| + subscriptions.Create( + this.ResourceName, + dependsOn = LinkedResource.addToSetIfManaged this.Topic this.DependsOn + ) with + properties = {| + defaultMessageTimeToLive = tryGetIso this.DefaultMessageTimeToLive requiresDuplicateDetection = match this.DuplicateDetectionHistoryTimeWindow with | Some _ -> Nullable true | None -> Nullable() duplicateDetectionHistoryTimeWindow = tryGetIso this.DuplicateDetectionHistoryTimeWindow - defaultMessageTimeToLive = tryGetIso this.DefaultMessageTimeToLive - requiresSession = this.Session |> Option.toNullable deadLetteringOnMessageExpiration = this.DeadLetteringOnMessageExpiration |> Option.toNullable - forwardTo = this.ForwardTo |> Option.map (fun x -> x.Value) |> Option.toObj + forwardTo = this.ForwardTo |> Option.map (fun n -> n.Value) |> Option.toObj maxDeliveryCount = this.MaxDeliveryCount |> Option.toNullable - maxSizeInMegabytes = this.MaxSizeInMegabytes |> Option.toNullable - enablePartitioning = this.EnablePartitioning |> Option.toNullable + requiresSession = this.Session |> Option.toNullable + lockDuration = tryGetIso this.LockDuration |} + resources = [ + for rule in this.Rules do + {| + rules.Create( + rule.Name, + dependsOn = [ ResourceId.create (ResourceType("", ""), this.Name) ] + ) with + properties = + match rule with + | SqlFilter(_, expression) -> {| + filterType = "SqlFilter" + sqlFilter = box {| sqlExpression = expression |} + correlationFilter = null + |} + | CorrelationFilter(_, correlationId, properties) -> {| + filterType = "CorrelationFilter" + correlationFilter = + box {| + correlationId = correlationId |> Option.toObj + properties = properties + |} + sqlFilter = null + |} + |} + ] |} - type QueueAuthorizationRule = - { - Name: ResourceName - Location: Location - Dependencies: ResourceId list - Rights: AuthorizationRuleRight Set - } + type Queue = { + Name: ResourceName + Namespace: LinkedResource + LockDuration: IsoDateTime option + DuplicateDetectionHistoryTimeWindow: IsoDateTime option + Session: bool option + DeadLetteringOnMessageExpiration: bool option + DefaultMessageTimeToLive: IsoDateTime option + ForwardTo: ResourceName option + MaxDeliveryCount: int option + MaxSizeInMegabytes: int option + EnablePartitioning: bool option + } with + + member private this.ResourceName = this.Namespace.Name / this.Name + + interface IArmResource with + member this.ResourceId = queues.resourceId (this.ResourceName) + + member this.JsonModel = {| + queues.Create( + this.ResourceName, + dependsOn = (LinkedResource.addToSetIfManaged this.Namespace Set.empty) + ) with + properties = {| + lockDuration = tryGetIso this.LockDuration + requiresDuplicateDetection = + match this.DuplicateDetectionHistoryTimeWindow with + | Some _ -> Nullable true + | None -> Nullable() + duplicateDetectionHistoryTimeWindow = tryGetIso this.DuplicateDetectionHistoryTimeWindow + defaultMessageTimeToLive = tryGetIso this.DefaultMessageTimeToLive + requiresSession = this.Session |> Option.toNullable + deadLetteringOnMessageExpiration = this.DeadLetteringOnMessageExpiration |> Option.toNullable + forwardTo = this.ForwardTo |> Option.map (fun x -> x.Value) |> Option.toObj + maxDeliveryCount = this.MaxDeliveryCount |> Option.toNullable + maxSizeInMegabytes = this.MaxSizeInMegabytes |> Option.toNullable + enablePartitioning = this.EnablePartitioning |> Option.toNullable + |} + |} + + type QueueAuthorizationRule = { + Name: ResourceName + Location: Location + Dependencies: ResourceId list + Rights: AuthorizationRuleRight Set + } with interface IArmResource with member this.ResourceId = queueAuthorizationRules.resourceId this.Name - member this.JsonModel = - {| queueAuthorizationRules.Create(this.Name, this.Location, this.Dependencies) with - properties = - {| - rights = this.Rights |> Set.map string |> Set.toList - |} - |} + member this.JsonModel = {| + queueAuthorizationRules.Create(this.Name, this.Location, this.Dependencies) with + properties = {| + rights = this.Rights |> Set.map string |> Set.toList + |} + |} - type NamespaceAuthorizationRule = - { - Name: ResourceName - Location: Location - Dependencies: ResourceId list - Rights: AuthorizationRuleRight Set - } + type NamespaceAuthorizationRule = { + Name: ResourceName + Location: Location + Dependencies: ResourceId list + Rights: AuthorizationRuleRight Set + } with interface IArmResource with member this.ResourceId = namespaceAuthorizationRules.resourceId this.Name - member this.JsonModel = - {| namespaceAuthorizationRules.Create(this.Name, this.Location, this.Dependencies) with - properties = - {| - rights = this.Rights |> Set.map string |> Set.toList - |} - |} + member this.JsonModel = {| + namespaceAuthorizationRules.Create(this.Name, this.Location, this.Dependencies) with + properties = {| + rights = this.Rights |> Set.map string |> Set.toList + |} + |} - type Topic = - { - Name: ResourceName - Dependencies: ResourceId Set - Namespace: ResourceId - DuplicateDetectionHistoryTimeWindow: IsoDateTime option - DefaultMessageTimeToLive: IsoDateTime option - EnablePartitioning: bool option - MaxSizeInMegabytes: int option - } + type Topic = { + Name: ResourceName + Dependencies: ResourceId Set + Namespace: ResourceId + DuplicateDetectionHistoryTimeWindow: IsoDateTime option + DefaultMessageTimeToLive: IsoDateTime option + EnablePartitioning: bool option + MaxSizeInMegabytes: int option + } with interface IArmResource with member this.ResourceId = topics.resourceId (this.Namespace.Name, this.Name) - member this.JsonModel = - {| topics.Create(this.Namespace.Name / this.Name, dependsOn = this.Dependencies) with - properties = - {| - defaultMessageTimeToLive = tryGetIso this.DefaultMessageTimeToLive - requiresDuplicateDetection = - match this.DuplicateDetectionHistoryTimeWindow with - | Some _ -> Nullable true - | None -> Nullable() - duplicateDetectionHistoryTimeWindow = tryGetIso this.DuplicateDetectionHistoryTimeWindow - enablePartitioning = this.EnablePartitioning |> Option.toNullable - maxSizeInMegabytes = this.MaxSizeInMegabytes |> Option.toNullable - |} - |} + member this.JsonModel = {| + topics.Create(this.Namespace.Name / this.Name, dependsOn = this.Dependencies) with + properties = {| + defaultMessageTimeToLive = tryGetIso this.DefaultMessageTimeToLive + requiresDuplicateDetection = + match this.DuplicateDetectionHistoryTimeWindow with + | Some _ -> Nullable true + | None -> Nullable() + duplicateDetectionHistoryTimeWindow = tryGetIso this.DuplicateDetectionHistoryTimeWindow + enablePartitioning = this.EnablePartitioning |> Option.toNullable + maxSizeInMegabytes = this.MaxSizeInMegabytes |> Option.toNullable + |} + |} -type Namespace = - { - Name: ResourceName - Location: Location - Sku: Sku - Dependencies: ResourceId Set - ZoneRedundant: FeatureFlag option - DisablePublicNetworkAccess: FeatureFlag option - MinTlsVersion: TlsVersion option - Tags: Map - } +type Namespace = { + Name: ResourceName + Location: Location + Sku: Sku + Dependencies: ResourceId Set + ZoneRedundant: FeatureFlag option + DisablePublicNetworkAccess: FeatureFlag option + MinTlsVersion: TlsVersion option + Tags: Map +} with member this.Capacity = match this.Sku with @@ -235,31 +220,29 @@ type Namespace = interface IArmResource with member this.ResourceId = namespaces.resourceId this.Name - member this.JsonModel = - {| namespaces.Create(this.Name, this.Location, this.Dependencies, this.Tags) with - sku = - {| - name = this.Sku.NameArmValue - tier = this.Sku.TierArmValue - capacity = this.Capacity |> Option.toNullable - |} - properties = - {| - minimumTlsVersion = - match this.MinTlsVersion with - | Some Tls10 -> "1.0" - | Some Tls11 -> "1.1" - | Some Tls12 -> "1.2" - | None -> null - publicNetworkAccess = - match this.DisablePublicNetworkAccess with - | Some FeatureFlag.Enabled -> "Disabled" - | Some FeatureFlag.Disabled -> "Enabled" - | None -> null - zoneRedundant = - match this.ZoneRedundant with - | Some FeatureFlag.Enabled -> "true" - | Some FeatureFlag.Disabled -> "false" - | None -> null - |} - |} + member this.JsonModel = {| + namespaces.Create(this.Name, this.Location, this.Dependencies, this.Tags) with + sku = {| + name = this.Sku.NameArmValue + tier = this.Sku.TierArmValue + capacity = this.Capacity |> Option.toNullable + |} + properties = {| + minimumTlsVersion = + match this.MinTlsVersion with + | Some Tls10 -> "1.0" + | Some Tls11 -> "1.1" + | Some Tls12 -> "1.2" + | None -> null + publicNetworkAccess = + match this.DisablePublicNetworkAccess with + | Some FeatureFlag.Enabled -> "Disabled" + | Some FeatureFlag.Disabled -> "Enabled" + | None -> null + zoneRedundant = + match this.ZoneRedundant with + | Some FeatureFlag.Enabled -> "true" + | Some FeatureFlag.Disabled -> "false" + | None -> null + |} + |} diff --git a/src/Farmer/Arm/SignalR.fs b/src/Farmer/Arm/SignalR.fs index b58b54c58..75b3de955 100644 --- a/src/Farmer/Arm/SignalR.fs +++ b/src/Farmer/Arm/SignalR.fs @@ -6,45 +6,41 @@ open Farmer.SignalR let signalR = ResourceType("Microsoft.SignalRService/signalR", "2018-10-01") -type SignalR = - { - Name: ResourceName - Location: Location - Sku: Sku - Capacity: int option - AllowedOrigins: string list - ServiceMode: ServiceMode - Tags: Map - } +type SignalR = { + Name: ResourceName + Location: Location + Sku: Sku + Capacity: int option + AllowedOrigins: string list + ServiceMode: ServiceMode + Tags: Map +} with interface IArmResource with member this.ResourceId = signalR.resourceId this.Name - member this.JsonModel = - {| signalR.Create(this.Name, this.Location, tags = this.Tags) with - sku = - {| - name = - match this.Sku with - | Free -> "Free_F1" - | Standard -> "Standard_S1" - capacity = - match this.Capacity with - | Some c -> c.ToString() - | None -> null - |} - properties = - {| - cors = - match this.AllowedOrigins with - | [] -> null - | aos -> box {| allowedOrigins = aos |} - features = - [ - {| - flag = "ServiceMode" - value = this.ServiceMode.ToString() - |} - ] - |} - |} + member this.JsonModel = {| + signalR.Create(this.Name, this.Location, tags = this.Tags) with + sku = {| + name = + match this.Sku with + | Free -> "Free_F1" + | Standard -> "Standard_S1" + capacity = + match this.Capacity with + | Some c -> c.ToString() + | None -> null + |} + properties = {| + cors = + match this.AllowedOrigins with + | [] -> null + | aos -> box {| allowedOrigins = aos |} + features = [ + {| + flag = "ServiceMode" + value = this.ServiceMode.ToString() + |} + ] + |} + |} diff --git a/src/Farmer/Arm/Sql.fs b/src/Farmer/Arm/Sql.fs index c061f2d3f..0f2a043a2 100644 --- a/src/Farmer/Arm/Sql.fs +++ b/src/Farmer/Arm/Sql.fs @@ -23,15 +23,16 @@ type DbKind = | Standalone of DbPurchaseModel | Pool of ResourceName -type Server = - { - ServerName: SqlAccountName - Location: Location - Credentials: {| Username: string - Password: SecureParameter |} - MinTlsVersion: TlsVersion option - Tags: Map - } +type Server = { + ServerName: SqlAccountName + Location: Location + Credentials: {| + Username: string + Password: SecureParameter + |} + MinTlsVersion: TlsVersion option + Tags: Map +} with interface IParameters with member this.SecureParameters = [ this.Credentials.Password ] @@ -39,148 +40,137 @@ type Server = interface IArmResource with member this.ResourceId = servers.resourceId this.ServerName.ResourceName - member this.JsonModel = - {| servers.Create( - this.ServerName.ResourceName, - this.Location, - tags = (this.Tags |> Map.add "displayName" this.ServerName.ResourceName.Value) - ) with - properties = - {| - administratorLogin = this.Credentials.Username - administratorLoginPassword = this.Credentials.Password.ArmExpression.Eval() - version = "12.0" - minimalTlsVersion = - match this.MinTlsVersion with - | Some Tls10 -> "1.0" - | Some Tls11 -> "1.1" - | Some Tls12 -> "1.2" - | None -> null - |} - |} + member this.JsonModel = {| + servers.Create( + this.ServerName.ResourceName, + this.Location, + tags = (this.Tags |> Map.add "displayName" this.ServerName.ResourceName.Value) + ) with + properties = {| + administratorLogin = this.Credentials.Username + administratorLoginPassword = this.Credentials.Password.ArmExpression.Eval() + version = "12.0" + minimalTlsVersion = + match this.MinTlsVersion with + | Some Tls10 -> "1.0" + | Some Tls11 -> "1.1" + | Some Tls12 -> "1.2" + | None -> null + |} + |} module Servers = - type ElasticPool = - { - Name: ResourceName - Server: SqlAccountName - Location: Location - Sku: PoolSku - MinMax: (int * int) option - MaxSizeBytes: int64 option - } + type ElasticPool = { + Name: ResourceName + Server: SqlAccountName + Location: Location + Sku: PoolSku + MinMax: (int * int) option + MaxSizeBytes: int64 option + } with interface IArmResource with member this.ResourceId = elasticPools.resourceId (this.Server.ResourceName / this.Name) - member this.JsonModel = - {| elasticPools.Create( - this.Server.ResourceName / this.Name, - this.Location, - [ servers.resourceId this.Server.ResourceName ] - ) with - properties = - {| - maxSizeBytes = this.MaxSizeBytes |> Option.toNullable - perDatabaseSettings = - match this.MinMax with - | Some (min, max) -> - box - {| - minCapacity = min - maxCapacity = max - |} - | None -> null - |} - sku = - {| - name = this.Sku.Name - tier = this.Sku.Edition - capacity = this.Sku.Capacity - |} - |} + member this.JsonModel = {| + elasticPools.Create( + this.Server.ResourceName / this.Name, + this.Location, + [ servers.resourceId this.Server.ResourceName ] + ) with + properties = {| + maxSizeBytes = this.MaxSizeBytes |> Option.toNullable + perDatabaseSettings = + match this.MinMax with + | Some(min, max) -> + box {| + minCapacity = min + maxCapacity = max + |} + | None -> null + |} + sku = {| + name = this.Sku.Name + tier = this.Sku.Edition + capacity = this.Sku.Capacity + |} + |} - type FirewallRule = - { - Name: ResourceName - Server: SqlAccountName - Location: Location - Start: IPAddress - End: IPAddress - } + type FirewallRule = { + Name: ResourceName + Server: SqlAccountName + Location: Location + Start: IPAddress + End: IPAddress + } with interface IArmResource with member this.ResourceId = firewallRules.resourceId (this.Server.ResourceName / this.Name) - member this.JsonModel = - {| firewallRules.Create( - this.Server.ResourceName / this.Name, - this.Location, - [ servers.resourceId this.Server.ResourceName ] - ) with - properties = - {| - startIpAddress = string this.Start - endIpAddress = string this.End - |} - |} + member this.JsonModel = {| + firewallRules.Create( + this.Server.ResourceName / this.Name, + this.Location, + [ servers.resourceId this.Server.ResourceName ] + ) with + properties = {| + startIpAddress = string this.Start + endIpAddress = string this.End + |} + |} - type Database = - { - Name: ResourceName - Server: SqlAccountName - Location: Location - MaxSizeBytes: int64 option - Sku: DbKind - Collation: string - } + type Database = { + Name: ResourceName + Server: SqlAccountName + Location: Location + MaxSizeBytes: int64 option + Sku: DbKind + Collation: string + } with interface IArmResource with member this.ResourceId = databases.resourceId (this.Server.ResourceName / this.Name) member this.JsonModel = - let dependsOn = - [ - servers.resourceId this.Server.ResourceName - match this.Sku with - | Pool poolName -> elasticPools.resourceId (this.Server.ResourceName, poolName) - | Standalone _ -> () - ] - - {| databases.Create( - this.Server.ResourceName / this.Name, - this.Location, - dependsOn, - tags = Map [ "displayName", this.Name.Value ] - ) with - sku = - match this.Sku with - | Standalone (VCore (GeneralPurpose (S_Gen5 (_, max)), _) as sku) - | Standalone (VCore (BusinessCritical (S_Gen5 (_, max)), _) as sku) - | Standalone (VCore (Hyperscale (S_Gen5 (_, max)), _) as sku) -> (* Serverless *) - box - {| + let dependsOn = [ + servers.resourceId this.Server.ResourceName + match this.Sku with + | Pool poolName -> elasticPools.resourceId (this.Server.ResourceName, poolName) + | Standalone _ -> () + ] + + {| + databases.Create( + this.Server.ResourceName / this.Name, + this.Location, + dependsOn, + tags = Map [ "displayName", this.Name.Value ] + ) with + sku = + match this.Sku with + | Standalone(VCore(GeneralPurpose(S_Gen5(_, max)), _) as sku) + | Standalone(VCore(BusinessCritical(S_Gen5(_, max)), _) as sku) + | Standalone(VCore(Hyperscale(S_Gen5(_, max)), _) as sku) -> (* Serverless *) + box {| name = sku.Name tier = sku.Edition capacity = max family = "Gen5" |} - | Standalone sku -> - box - {| + | Standalone sku -> + box {| name = sku.Name tier = sku.Edition |} - | Pool _ -> null - properties = - {| + | Pool _ -> null + properties = {| collation = this.Collation maxSizeBytes = this.MaxSizeBytes |> Option.toNullable licenseType = match this.Sku with - | Standalone (VCore (_, license)) -> license.ArmValue - | Standalone (DTU _) + | Standalone(VCore(_, license)) -> license.ArmValue + | Standalone(DTU _) | Pool _ -> null elasticPoolId = match this.Sku with @@ -188,36 +178,35 @@ module Servers = | Pool pool -> elasticPools.resourceId(this.Server.ResourceName, pool).Eval() autoPauseDelay = match this.Sku with - | Standalone (VCore (GeneralPurpose (S_Gen5 _), _)) - | Standalone (VCore (BusinessCritical (S_Gen5 _), _)) - | Standalone (VCore (Hyperscale (S_Gen5 _), _)) -> -1 |> box + | Standalone(VCore(GeneralPurpose(S_Gen5 _), _)) + | Standalone(VCore(BusinessCritical(S_Gen5 _), _)) + | Standalone(VCore(Hyperscale(S_Gen5 _), _)) -> -1 |> box | _ -> null minCapacity = match this.Sku with - | Standalone (VCore (GeneralPurpose (S_Gen5 (min, _)), _) as sku) - | Standalone (VCore (BusinessCritical (S_Gen5 (min, _)), _) as sku) - | Standalone (VCore (Hyperscale (S_Gen5 (min, _)), _) as sku) -> min |> box + | Standalone(VCore(GeneralPurpose(S_Gen5(min, _)), _) as sku) + | Standalone(VCore(BusinessCritical(S_Gen5(min, _)), _) as sku) + | Standalone(VCore(Hyperscale(S_Gen5(min, _)), _) as sku) -> min |> box | _ -> null |} |} module Databases = - type TransparentDataEncryption = - { - Server: SqlAccountName - Database: ResourceName - } + type TransparentDataEncryption = { + Server: SqlAccountName + Database: ResourceName + } with member this.Name = this.Server.ResourceName / this.Database / "current" interface IArmResource with member this.ResourceId = transparentDataEncryption.resourceId this.Name - member this.JsonModel = - {| transparentDataEncryption.Create( - this.Name, - dependsOn = [ databases.resourceId (this.Server.ResourceName, this.Database) ] - ) with + member this.JsonModel = {| + transparentDataEncryption.Create( + this.Name, + dependsOn = [ databases.resourceId (this.Server.ResourceName, this.Database) ] + ) with comments = "Transparent Data Encryption" properties = {| status = string Enabled |} - |} + |} diff --git a/src/Farmer/Arm/Storage.fs b/src/Farmer/Arm/Storage.fs index fd8373aec..7d48b02bb 100644 --- a/src/Farmer/Arm/Storage.fs +++ b/src/Farmer/Arm/Storage.fs @@ -61,12 +61,11 @@ type RuleAction = | Allow -> "Allow" | Deny -> "Deny" -type VirtualNetworkRule = - { - Subnet: ResourceName - VirtualNetwork: ResourceName - Action: RuleAction - } +type VirtualNetworkRule = { + Subnet: ResourceName + VirtualNetwork: ResourceName + Action: RuleAction +} type IpRuleValue = | IpRulePrefix of IPAddressCidr @@ -74,169 +73,163 @@ type IpRuleValue = member this.ArmValue = match this with - | IpRulePrefix (cidr) -> cidr |> IPAddressCidr.format - | IpRuleAddress (address) -> address.ToString() - -type IpRule = - { - Value: IpRuleValue - Action: RuleAction - } - -type NetworkRuleSet = - { - Bypass: Set - VirtualNetworkRules: VirtualNetworkRule list - IpRules: IpRule list - DefaultAction: RuleAction - } + | IpRulePrefix(cidr) -> cidr |> IPAddressCidr.format + | IpRuleAddress(address) -> address.ToString() + +type IpRule = { + Value: IpRuleValue + Action: RuleAction +} + +type NetworkRuleSet = { + Bypass: Set + VirtualNetworkRules: VirtualNetworkRule list + IpRules: IpRule list + DefaultAction: RuleAction +} /// Needed to build subnet resource ids for ACLs. let private subnets = ResourceType("Microsoft.Network/virtualNetworks/subnets", "") -type StorageAccount = - { - Name: StorageAccountName - Location: Location - Sku: Sku - Dependencies: ResourceId list - EnableHierarchicalNamespace: bool option - NetworkAcls: NetworkRuleSet option - StaticWebsite: {| IndexPage: string - ErrorPage: string option - ContentPath: string |} option - MinTlsVersion: TlsVersion option - DnsZoneType: string option - DisablePublicNetworkAccess: FeatureFlag option - DisableBlobPublicAccess: FeatureFlag option - DisableSharedKeyAccess: FeatureFlag option - DefaultToOAuthAuthentication: FeatureFlag option - Tags: Map - } +type StorageAccount = { + Name: StorageAccountName + Location: Location + Sku: Sku + Dependencies: ResourceId list + EnableHierarchicalNamespace: bool option + NetworkAcls: NetworkRuleSet option + StaticWebsite: + {| + IndexPage: string + ErrorPage: string option + ContentPath: string + |} option + MinTlsVersion: TlsVersion option + DnsZoneType: string option + DisablePublicNetworkAccess: FeatureFlag option + DisableBlobPublicAccess: FeatureFlag option + DisableSharedKeyAccess: FeatureFlag option + DefaultToOAuthAuthentication: FeatureFlag option + Tags: Map +} with interface IArmResource with member this.ResourceId = storageAccounts.resourceId this.Name.ResourceName - member this.JsonModel = - {| storageAccounts.Create(this.Name.ResourceName, this.Location, this.Dependencies, this.Tags) with - sku = - {| - name = - let performanceTier = - match this.Sku with - | GeneralPurpose (V1 (V1Replication.LRS performanceTier)) - | GeneralPurpose (V2 (V2Replication.LRS performanceTier, _)) -> performanceTier.ArmValue - | Files _ - | BlockBlobs _ -> "Premium" - | GeneralPurpose _ - | Blobs _ -> "Standard" - - let replicationModel = - match this.Sku with - | GeneralPurpose (V1 replication) -> replication.ReplicationModelDescription - | GeneralPurpose (V2 (replication, _)) -> replication.ReplicationModelDescription - | Blobs (replication, _) -> replication.ReplicationModelDescription - | Files replication - | BlockBlobs replication -> replication.ReplicationModelDescription - - $"{performanceTier}_{replicationModel}" - |} + member this.JsonModel = {| + storageAccounts.Create(this.Name.ResourceName, this.Location, this.Dependencies, this.Tags) with + sku = {| + name = + let performanceTier = + match this.Sku with + | GeneralPurpose(V1(V1Replication.LRS performanceTier)) + | GeneralPurpose(V2(V2Replication.LRS performanceTier, _)) -> performanceTier.ArmValue + | Files _ + | BlockBlobs _ -> "Premium" + | GeneralPurpose _ + | Blobs _ -> "Standard" + + let replicationModel = + match this.Sku with + | GeneralPurpose(V1 replication) -> replication.ReplicationModelDescription + | GeneralPurpose(V2(replication, _)) -> replication.ReplicationModelDescription + | Blobs(replication, _) -> replication.ReplicationModelDescription + | Files replication + | BlockBlobs replication -> replication.ReplicationModelDescription + + $"{performanceTier}_{replicationModel}" + |} kind = match this.Sku with - | GeneralPurpose (V1 _) -> "Storage" - | GeneralPurpose (V2 _) -> "StorageV2" + | GeneralPurpose(V1 _) -> "Storage" + | GeneralPurpose(V2 _) -> "StorageV2" | Blobs _ -> "BlobStorage" | Files _ -> "FileStorage" | BlockBlobs _ -> "BlockBlobStorage" - properties = - {| - isHnsEnabled = this.EnableHierarchicalNamespace |> Option.toNullable - accessTier = - match this.Sku with - | Blobs (_, Some tier) - | GeneralPurpose (V2 (_, Some tier)) -> - match tier with - | Hot -> "Hot" - | Cool -> "Cool" - | _ -> null - networkAcls = - this.NetworkAcls - |> Option.map (fun networkRuleSet -> - {| - bypass = - networkRuleSet.Bypass - |> Set.map NetworkRuleSetBypass.ArmValue - |> Set.toSeq - |> String.concat "," - virtualNetworkRules = - networkRuleSet.VirtualNetworkRules - |> List.map (fun rule -> - {| - id = subnets.resourceId(rule.VirtualNetwork, rule.Subnet).Eval() - action = rule.Action.ArmValue - |}) - ipRules = - networkRuleSet.IpRules - |> List.map (fun rule -> - {| - value = rule.Value.ArmValue - action = rule.Action.ArmValue - |}) - defaultAction = networkRuleSet.DefaultAction.ArmValue + properties = {| + isHnsEnabled = this.EnableHierarchicalNamespace |> Option.toNullable + accessTier = + match this.Sku with + | Blobs(_, Some tier) + | GeneralPurpose(V2(_, Some tier)) -> + match tier with + | Hot -> "Hot" + | Cool -> "Cool" + | _ -> null + networkAcls = + this.NetworkAcls + |> Option.map (fun networkRuleSet -> {| + bypass = + networkRuleSet.Bypass + |> Set.map NetworkRuleSetBypass.ArmValue + |> Set.toSeq + |> String.concat "," + virtualNetworkRules = + networkRuleSet.VirtualNetworkRules + |> List.map (fun rule -> {| + id = subnets.resourceId(rule.VirtualNetwork, rule.Subnet).Eval() + action = rule.Action.ArmValue |}) - |> Option.defaultValue Unchecked.defaultof<_> - minimumTlsVersion = - match this.MinTlsVersion with - | Some Tls10 -> "TLS1_0" - | Some Tls11 -> "TLS1_1" - | Some Tls12 -> "TLS1_2" - | None -> null - dnsEndpointType = - match this.DnsZoneType with - | Some s -> s - | None -> null - publicNetworkAccess = - match this.DisablePublicNetworkAccess with - | Some FeatureFlag.Disabled -> "Enabled" - | Some FeatureFlag.Enabled -> "Disabled" - | None -> null - allowBlobPublicAccess = - match this.DisableBlobPublicAccess with - | Some FeatureFlag.Disabled -> "true" - | Some FeatureFlag.Enabled -> "false" - | None -> null - allowSharedKeyAccess = - match this.DisableSharedKeyAccess with - | Some FeatureFlag.Disabled -> "true" - | Some FeatureFlag.Enabled -> "false" - | None -> null - defaultToOAuthAuthentication = - match this.DefaultToOAuthAuthentication with - | Some FeatureFlag.Disabled -> "false" - | Some FeatureFlag.Enabled -> "true" - | None -> null - |} - |} + ipRules = + networkRuleSet.IpRules + |> List.map (fun rule -> {| + value = rule.Value.ArmValue + action = rule.Action.ArmValue + |}) + defaultAction = networkRuleSet.DefaultAction.ArmValue + |}) + |> Option.defaultValue Unchecked.defaultof<_> + minimumTlsVersion = + match this.MinTlsVersion with + | Some Tls10 -> "TLS1_0" + | Some Tls11 -> "TLS1_1" + | Some Tls12 -> "TLS1_2" + | None -> null + dnsEndpointType = + match this.DnsZoneType with + | Some s -> s + | None -> null + publicNetworkAccess = + match this.DisablePublicNetworkAccess with + | Some FeatureFlag.Disabled -> "Enabled" + | Some FeatureFlag.Enabled -> "Disabled" + | None -> null + allowBlobPublicAccess = + match this.DisableBlobPublicAccess with + | Some FeatureFlag.Disabled -> "true" + | Some FeatureFlag.Enabled -> "false" + | None -> null + allowSharedKeyAccess = + match this.DisableSharedKeyAccess with + | Some FeatureFlag.Disabled -> "true" + | Some FeatureFlag.Enabled -> "false" + | None -> null + defaultToOAuthAuthentication = + match this.DefaultToOAuthAuthentication with + | Some FeatureFlag.Disabled -> "false" + | Some FeatureFlag.Enabled -> "true" + | None -> null + |} + |} interface IPostDeploy with member this.Run _ = this.StaticWebsite - |> Option.map (fun staticWebsite -> - result { - let! enableStaticResponse = - Deploy.Az.enableStaticWebsite - this.Name.ResourceName.Value - staticWebsite.IndexPage - staticWebsite.ErrorPage + |> Option.map (fun staticWebsite -> result { + let! enableStaticResponse = + Deploy.Az.enableStaticWebsite + this.Name.ResourceName.Value + staticWebsite.IndexPage + staticWebsite.ErrorPage - printfn - $"Deploying content of %s{staticWebsite.ContentPath} folder to $web container for storage account %s{this.Name.ResourceName.Value}" + printfn + $"Deploying content of %s{staticWebsite.ContentPath} folder to $web container for storage account %s{this.Name.ResourceName.Value}" - let! uploadResponse = - Deploy.Az.batchUploadStaticWebsite this.Name.ResourceName.Value staticWebsite.ContentPath + let! uploadResponse = + Deploy.Az.batchUploadStaticWebsite this.Name.ResourceName.Value staticWebsite.ContentPath - return enableStaticResponse + ", " + uploadResponse - }) + return enableStaticResponse + ", " + uploadResponse + }) [] module Extensions = @@ -245,21 +238,19 @@ module Extensions = member this.Emit(specificItemMapper: 'T -> string) = match this with | All -> [ "*" ] - | Specific items -> - [ - for item in items do - specificItemMapper item - ] + | Specific items -> [ + for item in items do + specificItemMapper item + ] /// A generic storage service that can be used for Blob, Table, Queue or FileServices -type StorageService = - { - StorageAccount: StorageResourceName - CorsRules: CorsRule list - Policies: Policy list - IsVersioningEnabled: bool - ResourceType: ResourceType - } +type StorageService = { + StorageAccount: StorageResourceName + CorsRules: CorsRule list + Policies: Policy list + IsVersioningEnabled: bool + ResourceType: ResourceType +} with interface IArmResource with member this.ResourceId = @@ -286,30 +277,27 @@ type StorageService = |} |> box - {| this.ResourceType.Create( - this.StorageAccount.ResourceName / "default", - dependsOn = [ storageAccounts.resourceId this.StorageAccount.ResourceName ] - ) with - properties = - {| - cors = - {| - corsRules = - [ - for rule in this.CorsRules do - {| - allowedOrigins = rule.AllowedOrigins.Emit(fun r -> r.OriginalString) - allowedMethods = - [ - for httpMethod in rule.AllowedMethods.Value do - httpMethod.ArmValue - ] - maxAgeInSeconds = rule.MaxAgeInSeconds - exposedHeaders = rule.ExposedHeaders.Emit id - allowedHeaders = rule.AllowedHeaders.Emit id - |} - ] - |} + {| + this.ResourceType.Create( + this.StorageAccount.ResourceName / "default", + dependsOn = [ storageAccounts.resourceId this.StorageAccount.ResourceName ] + ) with + properties = {| + cors = {| + corsRules = [ + for rule in this.CorsRules do + {| + allowedOrigins = rule.AllowedOrigins.Emit(fun r -> r.OriginalString) + allowedMethods = [ + for httpMethod in rule.AllowedMethods.Value do + httpMethod.ArmValue + ] + maxAgeInSeconds = rule.MaxAgeInSeconds + exposedHeaders = rule.ExposedHeaders.Emit id + allowedHeaders = rule.AllowedHeaders.Emit id + |} + ] + |} IsVersioningEnabled = this.IsVersioningEnabled deleteRetentionPolicy = this.Policies @@ -350,61 +338,56 @@ type StorageService = |} module BlobServices = - type Container = - { - Name: StorageResourceName - StorageAccount: ResourceName - Accessibility: StorageContainerAccess - } + type Container = { + Name: StorageResourceName + StorageAccount: ResourceName + Accessibility: StorageContainerAccess + } with interface IArmResource with member this.ResourceId = containers.resourceId (this.StorageAccount / "default" / this.Name.ResourceName) - member this.JsonModel = - {| containers.Create( - this.StorageAccount / "default" / this.Name.ResourceName, - dependsOn = [ storageAccounts.resourceId this.StorageAccount ] - ) with - properties = - {| - publicAccess = - match this.Accessibility with - | Private -> "None" - | Container -> "Container" - | Blob -> "Blob" - |} - |} + member this.JsonModel = {| + containers.Create( + this.StorageAccount / "default" / this.Name.ResourceName, + dependsOn = [ storageAccounts.resourceId this.StorageAccount ] + ) with + properties = {| + publicAccess = + match this.Accessibility with + | Private -> "None" + | Container -> "Container" + | Blob -> "Blob" + |} + |} module FileShares = - type FileShare = - { - Name: StorageResourceName - ShareQuota: int option - StorageAccount: ResourceName - } + type FileShare = { + Name: StorageResourceName + ShareQuota: int option + StorageAccount: ResourceName + } with interface IArmResource with member this.ResourceId = fileShares.resourceId (this.StorageAccount / "default" / this.Name.ResourceName) - member this.JsonModel = - {| fileShares.Create( - this.StorageAccount / "default" / this.Name.ResourceName, - dependsOn = [ storageAccounts.resourceId this.StorageAccount ] - ) with - properties = - {| - shareQuota = this.ShareQuota |> Option.defaultValue 5120 - |} - |} + member this.JsonModel = {| + fileShares.Create( + this.StorageAccount / "default" / this.Name.ResourceName, + dependsOn = [ storageAccounts.resourceId this.StorageAccount ] + ) with + properties = {| + shareQuota = this.ShareQuota |> Option.defaultValue 5120 + |} + |} module Tables = - type Table = - { - Name: StorageResourceName - StorageAccount: ResourceName - } + type Table = { + Name: StorageResourceName + StorageAccount: ResourceName + } with interface IArmResource with member this.ResourceId = @@ -417,11 +400,10 @@ module Tables = ) module Queues = - type Queue = - { - Name: StorageResourceName - StorageAccount: ResourceName - } + type Queue = { + Name: StorageResourceName + StorageAccount: ResourceName + } with interface IArmResource with member this.ResourceId = @@ -434,93 +416,83 @@ module Queues = ) module ManagementPolicies = - type ManagementPolicy = - { - Rules: {| Name: ResourceName - CoolBlobAfter: int option - ArchiveBlobAfter: int option - DeleteBlobAfter: int option - DeleteSnapshotAfter: int option - Filters: string list |} list - StorageAccount: ResourceName - } + type ManagementPolicy = { + Rules: + {| + Name: ResourceName + CoolBlobAfter: int option + ArchiveBlobAfter: int option + DeleteBlobAfter: int option + DeleteSnapshotAfter: int option + Filters: string list + |} list + StorageAccount: ResourceName + } with member this.ResourceName = this.StorageAccount / "default" interface IArmResource with member this.ResourceId = managementPolicies.resourceId this.ResourceName - member this.JsonModel = - {| managementPolicies.Create( - this.ResourceName, - dependsOn = [ storageAccounts.resourceId this.StorageAccount ] - ) with - properties = - {| - policy = - {| - rules = - [ - for rule in this.Rules do - {| - enabled = true - name = rule.Name.Value - ``type`` = "Lifecycle" - definition = + member this.JsonModel = {| + managementPolicies.Create( + this.ResourceName, + dependsOn = [ storageAccounts.resourceId this.StorageAccount ] + ) with + properties = {| + policy = {| + rules = [ + for rule in this.Rules do + {| + enabled = true + name = rule.Name.Value + ``type`` = "Lifecycle" + definition = {| + actions = {| + baseBlob = {| + tierToCool = + rule.CoolBlobAfter + |> Option.map (fun days -> + {| + daysAfterModificationGreaterThan = days + |} + |> box) + |> Option.toObj + tierToArchive = + rule.ArchiveBlobAfter + |> Option.map (fun days -> + {| + daysAfterModificationGreaterThan = days + |} + |> box) + |> Option.toObj + delete = + rule.DeleteBlobAfter + |> Option.map (fun days -> + {| + daysAfterModificationGreaterThan = days + |} + |> box) + |> Option.toObj + |} + snapshot = + rule.DeleteSnapshotAfter + |> Option.map (fun days -> {| - actions = - {| - baseBlob = - {| - tierToCool = - rule.CoolBlobAfter - |> Option.map (fun days -> - {| - daysAfterModificationGreaterThan = - days - |} - |> box) - |> Option.toObj - tierToArchive = - rule.ArchiveBlobAfter - |> Option.map (fun days -> - {| - daysAfterModificationGreaterThan = - days - |} - |> box) - |> Option.toObj - delete = - rule.DeleteBlobAfter - |> Option.map (fun days -> - {| - daysAfterModificationGreaterThan = - days - |} - |> box) - |> Option.toObj - |} - snapshot = - rule.DeleteSnapshotAfter - |> Option.map (fun days -> - {| - delete = - {| - daysAfterCreationGreaterThan = - days - |} - |} - |> box) - |> Option.toObj - |} - filters = - {| - blobTypes = [ "blockBlob" ] - prefixMatch = rule.Filters - |} + delete = {| + daysAfterCreationGreaterThan = days + |} |} - |} - ] - |} + |> box) + |> Option.toObj + |} + filters = {| + blobTypes = [ "blockBlob" ] + prefixMatch = rule.Filters + |} + |} + |} + ] |} - |} + |} + |} diff --git a/src/Farmer/Arm/TrafficManager.fs b/src/Farmer/Arm/TrafficManager.fs index 4a84e31f6..d7293ae5f 100644 --- a/src/Farmer/Arm/TrafficManager.fs +++ b/src/Farmer/Arm/TrafficManager.fs @@ -13,74 +13,67 @@ let azureEndpoints = let externalEndpoints = ResourceType("Microsoft.Network/trafficManagerProfiles/externalEndpoints", "2018-04-01") -type Endpoint = - { - Name: ResourceName - Status: FeatureFlag - Target: EndpointTarget - Weight: int option - Priority: int option - Location: Location option - } +type Endpoint = { + Name: ResourceName + Status: FeatureFlag + Target: EndpointTarget + Weight: int option + Priority: int option + Location: Location option +} with - member this.JsonModel = - {| - name = this.Name.Value - ``type`` = + member this.JsonModel = {| + name = this.Name.Value + ``type`` = + match this.Target with + | External _ -> externalEndpoints.Type + | Website _ -> azureEndpoints.Type + properties = {| + endpointStatus = this.Status.ArmValue + weight = this.Weight |> Option.toNullable + priority = this.Priority |> Option.toNullable + endpointLocation = this.Location |> Option.map (fun l -> l.ArmValue) |> Option.toObj + targetResourceId = match this.Target with - | External _ -> externalEndpoints.Type - | Website _ -> azureEndpoints.Type - properties = - {| - endpointStatus = this.Status.ArmValue - weight = this.Weight |> Option.toNullable - priority = this.Priority |> Option.toNullable - endpointLocation = this.Location |> Option.map (fun l -> l.ArmValue) |> Option.toObj - targetResourceId = - match this.Target with - | External _ -> null - | Website resourceName -> sites.resourceId(resourceName).Eval() - target = this.Target.ArmValue - |} + | External _ -> null + | Website resourceName -> sites.resourceId(resourceName).Eval() + target = this.Target.ArmValue |} + |} -type Profile = - { - Name: ResourceName - DnsTtl: int - Status: FeatureFlag - RoutingMethod: RoutingMethod - MonitorConfig: MonitorConfig - TrafficViewEnrollmentStatus: FeatureFlag - Endpoints: Endpoint list - Dependencies: ResourceId Set - Tags: Map - } +type Profile = { + Name: ResourceName + DnsTtl: int + Status: FeatureFlag + RoutingMethod: RoutingMethod + MonitorConfig: MonitorConfig + TrafficViewEnrollmentStatus: FeatureFlag + Endpoints: Endpoint list + Dependencies: ResourceId Set + Tags: Map +} with interface IArmResource with member this.ResourceId = profiles.resourceId (this.Name) - member this.JsonModel = - {| profiles.Create(this.Name, Location.Global, this.Dependencies, tags = this.Tags) with - properties = - {| - profileStatus = this.Status.ArmValue - trafficRoutingMethod = this.RoutingMethod.ArmValue - trafficViewEnrollmentStatus = this.TrafficViewEnrollmentStatus.ArmValue - dnsConfig = - {| - relativeName = this.Name.Value - ttl = this.DnsTtl - |} - monitorConfig = - {| - protocol = this.MonitorConfig.Protocol.ArmValue - port = this.MonitorConfig.Port - path = this.MonitorConfig.Path - intervalInSeconds = int this.MonitorConfig.IntervalInSeconds - toleratedNumberOfFailures = this.MonitorConfig.ToleratedNumberOfFailures - timeoutInSeconds = int this.MonitorConfig.TimeoutInSeconds - |} - endpoints = this.Endpoints |> List.map (fun e -> e.JsonModel) + member this.JsonModel = {| + profiles.Create(this.Name, Location.Global, this.Dependencies, tags = this.Tags) with + properties = {| + profileStatus = this.Status.ArmValue + trafficRoutingMethod = this.RoutingMethod.ArmValue + trafficViewEnrollmentStatus = this.TrafficViewEnrollmentStatus.ArmValue + dnsConfig = {| + relativeName = this.Name.Value + ttl = this.DnsTtl + |} + monitorConfig = {| + protocol = this.MonitorConfig.Protocol.ArmValue + port = this.MonitorConfig.Port + path = this.MonitorConfig.Path + intervalInSeconds = int this.MonitorConfig.IntervalInSeconds + toleratedNumberOfFailures = this.MonitorConfig.ToleratedNumberOfFailures + timeoutInSeconds = int this.MonitorConfig.TimeoutInSeconds |} - |} + endpoints = this.Endpoints |> List.map (fun e -> e.JsonModel) + |} + |} diff --git a/src/Farmer/Arm/VirtualHub.fs b/src/Farmer/Arm/VirtualHub.fs index aa9edf2b4..970a54987 100644 --- a/src/Farmer/Arm/VirtualHub.fs +++ b/src/Farmer/Arm/VirtualHub.fs @@ -25,106 +25,99 @@ type RoutingState = | Failed -> "Failed" | None -> "None" -type Route = - { - AddressPrefixes: IPAddressCidr list - NextHopIpAddress: System.Net.IPAddress - } +type Route = { + AddressPrefixes: IPAddressCidr list + NextHopIpAddress: System.Net.IPAddress +} // none of the VirtualHub Properties Objects are required so are being set as options -type VirtualHub = - { - Name: ResourceName - Location: Location - Dependencies: ResourceId Set - /// The addressPrefix that is associated with VirtualHub - cidr block string - AddressPrefix: IPAddressCidr option - AllowBranchToBranchTraffic: bool option - /// The azureFirewall that is associated with VirtualHub - AzureFirewall: ResourceId option - /// The expressRouteGateway that is associated with VirtualHub - ExpressRouteGateway: ResourceId option - /// The P2SVpnGateway that is associated with VirtualHub - P2SVpnGateway: ResourceId option - /// The routeTable that is associated with VirtualHub - RouteTable: Route list - /// The routingState that is associated with VirtualHub - RoutingState: RoutingState option - /// The securityProvider that is associated with VirtualHub - SecurityProvider: string option - /// The securityPartnerProvider that is associated with VirtualHub - SecurityPartnerProvider: ResourceId option - /// The virtualHubRouteTableV2s that is an array of all VirtualHub route table v2s associated - VirtualHubRouteTableV2s: obj list - /// The virtualHubSku that is associated with VirtualHub - VirtualHubSku: Sku - /// The VirtualRouterAsn that is associated with VirtualHub - VirtualRouterAsn: int option - /// The virtualRouterIps that is associated with VirtualHub - an array of IPs (string) - VirtualRouterIps: System.Net.IPAddress list - /// The VPN Gateway associated with the Virtual Hub - VpnGateway: ResourceId option - /// To be used for the depends on for VHUB to connect to previous VWAN deployment - Vwan: ResourceId option - } +type VirtualHub = { + Name: ResourceName + Location: Location + Dependencies: ResourceId Set + /// The addressPrefix that is associated with VirtualHub - cidr block string + AddressPrefix: IPAddressCidr option + AllowBranchToBranchTraffic: bool option + /// The azureFirewall that is associated with VirtualHub + AzureFirewall: ResourceId option + /// The expressRouteGateway that is associated with VirtualHub + ExpressRouteGateway: ResourceId option + /// The P2SVpnGateway that is associated with VirtualHub + P2SVpnGateway: ResourceId option + /// The routeTable that is associated with VirtualHub + RouteTable: Route list + /// The routingState that is associated with VirtualHub + RoutingState: RoutingState option + /// The securityProvider that is associated with VirtualHub + SecurityProvider: string option + /// The securityPartnerProvider that is associated with VirtualHub + SecurityPartnerProvider: ResourceId option + /// The virtualHubRouteTableV2s that is an array of all VirtualHub route table v2s associated + VirtualHubRouteTableV2s: obj list + /// The virtualHubSku that is associated with VirtualHub + VirtualHubSku: Sku + /// The VirtualRouterAsn that is associated with VirtualHub + VirtualRouterAsn: int option + /// The virtualRouterIps that is associated with VirtualHub - an array of IPs (string) + VirtualRouterIps: System.Net.IPAddress list + /// The VPN Gateway associated with the Virtual Hub + VpnGateway: ResourceId option + /// To be used for the depends on for VHUB to connect to previous VWAN deployment + Vwan: ResourceId option +} with interface IArmResource with member this.ResourceId = virtualHubs.resourceId this.Name - member this.JsonModel = - {| virtualHubs.Create(this.Name, this.Location, this.Dependencies) with - properties = - {| - addressPrefix = - this.AddressPrefix - |> Option.map IPAddressCidr.format - |> Option.defaultValue null - azureFirewall = this.AzureFirewall |> Option.defaultValue Unchecked.defaultof - routeTable = {| routes = this.RouteTable |} - sku = this.VirtualHubSku.ArmValue - virtualWan = - this.Vwan - |> Option.map (fun resId -> box {| id = resId.ArmExpression.Eval() |}) - |> Option.defaultValue null - |} - |} + member this.JsonModel = {| + virtualHubs.Create(this.Name, this.Location, this.Dependencies) with + properties = {| + addressPrefix = + this.AddressPrefix + |> Option.map IPAddressCidr.format + |> Option.defaultValue null + azureFirewall = this.AzureFirewall |> Option.defaultValue Unchecked.defaultof + routeTable = {| routes = this.RouteTable |} + sku = this.VirtualHubSku.ArmValue + virtualWan = + this.Vwan + |> Option.map (fun resId -> box {| id = resId.ArmExpression.Eval() |}) + |> Option.defaultValue null + |} + |} open Farmer.VirtualHub.HubRouteTable -type HubRoute = - { - Name: string - Destination: Destination - NextHop: NextHop - } +type HubRoute = { + Name: string + Destination: Destination + NextHop: NextHop +} with - member this.JsonModel = - {| - name = this.Name - destinationType = this.Destination.DestinationTypeArmValue - destinations = this.Destination.DestinationsArmValue - nextHopType = this.NextHop.NextHopTypeArmValue - nextHop = this.NextHop.NextHopArmValue - |} + member this.JsonModel = {| + name = this.Name + destinationType = this.Destination.DestinationTypeArmValue + destinations = this.Destination.DestinationsArmValue + nextHopType = this.NextHop.NextHopTypeArmValue + nextHop = this.NextHop.NextHopArmValue + |} // https://docs.microsoft.com/en-us/azure/templates/microsoft.network/virtualhubs/hubroutetables -type HubRouteTable = - { - Name: ResourceName - VirtualHub: ResourceId - Dependencies: ResourceId Set - Routes: HubRoute list - Labels: string list - } +type HubRouteTable = { + Name: ResourceName + VirtualHub: ResourceId + Dependencies: ResourceId Set + Routes: HubRoute list + Labels: string list +} with interface IArmResource with member this.ResourceId = hubRouteTables.resourceId (this.VirtualHub.Name / this.Name) - member this.JsonModel = - {| hubRouteTables.Create(this.VirtualHub.Name / this.Name, dependsOn = this.Dependencies) with - properties = - {| - routes = this.Routes |> List.map (fun r -> r.JsonModel) - labels = this.Labels - |} - |} + member this.JsonModel = {| + hubRouteTables.Create(this.VirtualHub.Name / this.Name, dependsOn = this.Dependencies) with + properties = {| + routes = this.Routes |> List.map (fun r -> r.JsonModel) + labels = this.Labels + |} + |} diff --git a/src/Farmer/Arm/VirtualWan.fs b/src/Farmer/Arm/VirtualWan.fs index b1f391f94..636008876 100644 --- a/src/Farmer/Arm/VirtualWan.fs +++ b/src/Farmer/Arm/VirtualWan.fs @@ -29,29 +29,27 @@ type VwanType = | Standard -> "Standard" | Basic -> "Basic" -type VirtualWan = - { - Name: ResourceName - Location: Location - AllowBranchToBranchTraffic: bool option - DisableVpnEncryption: bool option - Office365LocalBreakoutCategory: Office365LocalBreakoutCategory option - VwanType: VwanType - } +type VirtualWan = { + Name: ResourceName + Location: Location + AllowBranchToBranchTraffic: bool option + DisableVpnEncryption: bool option + Office365LocalBreakoutCategory: Office365LocalBreakoutCategory option + VwanType: VwanType +} with interface IArmResource with member this.ResourceId = virtualWans.resourceId this.Name - member this.JsonModel = - {| virtualWans.Create(this.Name, this.Location) with - properties = - {| - allowBranchToBranchTraffic = this.AllowBranchToBranchTraffic |> Option.defaultValue false - disableVpnEncryption = this.DisableVpnEncryption |> Option.defaultValue false - office365LocalBreakoutCategory = - (this.Office365LocalBreakoutCategory - |> Option.defaultValue Office365LocalBreakoutCategory.None) - .ArmValue - ``type`` = this.VwanType.ArmValue - |} - |} + member this.JsonModel = {| + virtualWans.Create(this.Name, this.Location) with + properties = {| + allowBranchToBranchTraffic = this.AllowBranchToBranchTraffic |> Option.defaultValue false + disableVpnEncryption = this.DisableVpnEncryption |> Option.defaultValue false + office365LocalBreakoutCategory = + (this.Office365LocalBreakoutCategory + |> Option.defaultValue Office365LocalBreakoutCategory.None) + .ArmValue + ``type`` = this.VwanType.ArmValue + |} + |} diff --git a/src/Farmer/Arm/Web.fs b/src/Farmer/Arm/Web.fs index 2f1b80dc4..28c9008e6 100644 --- a/src/Farmer/Arm/Web.fs +++ b/src/Farmer/Arm/Web.fs @@ -37,18 +37,17 @@ let private mapOrNull f = Option.map (Map.toList >> List.map f) >> Option.defaultValue Unchecked.defaultof<_> -type ServerFarm = - { - Name: ResourceName - Location: Location - Sku: Sku - WorkerSize: WorkerSize - WorkerCount: int - MaximumElasticWorkerCount: int option - OperatingSystem: OS - ZoneRedundant: FeatureFlag option - Tags: Map - } +type ServerFarm = { + Name: ResourceName + Location: Location + Sku: Sku + WorkerSize: WorkerSize + WorkerCount: int + MaximumElasticWorkerCount: int option + OperatingSystem: OS + ZoneRedundant: FeatureFlag option + Tags: Map +} with member this.IsDynamic = match this.Sku, this.WorkerSize with @@ -98,43 +97,41 @@ type ServerFarm = interface IArmResource with member this.ResourceId = serverFarms.resourceId this.Name - member this.JsonModel = - {| serverFarms.Create(this.Name, this.Location, tags = this.Tags) with - sku = - {| - name = - match this.Sku with - | Free -> "F1" - | Shared -> "D1" - | Basic sku - | Standard sku - | Premium sku - | PremiumV2 sku - | PremiumV3 sku - | ElasticPremium sku - | Isolated sku -> sku - | Dynamic -> "Y1" - tier = this.Tier - size = - match this.WorkerSize with - | Small -> "0" - | Medium -> "1" - | Large -> "2" - | Serverless -> "Y1" - family = if this.IsDynamic then "Y" else null - capacity = if this.IsDynamic then 0 else this.WorkerCount - |} - properties = - {| - name = this.Name.Value - 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 - zoneRedundant = this.ZoneRedundant |> Option.map (fun f -> f.AsBoolean) |> Option.toNullable - |} + member this.JsonModel = {| + serverFarms.Create(this.Name, this.Location, tags = this.Tags) with + sku = {| + name = + match this.Sku with + | Free -> "F1" + | Shared -> "D1" + | Basic sku + | Standard sku + | Premium sku + | PremiumV2 sku + | PremiumV3 sku + | ElasticPremium sku + | Isolated sku -> sku + | Dynamic -> "Y1" + tier = this.Tier + size = + match this.WorkerSize with + | Small -> "0" + | Medium -> "1" + | Large -> "2" + | Serverless -> "Y1" + family = if this.IsDynamic then "Y" else null + capacity = if this.IsDynamic then 0 else this.WorkerCount + |} + properties = {| + name = this.Name.Value + 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 + zoneRedundant = this.ZoneRedundant |> Option.map (fun f -> f.AsBoolean) |> Option.toNullable + |} kind = this.Kind |> Option.toObj - |} + |} module ZipDeploy = open System.IO @@ -204,42 +201,41 @@ type FTPState = | FtpsOnly | Disabled -type Site = - { - SiteType: SiteType - Location: Location - ServicePlan: ResourceId - AppSettings: Map option - ConnectionStrings: Map option - AlwaysOn: bool - WorkerProcess: Bitness option - HTTPSOnly: bool - FTPState: FTPState option - HTTP20Enabled: bool option - ClientAffinityEnabled: bool option - WebSocketsEnabled: bool option - Cors: Cors option - Dependencies: ResourceId Set - Kind: string - Identity: Identity.ManagedIdentity - KeyVaultReferenceIdentity: UserAssignedIdentity option - LinuxFxVersion: string option - AppCommandLine: string option - NetFrameworkVersion: string option - JavaVersion: string option - JavaContainer: string option - JavaContainerVersion: string option - PhpVersion: string option - PythonVersion: string option - Tags: Map - Metadata: List - AutoSwapSlotName: string option - ZipDeployPath: (string * ZipDeploy.ZipDeployTarget * ZipDeploy.ZipDeploySlot) option - HealthCheckPath: string option - IpSecurityRestrictions: IpSecurityRestriction list - LinkToSubnet: SubnetReference option - VirtualApplications: Map - } +type Site = { + SiteType: SiteType + Location: Location + ServicePlan: ResourceId + AppSettings: Map option + ConnectionStrings: Map option + AlwaysOn: bool + WorkerProcess: Bitness option + HTTPSOnly: bool + FTPState: FTPState option + HTTP20Enabled: bool option + ClientAffinityEnabled: bool option + WebSocketsEnabled: bool option + Cors: Cors option + Dependencies: ResourceId Set + Kind: string + Identity: Identity.ManagedIdentity + KeyVaultReferenceIdentity: UserAssignedIdentity option + LinuxFxVersion: string option + AppCommandLine: string option + NetFrameworkVersion: string option + JavaVersion: string option + JavaContainer: string option + JavaContainerVersion: string option + PhpVersion: string option + PythonVersion: string option + Tags: Map + Metadata: List + AutoSwapSlotName: string option + ZipDeployPath: (string * ZipDeploy.ZipDeployTarget * ZipDeploy.ZipDeploySlot) option + HealthCheckPath: string option + IpSecurityRestrictions: IpSecurityRestriction list + LinkToSubnet: SubnetReference option + VirtualApplications: Map +} with /// Shorthand for SiteType.ResourceType member this.ResourceType = this.SiteType.ResourceType @@ -265,7 +261,7 @@ type Site = member this.Run resourceGroupName = match this with | { - ZipDeployPath = Some (path, target, slot) + ZipDeployPath = Some(path, target, slot) SiteType = siteType } -> let path = @@ -312,15 +308,15 @@ type Site = } -> x.ResourceId.Eval() | _ -> null - {| this.ResourceType.Create(this.Name, this.Location, dependencies, this.Tags) with - kind = this.Kind - identity = - if this.Identity = ManagedIdentity.Empty then - Unchecked.defaultof<_> - else - this.Identity.ToArmJson - properties = - {| + {| + this.ResourceType.Create(this.Name, this.Location, dependencies, this.Tags) with + kind = this.Kind + identity = + if this.Identity = ManagedIdentity.Empty then + Unchecked.defaultof<_> + else + this.Identity.ToArmJson + properties = {| serverFarmId = this.ServicePlan.Eval() httpsOnly = this.HTTPSOnly clientAffinityEnabled = @@ -332,128 +328,118 @@ type Site = match this.LinkToSubnet with | None -> null | Some id -> id.ResourceId.ArmExpression.Eval() - siteConfig = - {| - alwaysOn = this.AlwaysOn - appSettings = - this.AppSettings |> mapOrNull (fun (k, v) -> {| name = k; value = v.Value |}) - connectionStrings = - this.ConnectionStrings - |> mapOrNull (fun (k, (v, t)) -> - {| - name = k - connectionString = v.Value - ``type`` = t.ToString() + siteConfig = {| + alwaysOn = this.AlwaysOn + appSettings = this.AppSettings |> mapOrNull (fun (k, v) -> {| name = k; value = v.Value |}) + connectionStrings = + this.ConnectionStrings + |> mapOrNull (fun (k, (v, t)) -> {| + name = k + connectionString = v.Value + ``type`` = t.ToString() + |}) + ftpsState = + match this.FTPState with + | Some FTPState.AllAllowed -> "AllAllowed" + | Some FTPState.FtpsOnly -> "FtpsOnly" + | Some FTPState.Disabled -> "Disabled" + | None -> null + linuxFxVersion = this.LinuxFxVersion |> Option.toObj + appCommandLine = this.AppCommandLine |> Option.toObj + netFrameworkVersion = this.NetFrameworkVersion |> Option.toObj + use32BitWorkerProcess = + this.WorkerProcess + |> Option.map (function + | Bits32 -> true + | Bits64 -> false) + |> Option.toNullable + javaVersion = this.JavaVersion |> Option.toObj + 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 + metadata = [ + for key, value in this.Metadata do + {| name = key; value = value |} + ] + cors = + this.Cors + |> Option.map (function + | AllOrigins -> box {| allowedOrigins = [ "*" ] |} + | SpecificOrigins(origins, credentials) -> + box {| + allowedOrigins = origins + supportCredentials = credentials |> Option.toNullable |}) - ftpsState = - match this.FTPState with - | Some FTPState.AllAllowed -> "AllAllowed" - | Some FTPState.FtpsOnly -> "FtpsOnly" - | Some FTPState.Disabled -> "Disabled" - | None -> null - linuxFxVersion = this.LinuxFxVersion |> Option.toObj - appCommandLine = this.AppCommandLine |> Option.toObj - netFrameworkVersion = this.NetFrameworkVersion |> Option.toObj - use32BitWorkerProcess = - this.WorkerProcess - |> Option.map (function - | Bits32 -> true - | Bits64 -> false) - |> Option.toNullable - javaVersion = this.JavaVersion |> Option.toObj - 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 - metadata = - [ - for key, value in this.Metadata do - {| name = key; value = value |} - ] - cors = - this.Cors - |> Option.map (function - | AllOrigins -> box {| allowedOrigins = [ "*" ] |} - | SpecificOrigins (origins, credentials) -> - box - {| - allowedOrigins = origins - supportCredentials = credentials |> Option.toNullable - |}) - |> 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() - virtualApplications = - if this.VirtualApplications.IsEmpty then - null - else - this.VirtualApplications - |> Seq.map (fun virtualAppKvp -> - {| - virtualPath = virtualAppKvp.Key - physicalPath = virtualAppKvp.Value.PhysicalPath - preloadEnabled = virtualAppKvp.Value.PreloadEnabled |> Option.toNullable - |}) - |> box - |} + |> 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() + virtualApplications = + if this.VirtualApplications.IsEmpty then + null + else + this.VirtualApplications + |> Seq.map (fun virtualAppKvp -> {| + virtualPath = virtualAppKvp.Key + physicalPath = virtualAppKvp.Value.PhysicalPath + preloadEnabled = virtualAppKvp.Value.PreloadEnabled |> Option.toNullable + |}) + |> box + |} |} |} module Sites = - type SourceControl = - { - Website: ResourceName - Location: Location - Repository: Uri - Branch: string - ContinuousIntegration: FeatureFlag - } + type SourceControl = { + Website: ResourceName + Location: Location + Repository: Uri + Branch: string + ContinuousIntegration: FeatureFlag + } with member this.Name = this.Website.Map(sprintf "%s/web") interface IArmResource with member this.ResourceId = sourceControls.resourceId this.Name - member this.JsonModel = - {| sourceControls.Create(this.Name, this.Location, [ sites.resourceId this.Website ]) with - properties = - {| - repoUrl = this.Repository.ToString() - branch = this.Branch - isManualIntegration = this.ContinuousIntegration.AsBoolean |> not - |} - |} + member this.JsonModel = {| + sourceControls.Create(this.Name, this.Location, [ sites.resourceId this.Website ]) with + properties = {| + repoUrl = this.Repository.ToString() + branch = this.Branch + isManualIntegration = this.ContinuousIntegration.AsBoolean |> not + |} + |} -type VirtualNetworkConnection = - { - Site: Site - Subnet: ResourceId - Dependencies: ResourceId list - } +type VirtualNetworkConnection = { + Site: Site + Subnet: ResourceId + Dependencies: ResourceId list +} with member this.Name = this.Site.Name / this.Subnet.Name member this.SiteId = this.Site.ResourceType.resourceId this.Site.Name @@ -467,81 +453,76 @@ type VirtualNetworkConnection = | Site _ -> virtualNetworkConnections | Slot _ -> slotsVirtualNetworkConnections - {| resourceType.Create(this.Name, dependsOn = [ this.SiteId; yield! this.Dependencies ]) with - properties = - {| + {| + 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 - Repository: Uri - Branch: string - RepositoryToken: SecureParameter - AppLocation: string - ApiLocation: string option - AppArtifactLocation: string option - } +type StaticSite = { + Name: ResourceName + Location: Location + Repository: Uri + Branch: string + RepositoryToken: SecureParameter + AppLocation: string + ApiLocation: string option + AppArtifactLocation: string option +} with interface IArmResource with member this.ResourceId = staticSites.resourceId this.Name - member this.JsonModel = - {| staticSites.Create(this.Name, this.Location) with - properties = - {| - repositoryUrl = this.Repository.ToString() - branch = this.Branch - repositoryToken = this.RepositoryToken.ArmExpression.Eval() - buildProperties = - {| - appLocation = this.AppLocation - apiLocation = this.ApiLocation |> Option.toObj - appArtifactLocation = this.AppArtifactLocation |> Option.toObj - |} + member this.JsonModel = {| + staticSites.Create(this.Name, this.Location) with + properties = {| + repositoryUrl = this.Repository.ToString() + branch = this.Branch + repositoryToken = this.RepositoryToken.ArmExpression.Eval() + buildProperties = {| + appLocation = this.AppLocation + apiLocation = this.ApiLocation |> Option.toObj + appArtifactLocation = this.AppArtifactLocation |> Option.toObj |} + |} sku = {| Tier = "Free"; Name = "Free" |} - |} + |} interface IParameters with member this.SecureParameters = [ this.RepositoryToken ] module StaticSites = - type Config = - { - StaticSite: ResourceName - Properties: Map - } + type Config = { + StaticSite: ResourceName + Properties: Map + } with member this.ResourceName = this.StaticSite / "appsettings" interface IArmResource with member this.ResourceId = staticSitesConfig.resourceId this.ResourceName - member this.JsonModel: obj = - {| staticSitesConfig.Create( - this.ResourceName, - dependsOn = [ ResourceId.create (staticSites, this.StaticSite) ] - ) with + member this.JsonModel: obj = {| + staticSitesConfig.Create( + this.ResourceName, + dependsOn = [ ResourceId.create (staticSites, this.StaticSite) ] + ) with properties = this.Properties - |} + |} type SslState = | SslDisabled | SniBased of thumbprint: ArmExpression -type HostNameBinding = - { - Location: Location - SiteId: LinkedResource - DomainName: string - SslState: SslState - } +type HostNameBinding = { + Location: Location + SiteId: LinkedResource + DomainName: string + SslState: SslState +} with member this.SiteResourceId = this.SiteId.Name member this.ResourceName = this.SiteResourceId / this.DomainName @@ -552,8 +533,8 @@ type HostNameBinding = interface IArmResource with member this.ResourceId = hostNameBindings.resourceId this.ResourceName - member this.JsonModel = - {| hostNameBindings.Create(this.ResourceName, this.Location) with + member this.JsonModel = {| + hostNameBindings.Create(this.ResourceName, this.Location) with properties = match this.SslState with | SniBased thumbprint -> @@ -562,16 +543,15 @@ type HostNameBinding = thumbprint = thumbprint.Eval() |} :> obj - | SslDisabled -> {| |} :> obj - |} + | SslDisabled -> {| |} :> obj + |} -type Certificate = - { - Location: Location - SiteId: LinkedResource - ServicePlanId: LinkedResource - DomainName: string - } +type Certificate = { + Location: Location + SiteId: LinkedResource + ServicePlanId: LinkedResource + DomainName: string +} with member this.ResourceName = ResourceName this.DomainName member this.Thumbprint = this.GetThumbprintReference None @@ -579,8 +559,9 @@ type Certificate = member this.GetThumbprintReference certificateResourceGroup = ArmExpression .reference( - { certificates.resourceId this.ResourceName with - ResourceGroup = certificateResourceGroup + { + certificates.resourceId this.ResourceName with + ResourceGroup = certificateResourceGroup } ) .Map(sprintf "%s.Thumbprint") @@ -591,18 +572,18 @@ type Certificate = member this.JsonModel = let dependencies = match this.SiteId with - | Managed r -> - [ - r - { hostNameBindings.resourceId (r.Name, ResourceName this.DomainName) with + | Managed r -> [ + r + { + hostNameBindings.resourceId (r.Name, ResourceName this.DomainName) with ResourceGroup = r.ResourceGroup - } - ] + } + ] | _ -> [] - {| certificates.Create(this.ResourceName, this.Location, dependencies) with - properties = - {| + {| + certificates.Create(this.ResourceName, this.Location, dependencies) with + properties = {| serverFarmId = this.ServicePlanId.ResourceId.Eval() canonicalName = this.DomainName |} @@ -610,12 +591,11 @@ type Certificate = [] module SiteExtensions = - type SiteExtension = - { - Name: ResourceName - SiteName: ResourceName - Location: Location - } + type SiteExtension = { + Name: ResourceName + SiteName: ResourceName + Location: Location + } with interface IArmResource with member this.ResourceId = siteExtensions.resourceId (this.SiteName / this.Name) diff --git a/src/Farmer/Builders/Builders.Alert.fs b/src/Farmer/Builders/Builders.Alert.fs index 947085fb3..4eb196f8b 100644 --- a/src/Farmer/Builders/Builders.Alert.fs +++ b/src/Farmer/Builders/Builders.Alert.fs @@ -5,48 +5,45 @@ open Farmer open Farmer.Insights open Farmer.Arm.InsightsAlerts -type AlertConfig = - { - Name: Farmer.ResourceName - Description: string - Severity: AlertSeverity - Frequency: IsoDateTime - Window: IsoDateTime - Actions: List - LinkedResources: LinkedResource list - Criteria: MetricAlertCriteria - } +type AlertConfig = { + Name: Farmer.ResourceName + Description: string + Severity: AlertSeverity + Frequency: IsoDateTime + Window: IsoDateTime + Actions: List + LinkedResources: LinkedResource list + Criteria: MetricAlertCriteria +} with interface IBuilder with member this.ResourceId = metricAlert.resourceId this.Name member this.BuildResources _ = - let a: AlertData = - { - Name = this.Name - Description = this.Description - Severity = this.Severity - Frequency = this.Frequency - Window = this.Window - Actions = this.Actions - LinkedResources = this.LinkedResources |> List.map (fun r -> r.ResourceId) - Criteria = this.Criteria - } + let a: AlertData = { + Name = this.Name + Description = this.Description + Severity = this.Severity + Frequency = this.Frequency + Window = this.Window + Actions = this.Actions + LinkedResources = this.LinkedResources |> List.map (fun r -> r.ResourceId) + Criteria = this.Criteria + } [ a ] type AlertBuilder() = - member __.Yield _ = - { - Name = ResourceName.Empty - Description = "" - Severity = AlertSeverity.Error - Frequency = System.TimeSpan(0, 5, 0) |> IsoDateTime.OfTimeSpan - Window = System.TimeSpan(0, 15, 0) |> IsoDateTime.OfTimeSpan - Actions = List.empty - LinkedResources = List.empty - Criteria = SingleResourceMultipleMetricCriteria [] - } + member __.Yield _ = { + Name = ResourceName.Empty + Description = "" + Severity = AlertSeverity.Error + Frequency = System.TimeSpan(0, 5, 0) |> IsoDateTime.OfTimeSpan + Window = System.TimeSpan(0, 15, 0) |> IsoDateTime.OfTimeSpan + Actions = List.empty + LinkedResources = List.empty + Criteria = SingleResourceMultipleMetricCriteria [] + } [] /// Sets the name of the alert. @@ -54,8 +51,7 @@ type AlertBuilder() = [] /// Sets the description of the alert. - member __.Description(state: AlertConfig, description) = - { state with Description = description } + member __.Description(state: AlertConfig, description) = { state with Description = description } [] /// How often the metric alert is evaluated @@ -71,17 +67,17 @@ type AlertBuilder() = [] /// Add the target resources on which the alert is created/updated. - member __.LinkedResources(state: AlertConfig, linked_resources) = - { state with + member __.LinkedResources(state: AlertConfig, linked_resources) = { + state with LinkedResources = linked_resources @ state.LinkedResources - } + } [] /// Add target resource on which the alert is created/updated. - member this.LinkedResource(state: AlertConfig, linked_resource) = - { state with + member this.LinkedResource(state: AlertConfig, linked_resource) = { + state with LinkedResources = linked_resource :: state.LinkedResources - } + } member this.LinkedResource(state: AlertConfig, resource: ResourceId) = this.LinkedResource(state, resource |> Managed) @@ -91,39 +87,39 @@ type AlertBuilder() = [] /// The rule criteria that defines the conditions of the alert rule. - member __.SingleCriteria(state: AlertConfig, criteria) = - { state with + member __.SingleCriteria(state: AlertConfig, criteria) = { + state with Criteria = SingleResourceMultipleMetricCriteria criteria - } + } [] /// The rule criteria that defines the conditions of the alert rule based on Application Insights custom metric. - member __.SingleCustomMetricCriteria(state: AlertConfig, criteria) = - { state with + member __.SingleCustomMetricCriteria(state: AlertConfig, criteria) = { + state with Criteria = SingleResourceMultipleCustomMetricCriteria criteria - } + } [] /// The rule criterias that defines the conditions of the alert rule. - member __.MultiCriteria(state: AlertConfig, criteria) = - { state with + member __.MultiCriteria(state: AlertConfig, criteria) = { + state with Criteria = MultipleResourceMultipleMetricCriteria criteria - } + } [] /// The rule criteria that defines the conditions of the alert rule. /// AppInsightsId * WebTestId * FailedLocationCount /// If webtest is failing at the same time from x different locations - member __.WebCriteria(state: AlertConfig, criteria) = - { state with + member __.WebCriteria(state: AlertConfig, criteria) = { + state with Criteria = WebtestLocationAvailabilityCriteria criteria - } + } [] /// Add an action that are performed when the alert rule becomes active. - member __.Actions(state: AlertConfig, action) = - { state with + member __.Actions(state: AlertConfig, action) = { + state with Actions = action :: state.Actions - } + } let alert = AlertBuilder() diff --git a/src/Farmer/Builders/Builders.AppInsights.fs b/src/Farmer/Builders/Builders.AppInsights.fs index f19a75ca8..285c11d94 100644 --- a/src/Farmer/Builders/Builders.AppInsights.fs +++ b/src/Farmer/Builders/Builders.AppInsights.fs @@ -26,15 +26,14 @@ type AppInsights = let resourceType = resourceType |> Option.defaultValue components AppInsights.getConnectionString (ResourceId.create (resourceType, name, ?group = resourceGroup)) -type AppInsightsConfig = - { - Name: ResourceName - DisableIpMasking: bool - SamplingPercentage: int - InstanceKind: InstanceKind - Dependencies: ResourceId Set - Tags: Map - } +type AppInsightsConfig = { + Name: ResourceName + DisableIpMasking: bool + SamplingPercentage: int + InstanceKind: InstanceKind + Dependencies: ResourceId Set + Tags: Map +} with /// Gets the ARM expression path to the instrumentation key of this App Insights instance. member this.InstrumentationKey = @@ -46,30 +45,28 @@ type AppInsightsConfig = interface IBuilder with member this.ResourceId = components.resourceId this.Name - member this.BuildResources location = - [ - { - Name = this.Name - Location = location - LinkedWebsite = None - DisableIpMasking = this.DisableIpMasking - SamplingPercentage = this.SamplingPercentage - Dependencies = this.Dependencies - InstanceKind = this.InstanceKind - Tags = this.Tags - } - ] + member this.BuildResources location = [ + { + Name = this.Name + Location = location + LinkedWebsite = None + DisableIpMasking = this.DisableIpMasking + SamplingPercentage = this.SamplingPercentage + Dependencies = this.Dependencies + InstanceKind = this.InstanceKind + Tags = this.Tags + } + ] type AppInsightsBuilder() = - member _.Yield _ = - { - Name = ResourceName.Empty - DisableIpMasking = false - SamplingPercentage = 100 - Tags = Map.empty - Dependencies = Set.empty - InstanceKind = Classic - } + member _.Yield _ = { + Name = ResourceName.Empty + DisableIpMasking = false + SamplingPercentage = 100 + Tags = Map.empty + Dependencies = Set.empty + InstanceKind = Classic + } [] /// Sets the name of the App Insights instance. @@ -81,18 +78,18 @@ type AppInsightsBuilder() = [] /// Sets the name of the App Insights instance. - member _.SamplingPercentage(state: AppInsightsConfig, samplingPercentage) = - { state with + 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 + 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) @@ -106,15 +103,15 @@ type AppInsightsBuilder() = state interface ITaggable with - member _.Add state tags = - { state with + member _.Add state tags = { + state with Tags = state.Tags |> Map.merge tags - } + } interface IDependable with - member _.Add state resources = - { state with + member _.Add state resources = { + state with Dependencies = state.Dependencies + resources - } + } let appInsights = AppInsightsBuilder() diff --git a/src/Farmer/Builders/Builders.ApplicationGateway.fs b/src/Farmer/Builders/Builders.ApplicationGateway.fs index a497f5350..837b798c8 100644 --- a/src/Farmer/Builders/Builders.ApplicationGateway.fs +++ b/src/Farmer/Builders/Builders.ApplicationGateway.fs @@ -8,21 +8,19 @@ open Farmer.PublicIpAddress open Farmer.Arm.ApplicationGateway open Farmer.ApplicationGateway -type GatewayIpConfig = - { - Name: ResourceName - Subnet: LinkedResource option - } - - static member BuildResource gatewayIp = - {| - Name = gatewayIp.Name - Subnet = - gatewayIp.Subnet - |> Option.map (function - | Managed resId -> resId - | Unmanaged resId -> resId) - |} +type GatewayIpConfig = { + Name: ResourceName + Subnet: LinkedResource option +} with + + static member BuildResource gatewayIp = {| + Name = gatewayIp.Name + Subnet = + gatewayIp.Subnet + |> Option.map (function + | Managed resId -> resId + | Unmanaged resId -> resId) + |} static member Dependencies gatewayIp = seq { @@ -35,50 +33,47 @@ type GatewayIpConfig = |> Set.ofSeq type GatewayIpBuilder() = - member _.Yield _ = - { - Name = ResourceName.Empty - Subnet = None - } + member _.Yield _ = { + Name = ResourceName.Empty + Subnet = None + } [] member _.Name(state: GatewayIpConfig, name) = { state with Name = ResourceName name } [] - member _.LinktoSubnet(state: GatewayIpConfig, vnet: ResourceName, subnet: ResourceName) = - { state with + member _.LinktoSubnet(state: GatewayIpConfig, vnet: ResourceName, subnet: ResourceName) = { + state with Subnet = Some(Unmanaged(subnets.resourceId (vnet, subnet))) - } + } - member _.LinktoSubnet(state: GatewayIpConfig, vnet: string, subnet: string) = - { state with + member _.LinktoSubnet(state: GatewayIpConfig, vnet: string, subnet: string) = { + state with Subnet = Some(Unmanaged(subnets.resourceId (ResourceName vnet, ResourceName subnet))) - } + } let gatewayIp = GatewayIpBuilder() // Subtle differences between load balancers -type FrontendIpConfig = - { - Name: ResourceName - PrivateIpAllocationMethod: PrivateIpAddress.AllocationMethod - PublicIp: LinkedResource option - } - - static member BuildResource frontend = - {| - Name = frontend.Name - PrivateIpAllocationMethod = frontend.PrivateIpAllocationMethod - PublicIp = - frontend.PublicIp - |> Option.map (function - | Managed resId -> resId - | Unmanaged resId -> resId) - |} +type FrontendIpConfig = { + Name: ResourceName + PrivateIpAllocationMethod: PrivateIpAddress.AllocationMethod + PublicIp: LinkedResource option +} with + + static member BuildResource frontend = {| + Name = frontend.Name + PrivateIpAllocationMethod = frontend.PrivateIpAllocationMethod + PublicIp = + frontend.PublicIp + |> Option.map (function + | Managed resId -> resId + | Unmanaged resId -> resId) + |} static member BuildIp (frontend: FrontendIpConfig) (location: Location) : PublicIpAddress option = match frontend.PublicIp with - | Some (Managed resId) -> + | Some(Managed resId) -> { Name = resId.Name AllocationMethod = AllocationMethod.Static @@ -92,12 +87,11 @@ type FrontendIpConfig = | _ -> None type FrontendIpBuilder() = - member _.Yield _ = - { - Name = ResourceName.Empty - PrivateIpAllocationMethod = PrivateIpAddress.DynamicPrivateIp - PublicIp = None - } + member _.Yield _ = { + Name = ResourceName.Empty + PrivateIpAllocationMethod = PrivateIpAddress.DynamicPrivateIp + PublicIp = None + } /// Sets the name of the frontend IP configuration. [] @@ -105,45 +99,42 @@ type FrontendIpBuilder() = /// Sets the frontend's private IP allocation method. [] - member _.PrivateIpAllocationMethod(state: FrontendIpConfig, allocationMethod) = - { state with + member _.PrivateIpAllocationMethod(state: FrontendIpConfig, allocationMethod) = { + state with PrivateIpAllocationMethod = allocationMethod - } + } /// Sets the name of the frontend public IP. [] - member _.PublicIp(state: FrontendIpConfig, publicIp) = - { state with + member _.PublicIp(state: FrontendIpConfig, publicIp) = { + state with PublicIp = Some(Managed(Farmer.Arm.Network.publicIPAddresses.resourceId (ResourceName publicIp))) - } + } /// Links the frontend to an existing public IP. [] - member _.LinkToPublicIp(state: FrontendIpConfig, publicIp: string) = - { state with + member _.LinkToPublicIp(state: FrontendIpConfig, publicIp: string) = { + state with PublicIp = Some(Unmanaged(virtualNetworks.resourceId (ResourceName publicIp))) - } + } let frontendIp = FrontendIpBuilder() -type FrontendPortConfig = - { - Name: ResourceName - Port: uint16 - } +type FrontendPortConfig = { + Name: ResourceName + Port: uint16 +} with - static member BuildResource frontendPort = - {| - Name = frontendPort.Name - Port = frontendPort.Port - |} + static member BuildResource frontendPort = {| + Name = frontendPort.Name + Port = frontendPort.Port + |} type FrontendPortBuilder() = - member _.Yield _ = - { - Name = ResourceName.Empty - Port = uint16 80 - } + member _.Yield _ = { + Name = ResourceName.Empty + Port = uint16 80 + } [] member _.Name(state: FrontendPortConfig, name) = { state with Name = ResourceName name } @@ -155,178 +146,171 @@ type FrontendPortBuilder() = let frontendPort = FrontendPortBuilder() -type HttpListenerConfig = - { - Name: ResourceName - FrontendIpConfiguration: ResourceName - BackendAddressPool: ResourceName - CustomErrorConfigurations: {| CustomErrorPageUrl: string - StatusCode: HttpStatusCode |} list - FirewallPolicy: ResourceId option - FrontendPort: ResourceName - RequireServerNameIndication: bool - HostNames: string list - Protocol: Protocol - SslCertificate: ResourceName option - SslProfile: ResourceName option - } - - static member BuildResource(listener: HttpListenerConfig) = +type HttpListenerConfig = { + Name: ResourceName + FrontendIpConfiguration: ResourceName + BackendAddressPool: ResourceName + CustomErrorConfigurations: {| - Name = listener.Name - BackendAddressPool = listener.BackendAddressPool - FrontendIpConfiguration = listener.FrontendIpConfiguration - FrontendPort = listener.FrontendPort - Protocol = listener.Protocol - HostNames = listener.HostNames - RequireServerNameIndication = listener.RequireServerNameIndication - CustomErrorConfigurations = listener.CustomErrorConfigurations - FirewallPolicy = listener.FirewallPolicy - SslCertificate = listener.SslCertificate - SslProfile = listener.SslProfile - |} + CustomErrorPageUrl: string + StatusCode: HttpStatusCode + |} list + FirewallPolicy: ResourceId option + FrontendPort: ResourceName + RequireServerNameIndication: bool + HostNames: string list + Protocol: Protocol + SslCertificate: ResourceName option + SslProfile: ResourceName option +} with + + static member BuildResource(listener: HttpListenerConfig) = {| + Name = listener.Name + BackendAddressPool = listener.BackendAddressPool + FrontendIpConfiguration = listener.FrontendIpConfiguration + FrontendPort = listener.FrontendPort + Protocol = listener.Protocol + HostNames = listener.HostNames + RequireServerNameIndication = listener.RequireServerNameIndication + CustomErrorConfigurations = listener.CustomErrorConfigurations + FirewallPolicy = listener.FirewallPolicy + SslCertificate = listener.SslCertificate + SslProfile = listener.SslProfile + |} type HttpListenerBuilder() = - member _.Yield _ = - { - Name = ResourceName.Empty - BackendAddressPool = ResourceName.Empty - FrontendIpConfiguration = ResourceName.Empty - FrontendPort = ResourceName.Empty - Protocol = Protocol.Http - HostNames = [] - RequireServerNameIndication = false - CustomErrorConfigurations = [] - FirewallPolicy = None - SslCertificate = None - SslProfile = None - } + member _.Yield _ = { + Name = ResourceName.Empty + BackendAddressPool = ResourceName.Empty + FrontendIpConfiguration = ResourceName.Empty + FrontendPort = ResourceName.Empty + Protocol = Protocol.Http + HostNames = [] + RequireServerNameIndication = false + CustomErrorConfigurations = [] + FirewallPolicy = None + SslCertificate = None + SslProfile = None + } [] member _.Name(state: HttpListenerConfig, name) = { state with Name = ResourceName name } [] - member _.BackendPool(state: HttpListenerConfig, backendPool: BackendAddressPoolConfig) = - { state with + member _.BackendPool(state: HttpListenerConfig, backendPool: BackendAddressPoolConfig) = { + state with BackendAddressPool = backendPool.Name - } + } - member _.BackendPool(state: HttpListenerConfig, backendPool: string) = - { state with + member _.BackendPool(state: HttpListenerConfig, backendPool: string) = { + state with BackendAddressPool = ResourceName backendPool - } + } [] - member _.FrontendIp(state: HttpListenerConfig, frontendIp: FrontendIpConfig) = - { state with + member _.FrontendIp(state: HttpListenerConfig, frontendIp: FrontendIpConfig) = { + state with FrontendIpConfiguration = frontendIp.Name - } + } - member _.FrontendIp(state: HttpListenerConfig, frontendIp: string) = - { state with + member _.FrontendIp(state: HttpListenerConfig, frontendIp: string) = { + state with FrontendIpConfiguration = ResourceName frontendIp - } + } [] - member _.FrontendPort(state: HttpListenerConfig, frontendPort: string) = - { state with + member _.FrontendPort(state: HttpListenerConfig, frontendPort: string) = { + state with FrontendPort = ResourceName frontendPort - } + } - member _.FrontendPort(state: HttpListenerConfig, frontendPort: FrontendPortConfig) = - { state with + member _.FrontendPort(state: HttpListenerConfig, frontendPort: FrontendPortConfig) = { + state with FrontendPort = frontendPort.Name - } + } [] member _.Protocol(state: HttpListenerConfig, protocol) = { state with Protocol = protocol } let httpListener = HttpListenerBuilder() -type BackendAddressConfig = - { - Fqdn: string - IpAddress: System.Net.IPAddress - } +type BackendAddressConfig = { + Fqdn: string + IpAddress: System.Net.IPAddress +} with - static member BuildResource backendAddress = - {| - Fqdn = backendAddress.Fqdn - IpAddress = backendAddress.IpAddress - |} + static member BuildResource backendAddress = {| + Fqdn = backendAddress.Fqdn + IpAddress = backendAddress.IpAddress + |} let backend_fqdn (fqdn: string) = BackendAddress.Fqdn fqdn let backend_ip_address (ip: string) = BackendAddress.Ip(System.Net.IPAddress.Parse ip) -type BackendAddressPoolConfig = - { - Name: ResourceName - BackendAddresses: BackendAddress list - } +type BackendAddressPoolConfig = { + Name: ResourceName + BackendAddresses: BackendAddress list +} type BackendAddressPoolBuilder() = - member _.Yield _ = - { - Name = ResourceName.Empty - BackendAddresses = [] - } + member _.Yield _ = { + Name = ResourceName.Empty + BackendAddresses = [] + } [] member _.Name(state: BackendAddressPoolConfig, name) = { state with Name = ResourceName name } [] - member _.BackendAddresses(state: BackendAddressPoolConfig, backendAddresses) = - { state with + member _.BackendAddresses(state: BackendAddressPoolConfig, backendAddresses) = { + state with BackendAddresses = state.BackendAddresses @ backendAddresses - } + } let appGatewayBackendAddressPool = BackendAddressPoolBuilder() -type AppGatewayProbeConfig = - { - Name: ResourceName - Host: string - Port: uint16 option - Path: string - Protocol: Protocol - IntervalInSeconds: int - TimeoutInSeconds: int - UnhealthyThreshold: uint16 - PickHostNameFromBackendHttpSettings: bool - MinServers: uint16 option - } - - static member BuildResource(probe: AppGatewayProbeConfig) = - {| - Name = probe.Name - Host = probe.Host - Port = probe.Port - Path = probe.Path - Protocol = probe.Protocol - IntervalInSeconds = probe.IntervalInSeconds - TimeoutInSeconds = probe.TimeoutInSeconds - UnhealthyThreshold = probe.UnhealthyThreshold - PickHostNameFromBackendHttpSettings = probe.PickHostNameFromBackendHttpSettings - MinServers = probe.MinServers - Match = None - |} +type AppGatewayProbeConfig = { + Name: ResourceName + Host: string + Port: uint16 option + Path: string + Protocol: Protocol + IntervalInSeconds: int + TimeoutInSeconds: int + UnhealthyThreshold: uint16 + PickHostNameFromBackendHttpSettings: bool + MinServers: uint16 option +} with + + static member BuildResource(probe: AppGatewayProbeConfig) = {| + Name = probe.Name + Host = probe.Host + Port = probe.Port + Path = probe.Path + Protocol = probe.Protocol + IntervalInSeconds = probe.IntervalInSeconds + TimeoutInSeconds = probe.TimeoutInSeconds + UnhealthyThreshold = probe.UnhealthyThreshold + PickHostNameFromBackendHttpSettings = probe.PickHostNameFromBackendHttpSettings + MinServers = probe.MinServers + Match = None + |} type AppGatewayProbeBuilder() = - member _.Yield _ = - { - Name = ResourceName.Empty - Host = "localhost" - Path = "/" - Port = None - Protocol = Protocol.Http - IntervalInSeconds = 30 - TimeoutInSeconds = 10 - UnhealthyThreshold = 3us - PickHostNameFromBackendHttpSettings = false - MinServers = None - } + member _.Yield _ = { + Name = ResourceName.Empty + Host = "localhost" + Path = "/" + Port = None + Protocol = Protocol.Http + IntervalInSeconds = 30 + TimeoutInSeconds = 10 + UnhealthyThreshold = 3us + PickHostNameFromBackendHttpSettings = false + MinServers = None + } [] member _.Name(state: AppGatewayProbeConfig, name) = { state with Name = ResourceName name } @@ -347,121 +331,115 @@ type AppGatewayProbeBuilder() = let appGatewayProbe = AppGatewayProbeBuilder() -type ConnectionDrainingConfig = - { - DrainTimeoutInSeconds: int - Enabled: bool - } +type ConnectionDrainingConfig = { + DrainTimeoutInSeconds: int + Enabled: bool +} with - static member BuildResource connDraining = - {| - DrainTimeoutInSeconds = connDraining.DrainTimeoutInSeconds - Enabled = connDraining.Enabled - |} + static member BuildResource connDraining = {| + DrainTimeoutInSeconds = connDraining.DrainTimeoutInSeconds + Enabled = connDraining.Enabled + |} type ConnectionDrainingBuilder() = - member _.Yield _ = - { - DrainTimeoutInSeconds = 0 - Enabled = false - } + member _.Yield _ = { + DrainTimeoutInSeconds = 0 + Enabled = false + } [] - member _.DrainTimeoutInSeconds(state: ConnectionDrainingConfig, timeout) = - { state with + member _.DrainTimeoutInSeconds(state: ConnectionDrainingConfig, timeout) = { + state with DrainTimeoutInSeconds = timeout - } + } [] member _.Enabled(state: ConnectionDrainingConfig, enabled) = { state with Enabled = enabled } let connectionDraining = ConnectionDrainingBuilder() -type BackendHttpSettingsConfig = - { - Name: ResourceName - AffinityCookieName: string option - AuthenticationCertificates: ResourceName list - ConnectionDraining: ConnectionDrainingConfig option - CookieBasedAffinity: FeatureFlag - HostName: string option - Path: string option - Port: uint16 - Protocol: Protocol - PickHostNameFromBackendAddress: bool - RequestTimeoutInSeconds: int - Probe: ResourceName option - ProbeEnabled: bool - TrustedRootCertificates: ResourceName list - } - - static member BuildResource backendHttpSettings = - {| - Name = backendHttpSettings.Name - AffinityCookieName = backendHttpSettings.AffinityCookieName - AuthenticationCertificates = backendHttpSettings.AuthenticationCertificates - ConnectionDraining = - backendHttpSettings.ConnectionDraining - |> Option.map ConnectionDrainingConfig.BuildResource - CookieBasedAffinity = backendHttpSettings.CookieBasedAffinity - HostName = backendHttpSettings.HostName - Path = backendHttpSettings.Path - Port = backendHttpSettings.Port - Protocol = backendHttpSettings.Protocol - PickHostNameFromBackendAddress = backendHttpSettings.PickHostNameFromBackendAddress - RequestTimeoutInSeconds = backendHttpSettings.RequestTimeoutInSeconds - Probe = backendHttpSettings.Probe - ProbeEnabled = backendHttpSettings.ProbeEnabled - TrustedRootCertificates = backendHttpSettings.TrustedRootCertificates - |} +type BackendHttpSettingsConfig = { + Name: ResourceName + AffinityCookieName: string option + AuthenticationCertificates: ResourceName list + ConnectionDraining: ConnectionDrainingConfig option + CookieBasedAffinity: FeatureFlag + HostName: string option + Path: string option + Port: uint16 + Protocol: Protocol + PickHostNameFromBackendAddress: bool + RequestTimeoutInSeconds: int + Probe: ResourceName option + ProbeEnabled: bool + TrustedRootCertificates: ResourceName list +} with + + static member BuildResource backendHttpSettings = {| + Name = backendHttpSettings.Name + AffinityCookieName = backendHttpSettings.AffinityCookieName + AuthenticationCertificates = backendHttpSettings.AuthenticationCertificates + ConnectionDraining = + backendHttpSettings.ConnectionDraining + |> Option.map ConnectionDrainingConfig.BuildResource + CookieBasedAffinity = backendHttpSettings.CookieBasedAffinity + HostName = backendHttpSettings.HostName + Path = backendHttpSettings.Path + Port = backendHttpSettings.Port + Protocol = backendHttpSettings.Protocol + PickHostNameFromBackendAddress = backendHttpSettings.PickHostNameFromBackendAddress + RequestTimeoutInSeconds = backendHttpSettings.RequestTimeoutInSeconds + Probe = backendHttpSettings.Probe + ProbeEnabled = backendHttpSettings.ProbeEnabled + TrustedRootCertificates = backendHttpSettings.TrustedRootCertificates + |} type BackendHttpSettingsBuilder() = - member _.Yield _ = - { - Name = ResourceName.Empty - AffinityCookieName = None - AuthenticationCertificates = [] - ConnectionDraining = None - CookieBasedAffinity = FeatureFlag.Disabled - HostName = None - Path = None - Port = 80us - Protocol = Protocol.Http - PickHostNameFromBackendAddress = false - RequestTimeoutInSeconds = 500 - Probe = None // ResourceName.Empty - ProbeEnabled = false - TrustedRootCertificates = [] - } + member _.Yield _ = { + Name = ResourceName.Empty + AffinityCookieName = None + AuthenticationCertificates = [] + ConnectionDraining = None + CookieBasedAffinity = FeatureFlag.Disabled + HostName = None + Path = None + Port = 80us + Protocol = Protocol.Http + PickHostNameFromBackendAddress = false + RequestTimeoutInSeconds = 500 + Probe = None // ResourceName.Empty + ProbeEnabled = false + TrustedRootCertificates = [] + } [] member _.Name(state: BackendHttpSettingsConfig, name) = { state with Name = ResourceName name } [] - member _.AffinityCookieName(state: BackendHttpSettingsConfig, name) = - { state with + member _.AffinityCookieName(state: BackendHttpSettingsConfig, name) = { + state with AffinityCookieName = Some name - } + } [] - member _.AddAuthCerts(state: BackendHttpSettingsConfig, authCerts) = - { state with + member _.AddAuthCerts(state: BackendHttpSettingsConfig, authCerts) = { + state with AuthenticationCertificates = state.AuthenticationCertificates @ (authCerts |> List.map (fun authCert -> ResourceName authCert)) - } + } [] - member _.ConnectionDraining(state: BackendHttpSettingsConfig, connDraining) = - { state with + member _.ConnectionDraining(state: BackendHttpSettingsConfig, connDraining) = { + state with ConnectionDraining = Some connDraining - } + } [] - member _.CookieBasedAffinity(state: BackendHttpSettingsConfig, cookieBasedAffinity) = - { state with + member _.CookieBasedAffinity(state: BackendHttpSettingsConfig, cookieBasedAffinity) = { + state with CookieBasedAffinity = cookieBasedAffinity - } + } [] member _.HostName(state: BackendHttpSettingsConfig, name) = { state with HostName = Some name } @@ -478,136 +456,134 @@ type BackendHttpSettingsBuilder() = member _.Protocol(state: BackendHttpSettingsConfig, protocol) = { state with Protocol = protocol } [] - member _.PickHostNameFromBackendAddress(state: BackendHttpSettingsConfig, pickHostNameFromBackendAddress) = - { state with + member _.PickHostNameFromBackendAddress(state: BackendHttpSettingsConfig, pickHostNameFromBackendAddress) = { + state with PickHostNameFromBackendAddress = pickHostNameFromBackendAddress - } + } [] - member _.RequestTimeoutInSeconds(state: BackendHttpSettingsConfig, reqTimeout) = - { state with + member _.RequestTimeoutInSeconds(state: BackendHttpSettingsConfig, reqTimeout) = { + state with RequestTimeoutInSeconds = reqTimeout - } + } [] - member _.Probe(state: BackendHttpSettingsConfig, probe: string) = - { state with + member _.Probe(state: BackendHttpSettingsConfig, probe: string) = { + state with Probe = Some(ResourceName probe) - } + } - member _.Probe(state: BackendHttpSettingsConfig, probe: AppGatewayProbeConfig) = - { state with Probe = Some probe.Name } + member _.Probe(state: BackendHttpSettingsConfig, probe: AppGatewayProbeConfig) = { + state with + Probe = Some probe.Name + } [] - member _.ProbeEnabled(state: BackendHttpSettingsConfig, probeEnabled) = - { state with + member _.ProbeEnabled(state: BackendHttpSettingsConfig, probeEnabled) = { + state with ProbeEnabled = probeEnabled - } + } [] - member _.TrustedRootCertificates(state: BackendHttpSettingsConfig, trustedRootCerts) = - { state with + member _.TrustedRootCertificates(state: BackendHttpSettingsConfig, trustedRootCerts) = { + state with TrustedRootCertificates = state.TrustedRootCertificates @ (trustedRootCerts |> List.map (fun rootCert -> ResourceName rootCert)) - } + } let backendHttpSettings = BackendHttpSettingsBuilder() -type RequestRoutingRuleConfig = - { - Name: ResourceName - RuleType: RuleType - HttpListener: ResourceName - BackendAddressPool: ResourceName - BackendHttpSettings: ResourceName - RedirectConfiguration: ResourceName option - RewriteRuleSet: ResourceName option - UrlPathMap: ResourceName option - Priority: int option - } - - static member BuildResource(rule: RequestRoutingRuleConfig) = - {| - Name = rule.Name - RuleType = rule.RuleType - HttpListener = rule.HttpListener - BackendAddressPool = rule.BackendAddressPool - BackendHttpSettings = rule.BackendHttpSettings - RedirectConfiguration = rule.RedirectConfiguration - RewriteRuleSet = rule.RewriteRuleSet - UrlPathMap = rule.UrlPathMap - Priority = rule.Priority - |} +type RequestRoutingRuleConfig = { + Name: ResourceName + RuleType: RuleType + HttpListener: ResourceName + BackendAddressPool: ResourceName + BackendHttpSettings: ResourceName + RedirectConfiguration: ResourceName option + RewriteRuleSet: ResourceName option + UrlPathMap: ResourceName option + Priority: int option +} with + + static member BuildResource(rule: RequestRoutingRuleConfig) = {| + Name = rule.Name + RuleType = rule.RuleType + HttpListener = rule.HttpListener + BackendAddressPool = rule.BackendAddressPool + BackendHttpSettings = rule.BackendHttpSettings + RedirectConfiguration = rule.RedirectConfiguration + RewriteRuleSet = rule.RewriteRuleSet + UrlPathMap = rule.UrlPathMap + Priority = rule.Priority + |} type BasicRequestRoutingRuleBuilder() = - member _.Yield _ = - { - Name = ResourceName.Empty - RuleType = RuleType.Basic - HttpListener = ResourceName.Empty - BackendAddressPool = ResourceName.Empty - BackendHttpSettings = ResourceName.Empty - RedirectConfiguration = None - RewriteRuleSet = None - UrlPathMap = None - Priority = None - } + member _.Yield _ = { + Name = ResourceName.Empty + RuleType = RuleType.Basic + HttpListener = ResourceName.Empty + BackendAddressPool = ResourceName.Empty + BackendHttpSettings = ResourceName.Empty + RedirectConfiguration = None + RewriteRuleSet = None + UrlPathMap = None + Priority = None + } [] member _.Name(state: RequestRoutingRuleConfig, name) = { state with Name = ResourceName name } [] - member _.HttpListener(state: RequestRoutingRuleConfig, listener: string) = - { state with + member _.HttpListener(state: RequestRoutingRuleConfig, listener: string) = { + state with HttpListener = ResourceName listener - } + } - member _.HttpListener(state: RequestRoutingRuleConfig, listener: HttpListenerConfig) = - { state with + member _.HttpListener(state: RequestRoutingRuleConfig, listener: HttpListenerConfig) = { + state with HttpListener = listener.Name - } + } [] - member _.BackendAddressPool(state: RequestRoutingRuleConfig, backendAddressPool: string) = - { state with + member _.BackendAddressPool(state: RequestRoutingRuleConfig, backendAddressPool: string) = { + state with BackendAddressPool = ResourceName backendAddressPool - } + } - member _.BackendAddressPool(state: RequestRoutingRuleConfig, backendAddressPool: BackendAddressPoolConfig) = - { state with + member _.BackendAddressPool(state: RequestRoutingRuleConfig, backendAddressPool: BackendAddressPoolConfig) = { + state with BackendAddressPool = backendAddressPool.Name - } + } [] - member _.BackendHttpSettings(state: RequestRoutingRuleConfig, httpSettings: string) = - { state with + member _.BackendHttpSettings(state: RequestRoutingRuleConfig, httpSettings: string) = { + state with BackendHttpSettings = ResourceName httpSettings - } + } - member _.BackendHttpSettings(state: RequestRoutingRuleConfig, httpSettings: BackendHttpSettingsConfig) = - { state with + member _.BackendHttpSettings(state: RequestRoutingRuleConfig, httpSettings: BackendHttpSettingsConfig) = { + state with BackendHttpSettings = httpSettings.Name - } + } let basicRequestRoutingRule = BasicRequestRoutingRuleBuilder() -type AppGatewayConfig = - { - Name: ResourceName - Sku: ApplicationGatewaySku - Identity: Identity.ManagedIdentity - GatewayIpConfigs: GatewayIpConfig list - FrontendIpConfigs: FrontendIpConfig list - FrontendPorts: FrontendPortConfig list - BackendAddressPools: BackendAddressPoolConfig list - BackendHttpSettingsCollection: BackendHttpSettingsConfig list - HttpListeners: HttpListenerConfig list - Probes: AppGatewayProbeConfig list - RequestRoutingRules: RequestRoutingRuleConfig list - Dependencies: Set - Tags: Map - } +type AppGatewayConfig = { + Name: ResourceName + Sku: ApplicationGatewaySku + Identity: Identity.ManagedIdentity + GatewayIpConfigs: GatewayIpConfig list + FrontendIpConfigs: FrontendIpConfig list + FrontendPorts: FrontendPortConfig list + BackendAddressPools: BackendAddressPoolConfig list + BackendHttpSettingsCollection: BackendHttpSettingsConfig list + HttpListeners: HttpListenerConfig list + Probes: AppGatewayProbeConfig list + RequestRoutingRules: RequestRoutingRuleConfig list + Dependencies: Set + Tags: Map +} with interface IBuilder with member this.ResourceId = applicationGateways.resourceId this.Name @@ -628,11 +604,10 @@ type AppGatewayConfig = FrontendPorts = this.FrontendPorts |> List.map FrontendPortConfig.BuildResource BackendAddressPools = this.BackendAddressPools - |> List.map (fun p -> - {| - Name = p.Name - Addresses = p.BackendAddresses - |}) + |> List.map (fun p -> {| + Name = p.Name + Addresses = p.BackendAddresses + |}) BackendHttpSettingsCollection = this.BackendHttpSettingsCollection |> List.map BackendHttpSettingsConfig.BuildResource @@ -672,93 +647,91 @@ type AppGatewayConfig = type AppGatewayBuilder() = - member _.Yield _ : AppGatewayConfig = - { - Name = ResourceName.Empty - Sku = - { - Name = Sku.Standard_v2 - Capacity = None - Tier = Tier.Standard_v2 - } - Identity = Identity.ManagedIdentity.Empty - GatewayIpConfigs = [] - FrontendIpConfigs = [] - FrontendPorts = [] - BackendAddressPools = [] - BackendHttpSettingsCollection = [] - HttpListeners = [] - Probes = [] - RequestRoutingRules = [] - Dependencies = Set.empty - Tags = Map.empty - } + member _.Yield _ : AppGatewayConfig = { + Name = ResourceName.Empty + Sku = { + Name = Sku.Standard_v2 + Capacity = None + Tier = Tier.Standard_v2 + } + Identity = Identity.ManagedIdentity.Empty + GatewayIpConfigs = [] + FrontendIpConfigs = [] + FrontendPorts = [] + BackendAddressPools = [] + BackendHttpSettingsCollection = [] + HttpListeners = [] + Probes = [] + RequestRoutingRules = [] + Dependencies = Set.empty + Tags = Map.empty + } [] member _.Name(state: AppGatewayConfig, name) = { state with Name = ResourceName name } [] - member _.Sku(state: AppGatewayConfig, skuName) = - { state with + member _.Sku(state: AppGatewayConfig, skuName) = { + state with Sku = { state.Sku with Name = skuName } - } + } [] - member _.Tier(state: AppGatewayConfig, skuTier) = - { state with + member _.Tier(state: AppGatewayConfig, skuTier) = { + state with Sku = { state.Sku with Tier = skuTier } - } + } [] - member _.SkuCapacity(state: AppGatewayConfig, skuCapacity) = - { state with - Sku = - { state.Sku with + member _.SkuCapacity(state: AppGatewayConfig, skuCapacity) = { + state with + Sku = { + state.Sku with Capacity = Some skuCapacity - } - } + } + } /// Sets the managed identity on this Application Gateway. interface IIdentity with - member _.Add state updater = - { state with + member _.Add state updater = { + state with Identity = updater state.Identity - } + } /// Support for adding tags to this Application Gateway. interface ITaggable with - member _.Add state tags = - { state with + member _.Add state tags = { + state with Tags = state.Tags |> Map.merge tags - } + } /// Support for adding dependencies to this Application Gateway. interface IDependable with - member _.Add state newDeps = - { state with + member _.Add state newDeps = { + state with Dependencies = state.Dependencies + newDeps - } + } [] - member _.AddIpConfigs(state: AppGatewayConfig, ipConfigs) = - { state with + member _.AddIpConfigs(state: AppGatewayConfig, ipConfigs) = { + state with GatewayIpConfigs = state.GatewayIpConfigs @ ipConfigs - } + } [] - member _.AddFrontends(state: AppGatewayConfig, frontends) = - { state with + member _.AddFrontends(state: AppGatewayConfig, frontends) = { + state with FrontendIpConfigs = state.FrontendIpConfigs @ frontends - } + } [] - member _.AddFrontendPorts(state: AppGatewayConfig, frontendPorts) = - { state with + member _.AddFrontendPorts(state: AppGatewayConfig, frontendPorts) = { + state with FrontendPorts = state.FrontendPorts @ frontendPorts - } + } [] - member _.AddBackendAddressPools(state: AppGatewayConfig, backendAddressPools) = - { state with + member _.AddBackendAddressPools(state: AppGatewayConfig, backendAddressPools) = { + state with BackendAddressPools = state.BackendAddressPools @ backendAddressPools - } + } [] member _.AddBackendHttpSettingsCollection @@ -766,26 +739,27 @@ type AppGatewayBuilder() = state: AppGatewayConfig, backendHttpSettings: BackendHttpSettingsConfig list ) = - { state with - BackendHttpSettingsCollection = state.BackendHttpSettingsCollection @ backendHttpSettings + { + state with + BackendHttpSettingsCollection = state.BackendHttpSettingsCollection @ backendHttpSettings } [] - member _.AddHttpListeners(state: AppGatewayConfig, httpListeners: HttpListenerConfig list) = - { state with + member _.AddHttpListeners(state: AppGatewayConfig, httpListeners: HttpListenerConfig list) = { + state with HttpListeners = state.HttpListeners @ httpListeners - } + } [] - member _.AddProbes(state: AppGatewayConfig, probes) = - { state with + member _.AddProbes(state: AppGatewayConfig, probes) = { + state with Probes = state.Probes @ probes - } + } [] - member _.AddRequestRoutingRules(state: AppGatewayConfig, reqRoutingRules: RequestRoutingRuleConfig list) = - { state with + member _.AddRequestRoutingRules(state: AppGatewayConfig, reqRoutingRules: RequestRoutingRuleConfig list) = { + state with RequestRoutingRules = state.RequestRoutingRules @ reqRoutingRules - } + } let appGateway = AppGatewayBuilder() diff --git a/src/Farmer/Builders/Builders.AvailabilityTest.fs b/src/Farmer/Builders/Builders.AvailabilityTest.fs index 592c4ea4f..06413f532 100644 --- a/src/Farmer/Builders/Builders.AvailabilityTest.fs +++ b/src/Farmer/Builders/Builders.AvailabilityTest.fs @@ -4,42 +4,39 @@ module Farmer.Builders.AppInsightsAvailabilityTest open Farmer open Farmer.Arm.InsightsAvailabilityTest -type AvailabilityTestProperties = - { - Name: ResourceName - AppInsightsName: ResourceName - Timeout: int - VisitFrequency: int - Locations: AvailabilityTest.TestSiteLocation list - WebTest: AvailabilityTest.WebTestType option - } +type AvailabilityTestProperties = { + Name: ResourceName + AppInsightsName: ResourceName + Timeout: int + VisitFrequency: int + Locations: AvailabilityTest.TestSiteLocation list + WebTest: AvailabilityTest.WebTestType option +} with interface IBuilder with member this.ResourceId = availabilitytest.resourceId this.Name - member this.BuildResources location = - [ - { - Name = this.Name - AppInsightsName = this.AppInsightsName - Location = location - Timeout = this.Timeout - VisitFrequency = this.VisitFrequency - Locations = this.Locations - WebTest = this.WebTest - } - ] + member this.BuildResources location = [ + { + Name = this.Name + AppInsightsName = this.AppInsightsName + Location = location + Timeout = this.Timeout + VisitFrequency = this.VisitFrequency + Locations = this.Locations + WebTest = this.WebTest + } + ] type AvailabilityTestBuilder() = - member _.Yield _ = - { - Name = ResourceName.Empty - AppInsightsName = ResourceName.Empty - Timeout = 120 - VisitFrequency = 900 - Locations = List.empty - WebTest = None - } + member _.Yield _ = { + Name = ResourceName.Empty + AppInsightsName = ResourceName.Empty + Timeout = 120 + VisitFrequency = 900 + Locations = List.empty + WebTest = None + } [] /// Sets the name of this Webtest instance. @@ -51,10 +48,10 @@ type AvailabilityTestBuilder() = [] /// Frequency how often the test is run. Default: 900 seconds. - member _.Frequency(state: AvailabilityTestProperties, frequency) = - { state with + member _.Frequency(state: AvailabilityTestProperties, frequency) = { + state with VisitFrequency = frequency - } + } [] /// List of locations where the site is pinged. These are not format of Farmer.Location but AvailabilityTest.TestSiteLocation. diff --git a/src/Farmer/Builders/Builders.AzureFirewall.fs b/src/Farmer/Builders/Builders.AzureFirewall.fs index f59feb32f..7787aae73 100644 --- a/src/Farmer/Builders/Builders.AzureFirewall.fs +++ b/src/Farmer/Builders/Builders.AzureFirewall.fs @@ -12,74 +12,68 @@ type HubIPAddressSpace = member this.Arm = match this with - | PublicCount count -> - { - PublicIPAddresses = { Count = count; Addresses = [] } |> Some - } - -type AzureFirewallConfig = - { - Name: ResourceName - FirewallPolicy: LinkedResource option - VirtualHub: LinkedResource option - HubIPAddressSpace: HubIPAddressSpace option - Sku: Sku - AvailabilityZones: string list - Dependencies: ResourceId Set - } + | PublicCount count -> { + PublicIPAddresses = { Count = count; Addresses = [] } |> Some + } + +type AzureFirewallConfig = { + Name: ResourceName + FirewallPolicy: LinkedResource option + VirtualHub: LinkedResource option + HubIPAddressSpace: HubIPAddressSpace option + Sku: Sku + AvailabilityZones: string list + Dependencies: ResourceId Set +} with interface IBuilder with member this.ResourceId = azureFirewalls.resourceId this.Name - member this.BuildResources location = - [ - { - Name = this.Name - Location = location - Dependencies = - Set - [ - yield! this.Dependencies - match this.FirewallPolicy with - | Some (Managed resId) -> resId // Only generate dependency if this is managed by Farmer (same template) - | _ -> () - match this.VirtualHub with - | Some (Managed resId) -> resId - | _ -> () - ] - 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 - AvailabilityZones = this.AvailabilityZones - } - ] + member this.BuildResources location = [ + { + Name = this.Name + Location = location + Dependencies = + Set [ + yield! this.Dependencies + match this.FirewallPolicy with + | Some(Managed resId) -> resId // Only generate dependency if this is managed by Farmer (same template) + | _ -> () + match this.VirtualHub with + | Some(Managed resId) -> resId + | _ -> () + ] + 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 + AvailabilityZones = this.AvailabilityZones + } + ] type AzureFirewallBuilder() = - member _.Yield _ = - { - Name = ResourceName.Empty - Sku = - { - Name = SkuName.AZFW_Hub - Tier = SkuTier.Standard - } - FirewallPolicy = None - VirtualHub = None - HubIPAddressSpace = None - AvailabilityZones = List.empty - Dependencies = Set.empty + member _.Yield _ = { + Name = ResourceName.Empty + Sku = { + Name = SkuName.AZFW_Hub + Tier = SkuTier.Standard } + FirewallPolicy = None + VirtualHub = None + HubIPAddressSpace = None + AvailabilityZones = List.empty + Dependencies = Set.empty + } /// The name of the firewall. [] @@ -87,60 +81,59 @@ type AzureFirewallBuilder() = /// The SKU of the firewall. [] - member _.Sku(state: AzureFirewallConfig, name, tier) = - { state with + member _.Sku(state: AzureFirewallConfig, name, tier) = { + state with Sku = { Name = name; Tier = tier } - } + } /// Configure this firewall to use an unmanaged firewall policy. [] - member this.LinkToUnmanagedFirewallPolicy(state: AzureFirewallConfig, resourceId) = - { state with + member this.LinkToUnmanagedFirewallPolicy(state: AzureFirewallConfig, resourceId) = { + state with FirewallPolicy = Some(Unmanaged resourceId) - } + } /// Configure this firewall to use a managed firewall policy. [] - member this.LinkToFirewallPolicy(state: AzureFirewallConfig, firewallPolicy: IArmResource) = - { state with + member this.LinkToFirewallPolicy(state: AzureFirewallConfig, firewallPolicy: IArmResource) = { + state with FirewallPolicy = Some(Managed firewallPolicy.ResourceId) - } + } - member this.LinkToFirewallPolicy(state: AzureFirewallConfig, firewallPolicy: IBuilder) = - { state with + member this.LinkToFirewallPolicy(state: AzureFirewallConfig, firewallPolicy: IBuilder) = { + state with FirewallPolicy = Some(Managed firewallPolicy.ResourceId) - } + } /// The unmanaged virtualHub to which the firewall belongs. [] - member this.LinkToUnmanagedVirtualHub(state: AzureFirewallConfig, resourceId) = - { state with + member this.LinkToUnmanagedVirtualHub(state: AzureFirewallConfig, resourceId) = { + state with VirtualHub = Some(Unmanaged resourceId) - } + } /// The managed virtualHub to which the firewall belongs [] - member this.LinkToVirtualHub(state: AzureFirewallConfig, vhub: VirtualHub) = - { state with + member this.LinkToVirtualHub(state: AzureFirewallConfig, vhub: VirtualHub) = { + state with VirtualHub = Some(Managed (vhub :> IArmResource).ResourceId) - } + } - member this.LinkToVirtualHub(state: AzureFirewallConfig, vhub: VirtualHubConfig) = - { state with + member this.LinkToVirtualHub(state: AzureFirewallConfig, vhub: VirtualHubConfig) = { + state with VirtualHub = Some(Managed (vhub :> IBuilder).ResourceId) - } + } /// Configure this firewall to reserve a specified number of public ips. /// 0 is not a valid value for AZFW_HUB [] - member _.PublicIpReservationCount(state: AzureFirewallConfig, count) = - { state with + member _.PublicIpReservationCount(state: AzureFirewallConfig, count) = { + state with HubIPAddressSpace = Some(HubIPAddressSpace.PublicCount count) - } + } [] - member _.AvailabilityZones(state: AzureFirewallConfig, zones) = - { state with AvailabilityZones = zones } + member _.AvailabilityZones(state: AzureFirewallConfig, zones) = { state with AvailabilityZones = zones } member _.Run(state: AzureFirewallConfig) = let stateIBuilder = state :> IBuilder @@ -151,18 +144,18 @@ type AzureFirewallBuilder() = | None -> raiseFarmer $"Sku AZFW_Hub requires Public IPs provided for Azure Firewall {stateIBuilder.ResourceId}. Please specify valid IPs count (count cannot be zero) and/or Public IPs to be retained (in case of deleting IPs). Some Public IPs specified may be incorrect, please specify the IPs that are linked to the firewall" - | Some (PublicCount 0) -> + | Some(PublicCount 0) -> raiseFarmer $"Sku AZFW_Hub requires Public IPs count be > 0 for Azure Firewall {stateIBuilder.ResourceId}" - | Some (PublicCount _) -> () + | Some(PublicCount _) -> () | AZFW_VNet -> () state interface IDependable with - member _.Add state newDeps = - { state with + member _.Add state newDeps = { + state with Dependencies = state.Dependencies + newDeps - } + } let azureFirewall = AzureFirewallBuilder() diff --git a/src/Farmer/Builders/Builders.Bastion.fs b/src/Farmer/Builders/Builders.Bastion.fs index 869bc44da..9bfa12292 100644 --- a/src/Farmer/Builders/Builders.Bastion.fs +++ b/src/Farmer/Builders/Builders.Bastion.fs @@ -6,12 +6,11 @@ open Farmer.Arm.Bastion open Farmer.Arm.Network open Farmer.PublicIpAddress -type BastionConfig = - { - Name: ResourceName - VirtualNetwork: ResourceName - Tags: Map - } +type BastionConfig = { + Name: ResourceName + VirtualNetwork: ResourceName + Tags: Map +} with interface IBuilder with member this.ResourceId = bastionHosts.resourceId this.Name @@ -41,12 +40,11 @@ type BastionConfig = ] type BastionBuilder() = - member _.Yield _ = - { - Name = ResourceName.Empty - VirtualNetwork = ResourceName.Empty - Tags = Map.empty - } + member _.Yield _ = { + Name = ResourceName.Empty + VirtualNetwork = ResourceName.Empty + Tags = Map.empty + } /// Sets the name of the bastion host. [] @@ -54,9 +52,9 @@ type BastionBuilder() = /// Sets the virtual network where this bastion host is attached. [] - member _.VNet(state: BastionConfig, vnet) = - { state with + member _.VNet(state: BastionConfig, vnet) = { + state with VirtualNetwork = ResourceName vnet - } + } let bastion = BastionBuilder() diff --git a/src/Farmer/Builders/Builders.BingSearch.fs b/src/Farmer/Builders/Builders.BingSearch.fs index b3fd76907..2d016ac34 100644 --- a/src/Farmer/Builders/Builders.BingSearch.fs +++ b/src/Farmer/Builders/Builders.BingSearch.fs @@ -13,13 +13,12 @@ type BingSearch = static member getKey(name: ResourceName) = BingSearch.getKey (accounts.resourceId name) -type BingSearchConfig = - { - Name: ResourceName - Sku: Sku - Tags: Map - Statistics: FeatureFlag - } +type BingSearchConfig = { + Name: ResourceName + Sku: Sku + Tags: Map + Statistics: FeatureFlag +} with /// Gets an ARM expression to the key of this Bing Search instance. member this.Key = BingSearch.getKey (accounts.resourceId this.Name) @@ -27,25 +26,23 @@ type BingSearchConfig = interface IBuilder with member this.ResourceId = accounts.resourceId this.Name - member this.BuildResources location = - [ - { - Name = this.Name - Location = location - Sku = this.Sku - Tags = this.Tags - Statistics = this.Statistics - } - ] + member this.BuildResources location = [ + { + Name = this.Name + Location = location + Sku = this.Sku + Tags = this.Tags + Statistics = this.Statistics + } + ] type BingSearchBuilder() = - member _.Yield _ = - { - Name = ResourceName.Empty - Sku = F1 - Tags = Map.empty - Statistics = FeatureFlag.Disabled - } + member _.Yield _ = { + Name = ResourceName.Empty + Sku = F1 + Tags = Map.empty + Statistics = FeatureFlag.Disabled + } [] member _.Name(state: BingSearchConfig, name) = { state with Name = ResourceName name } @@ -57,9 +54,9 @@ type BingSearchBuilder() = member _.EnableStatistics(state: BingSearchConfig, value) = { state with Statistics = value } interface ITaggable with - member _.Add state tags = - { state with + member _.Add state tags = { + state with Tags = state.Tags |> Map.merge tags - } + } let bingSearch = BingSearchBuilder() diff --git a/src/Farmer/Builders/Builders.Cdn.fs b/src/Farmer/Builders/Builders.Cdn.fs index 2e11b8e1b..6ab76141d 100644 --- a/src/Farmer/Builders/Builders.Cdn.fs +++ b/src/Farmer/Builders/Builders.Cdn.fs @@ -9,95 +9,87 @@ open Farmer.Cdn open System open CdnRule -type CdnRuleConfig = - { - Name: ResourceName - Order: int - Conditions: Condition list - Actions: Action list - } - -type EndpointConfig = - { - Name: ResourceName - Dependencies: ResourceId Set - CompressedContentTypes: string Set - QueryStringCachingBehaviour: QueryStringCachingBehaviour - Http: FeatureFlag - Https: FeatureFlag - Compression: FeatureFlag - Origin: ArmExpression - CustomDomain: string option - OptimizationType: OptimizationType - DeliveryPolicyDescription: string - Rules: CdnRuleConfig list - } - -type CdnConfig = - { - Name: ResourceName - Sku: Sku - Endpoints: EndpointConfig list - Tags: Map - } +type CdnRuleConfig = { + Name: ResourceName + Order: int + Conditions: Condition list + Actions: Action list +} + +type EndpointConfig = { + Name: ResourceName + Dependencies: ResourceId Set + CompressedContentTypes: string Set + QueryStringCachingBehaviour: QueryStringCachingBehaviour + Http: FeatureFlag + Https: FeatureFlag + Compression: FeatureFlag + Origin: ArmExpression + CustomDomain: string option + OptimizationType: OptimizationType + DeliveryPolicyDescription: string + Rules: CdnRuleConfig list +} + +type CdnConfig = { + Name: ResourceName + Sku: Sku + Endpoints: EndpointConfig list + Tags: Map +} with interface IBuilder with member this.ResourceId = profiles.resourceId this.Name - member this.BuildResources _ = - [ + member this.BuildResources _ = [ + { + Name = this.Name + Sku = this.Sku + Tags = this.Tags + } + for endpoint in this.Endpoints do { - Name = this.Name - Sku = this.Sku + Name = endpoint.Name + Profile = this.Name + Dependencies = endpoint.Dependencies + CompressedContentTypes = endpoint.CompressedContentTypes + QueryStringCachingBehaviour = endpoint.QueryStringCachingBehaviour + Http = endpoint.Http + Https = endpoint.Https + Compression = endpoint.Compression + Origin = endpoint.Origin + OptimizationType = endpoint.OptimizationType Tags = this.Tags - } - for endpoint in this.Endpoints do - { - Name = endpoint.Name - Profile = this.Name - Dependencies = endpoint.Dependencies - CompressedContentTypes = endpoint.CompressedContentTypes - QueryStringCachingBehaviour = endpoint.QueryStringCachingBehaviour - Http = endpoint.Http - Https = endpoint.Https - Compression = endpoint.Compression - Origin = endpoint.Origin - OptimizationType = endpoint.OptimizationType - Tags = this.Tags - DeliveryPolicy = - { - Description = endpoint.DeliveryPolicyDescription - Rules = - endpoint.Rules - |> List.map (fun r -> - { - Name = r.Name - Order = r.Order - Conditions = r.Conditions - Actions = r.Actions - }) - } + DeliveryPolicy = { + Description = endpoint.DeliveryPolicyDescription + Rules = + endpoint.Rules + |> List.map (fun r -> { + Name = r.Name + Order = r.Order + Conditions = r.Conditions + Actions = r.Actions + }) } + } - match endpoint.CustomDomain with - | Some customDomain -> - { - Name = endpoint.Name.Map(sprintf "%sdomain") - Profile = this.Name - Endpoint = endpoint.Name - Hostname = customDomain - } - | None -> () - ] + match endpoint.CustomDomain with + | Some customDomain -> { + Name = endpoint.Name.Map(sprintf "%sdomain") + Profile = this.Name + Endpoint = endpoint.Name + Hostname = customDomain + } + | None -> () + ] type CdnBuilder() = - member _.Yield _ = - { - Name = ResourceName.Empty - Sku = Standard_Akamai - Endpoints = [] - Tags = Map.empty - } + member _.Yield _ = { + Name = ResourceName.Empty + Sku = Standard_Akamai + Endpoints = [] + Tags = Map.empty + } [] member _.Name(state: CdnConfig, name) = { state with Name = ResourceName name } @@ -106,40 +98,39 @@ type CdnBuilder() = member _.Sku(state: CdnConfig, sku) = { state with Sku = sku } [] - member _.AddEndpoints(state: CdnConfig, endpoints) = - { state with + member _.AddEndpoints(state: CdnConfig, endpoints) = { + state with Endpoints = state.Endpoints @ endpoints - } + } interface ITaggable with - member _.Add state tags = - { state with + member _.Add state tags = { + state with Tags = state.Tags |> Map.merge tags - } + } type EndpointBuilder() = interface IDependable with - member _.Add state newDeps = - { state with + member _.Add state newDeps = { + state with Dependencies = state.Dependencies + newDeps - } - - member _.Yield _ : EndpointConfig = - { - Name = ResourceName.Empty - Dependencies = Set.empty - CompressedContentTypes = Set.empty - QueryStringCachingBehaviour = UseQueryString - Http = Enabled - Https = Enabled - Compression = Disabled - Origin = ArmExpression.Empty - CustomDomain = None - OptimizationType = GeneralWebDelivery - DeliveryPolicyDescription = "" - Rules = [] } + member _.Yield _ : EndpointConfig = { + Name = ResourceName.Empty + Dependencies = Set.empty + CompressedContentTypes = Set.empty + QueryStringCachingBehaviour = UseQueryString + Http = Enabled + Https = Enabled + Compression = Disabled + Origin = ArmExpression.Empty + CustomDomain = None + OptimizationType = GeneralWebDelivery + DeliveryPolicyDescription = "" + Rules = [] + } + /// Name of the endpoint within the CDN. [] member _.Name(state: EndpointConfig, name) = { state with Name = name } @@ -148,15 +139,15 @@ type EndpointBuilder() = /// The address of the origin. [] - member _.Origin(state: EndpointConfig, name: ArmExpression) = - { state with + member _.Origin(state: EndpointConfig, name: ArmExpression) = { + state with Name = state.Name.IfEmpty( (name.Value |> Seq.filter Char.IsLetterOrDigit |> Seq.toArray |> String) + "-endpoint" ) Origin = name - } + } member this.Origin(state: EndpointConfig, name) = this.Origin(state, ArmExpression.literal name) @@ -166,18 +157,18 @@ type EndpointBuilder() = /// Adds a list of MIME content types on which compression applies. [] - member _.AddCompressedContentTypes(state: EndpointConfig, types) = - { state with + member _.AddCompressedContentTypes(state: EndpointConfig, types) = { + state with CompressedContentTypes = state.CompressedContentTypes + Set types Compression = Enabled - } + } /// Defines how CDN caches requests that include query strings. [] - member _.QueryStringCachingBehaviour(state: EndpointConfig, behaviour) = - { state with + member _.QueryStringCachingBehaviour(state: EndpointConfig, behaviour) = { + state with QueryStringCachingBehaviour = behaviour - } + } [] member _.EnableHttp(state: EndpointConfig) = { state with Http = Enabled } @@ -193,45 +184,44 @@ type EndpointBuilder() = /// Name of the custom domain hostname. [] - member _.CustomDomain(state: EndpointConfig, hostname) = - { state with + member _.CustomDomain(state: EndpointConfig, hostname) = { + state with CustomDomain = Some hostname - } + } /// Specifies what scenario the customer wants this CDN endpoint to optimise for. [] - member _.OptimiseFor(state: EndpointConfig, optimizationType) = - { state with + member _.OptimiseFor(state: EndpointConfig, optimizationType) = { + state with OptimizationType = optimizationType - } + } [] - member _.AddRule(state: EndpointConfig, rule: CdnRuleConfig) = - { state with + member _.AddRule(state: EndpointConfig, rule: CdnRuleConfig) = { + state with Rules = state.Rules @ [ rule ] - } + } [] - member _.AddRules(state: EndpointConfig, rules: CdnRuleConfig list) = - { state with + member _.AddRules(state: EndpointConfig, rules: CdnRuleConfig list) = { + state with Rules = state.Rules @ rules - } + } type CdnRuleBuilder() = interface IDependable with - member _.Add state newDeps = - { state with + member _.Add state newDeps = { + state with Dependencies = state.Dependencies + newDeps - } - - member _.Yield _ : CdnRuleConfig = - { - Name = ResourceName.Empty - Order = 1 - Conditions = list.Empty - Actions = list.Empty } + member _.Yield _ : CdnRuleConfig = { + Name = ResourceName.Empty + Order = 1 + Conditions = list.Empty + Actions = list.Empty + } + [] member _.Name(state: CdnRuleConfig, name) = { state with Name = name } @@ -241,269 +231,252 @@ type CdnRuleBuilder() = member _.Order(state: CdnRuleConfig, order) = { state with Order = order } [] - member _.WhenDeviceType(state: CdnRuleConfig, operator, deviceType) = - { state with + member _.WhenDeviceType(state: CdnRuleConfig, operator, deviceType) = { + state with Conditions = state.Conditions @ [ - IsDevice - {| - Operator = operator - DeviceType = deviceType - |} + IsDevice {| + Operator = operator + DeviceType = deviceType + |} ] - } + } [] - member _.WhenHttpVersion(state: CdnRuleConfig, operator, httpVersions) = - { state with + member _.WhenHttpVersion(state: CdnRuleConfig, operator, httpVersions) = { + state with Conditions = state.Conditions @ [ - HttpVersion - {| - Operator = operator - HttpVersions = httpVersions - |} + HttpVersion {| + Operator = operator + HttpVersions = httpVersions + |} ] - } + } [] - member _.WhenRequestCookies(state: CdnRuleConfig, cookiesName, operator, cookiesValue, caseTransform) = - { state with + member _.WhenRequestCookies(state: CdnRuleConfig, cookiesName, operator, cookiesValue, caseTransform) = { + state with Conditions = state.Conditions @ [ - RequestCookies - {| - CookiesName = cookiesName - Operator = operator - CookiesValue = cookiesValue - CaseTransform = caseTransform - |} + RequestCookies {| + CookiesName = cookiesName + Operator = operator + CookiesValue = cookiesValue + CaseTransform = caseTransform + |} ] - } + } [] - member _.WhenPostArgument(state: CdnRuleConfig, argumentName, operator, argumentValue, caseTransform) = - { state with + member _.WhenPostArgument(state: CdnRuleConfig, argumentName, operator, argumentValue, caseTransform) = { + state with Conditions = state.Conditions @ [ - PostArgument - {| - ArgumentName = argumentName - Operator = operator - ArgumentValue = argumentValue - CaseTransform = caseTransform - |} + PostArgument {| + ArgumentName = argumentName + Operator = operator + ArgumentValue = argumentValue + CaseTransform = caseTransform + |} ] - } + } [] - member _.WhenQueryString(state: CdnRuleConfig, operator, queryString, caseTransform) = - { state with + member _.WhenQueryString(state: CdnRuleConfig, operator, queryString, caseTransform) = { + state with Conditions = state.Conditions @ [ - QueryString - {| - Operator = operator - QueryString = queryString - CaseTransform = caseTransform - |} + QueryString {| + Operator = operator + QueryString = queryString + CaseTransform = caseTransform + |} ] - } + } [] - member _.WhenRemoteAddress(state: CdnRuleConfig, operator, matchValues) = - { state with + member _.WhenRemoteAddress(state: CdnRuleConfig, operator, matchValues) = { + state with Conditions = state.Conditions @ [ - RemoteAddress - {| - Operator = operator - MatchValues = matchValues - |} + RemoteAddress {| + Operator = operator + MatchValues = matchValues + |} ] - } + } [] - member _.WhenRequestBody(state: CdnRuleConfig, operator, requestBody, caseTransform) = - { state with + member _.WhenRequestBody(state: CdnRuleConfig, operator, requestBody, caseTransform) = { + state with Conditions = state.Conditions @ [ - RequestBody - {| - Operator = operator - RequestBody = requestBody - CaseTransform = caseTransform - |} + RequestBody {| + Operator = operator + RequestBody = requestBody + CaseTransform = caseTransform + |} ] - } + } [] - member _.WhenRequestHeader(state: CdnRuleConfig, headerName, operator, headerValue, caseTransform) = - { state with + member _.WhenRequestHeader(state: CdnRuleConfig, headerName, operator, headerValue, caseTransform) = { + state with Conditions = state.Conditions @ [ - RequestHeader - {| - HeaderName = headerName - Operator = operator - HeaderValue = headerValue - CaseTransform = caseTransform - |} + RequestHeader {| + HeaderName = headerName + Operator = operator + HeaderValue = headerValue + CaseTransform = caseTransform + |} ] - } + } [] - member _.WhenRequestMethod(state: CdnRuleConfig, operator, requestMethod) = - { state with + member _.WhenRequestMethod(state: CdnRuleConfig, operator, requestMethod) = { + state with Conditions = state.Conditions @ [ - RequestMethod - {| - Operator = operator - RequestMethod = requestMethod - |} + RequestMethod {| + Operator = operator + RequestMethod = requestMethod + |} ] - } + } [] - member _.WhenRequestProtocol(state: CdnRuleConfig, operator, value) = - { state with + member _.WhenRequestProtocol(state: CdnRuleConfig, operator, value) = { + state with Conditions = state.Conditions @ [ RequestProtocol {| Operator = operator; Value = value |} ] - } + } [] - member _.WhenRequestUrl(state: CdnRuleConfig, operator, requestUrl, caseTransform) = - { state with + member _.WhenRequestUrl(state: CdnRuleConfig, operator, requestUrl, caseTransform) = { + state with Conditions = state.Conditions @ [ - RequestUrl - {| - Operator = operator - RequestUrl = requestUrl - CaseTransform = caseTransform - |} + RequestUrl {| + Operator = operator + RequestUrl = requestUrl + CaseTransform = caseTransform + |} ] - } + } [] - member _.WhenUrlFileExtension(state: CdnRuleConfig, operator, extension, caseTransform) = - { state with + member _.WhenUrlFileExtension(state: CdnRuleConfig, operator, extension, caseTransform) = { + state with Conditions = state.Conditions @ [ - UrlFileExtension - {| - Operator = operator - Extension = extension - CaseTransform = caseTransform - |} + UrlFileExtension {| + Operator = operator + Extension = extension + CaseTransform = caseTransform + |} ] - } + } [] - member _.WhenUrlFileName(state: CdnRuleConfig, operator, fileName, caseTransform) = - { state with + member _.WhenUrlFileName(state: CdnRuleConfig, operator, fileName, caseTransform) = { + state with Conditions = state.Conditions @ [ - UrlFileName - {| - Operator = operator - FileName = fileName - CaseTransform = caseTransform - |} + UrlFileName {| + Operator = operator + FileName = fileName + CaseTransform = caseTransform + |} ] - } + } [] - member _.WhenUrlPath(state: CdnRuleConfig, operator, value, caseTransform) = - { state with + member _.WhenUrlPath(state: CdnRuleConfig, operator, value, caseTransform) = { + state with Conditions = state.Conditions @ [ - UrlPath - {| - Operator = operator - Value = value - CaseTransform = caseTransform - |} + UrlPath {| + Operator = operator + Value = value + CaseTransform = caseTransform + |} ] - } + } [] - member _.CacheExpiration(state: CdnRuleConfig, cacheBehaviour) = - { state with + member _.CacheExpiration(state: CdnRuleConfig, cacheBehaviour) = { + state with Actions = state.Actions @ [ CacheExpiration {| CacheBehaviour = cacheBehaviour |} ] - } + } [] - member _.CacheKeyQueryString(state: CdnRuleConfig, behaviour, parameters) = - { state with + member _.CacheKeyQueryString(state: CdnRuleConfig, behaviour, parameters) = { + state with Actions = state.Actions @ [ - CacheKeyQueryString - {| - Behaviour = behaviour - Parameters = parameters - |} + CacheKeyQueryString {| + Behaviour = behaviour + Parameters = parameters + |} ] - } + } [] - member _.ModifyRequestHeader(state: CdnRuleConfig, action, httpHeaderName, httpHeaderValue) = - { state with + member _.ModifyRequestHeader(state: CdnRuleConfig, action, httpHeaderName, httpHeaderValue) = { + state with Actions = state.Actions @ [ - ModifyRequestHeader - { - Action = action - HttpHeaderName = httpHeaderName - HttpHeaderValue = httpHeaderValue - } + ModifyRequestHeader { + Action = action + HttpHeaderName = httpHeaderName + HttpHeaderValue = httpHeaderValue + } ] - } + } [] - member _.ModifyResponseHeader(state: CdnRuleConfig, action, httpHeaderName, httpHeaderValue) = - { state with + member _.ModifyResponseHeader(state: CdnRuleConfig, action, httpHeaderName, httpHeaderValue) = { + state with Actions = state.Actions @ [ - ModifyResponseHeader - { - Action = action - HttpHeaderName = httpHeaderName - HttpHeaderValue = httpHeaderValue - } + ModifyResponseHeader { + Action = action + HttpHeaderName = httpHeaderName + HttpHeaderValue = httpHeaderValue + } ] - } + } [] - member _.UrlRewrite(state: CdnRuleConfig, sourcePattern, destination, preserveUnmatchedPath) = - { state with + member _.UrlRewrite(state: CdnRuleConfig, sourcePattern, destination, preserveUnmatchedPath) = { + state with Actions = state.Actions @ [ - UrlRewrite - {| - SourcePattern = sourcePattern - Destination = destination - PreserveUnmatchedPath = preserveUnmatchedPath - |} + UrlRewrite {| + SourcePattern = sourcePattern + Destination = destination + PreserveUnmatchedPath = preserveUnmatchedPath + |} ] - } + } [] member _.UrlRedirect @@ -516,12 +489,12 @@ type CdnRuleBuilder() = ?queryString, ?fragment ) = - { state with - Actions = - state.Actions - @ [ - UrlRedirect - {| + { + state with + Actions = + state.Actions + @ [ + UrlRedirect {| RedirectType = redirectType DestinationProtocol = destinationProtocol Hostname = hostname @@ -529,7 +502,7 @@ type CdnRuleBuilder() = QueryString = queryString Fragment = fragment |} - ] + ] } let cdn = CdnBuilder() diff --git a/src/Farmer/Builders/Builders.CognitiveServices.fs b/src/Farmer/Builders/Builders.CognitiveServices.fs index 8a3c4e648..051f4af97 100644 --- a/src/Farmer/Builders/Builders.CognitiveServices.fs +++ b/src/Farmer/Builders/Builders.CognitiveServices.fs @@ -13,13 +13,12 @@ type CognitiveServices = static member getKey(name: ResourceName) = CognitiveServices.getKey (accounts.resourceId name) -type CognitiveServicesConfig = - { - Name: ResourceName - Sku: Sku - Api: Kind - Tags: Map - } +type CognitiveServicesConfig = { + Name: ResourceName + Sku: Sku + Api: Kind + Tags: Map +} with /// Gets an ARM expression to the key of this Cognitive Services instance. member this.Key = CognitiveServices.getKey (accounts.resourceId this.Name) @@ -27,25 +26,23 @@ type CognitiveServicesConfig = interface IBuilder with member this.ResourceId = accounts.resourceId this.Name - member this.BuildResources location = - [ - { - Name = this.Name - Location = location - Sku = this.Sku - Kind = this.Api - Tags = this.Tags - } - ] + member this.BuildResources location = [ + { + Name = this.Name + Location = location + Sku = this.Sku + Kind = this.Api + Tags = this.Tags + } + ] type CognitiveServicesBuilder() = - member _.Yield _ = - { - Name = ResourceName.Empty - Sku = F0 - Api = AllInOne - Tags = Map.empty - } + member _.Yield _ = { + Name = ResourceName.Empty + Sku = F0 + Api = AllInOne + Tags = Map.empty + } [] member _.Name(state: CognitiveServicesConfig, name) = { state with Name = ResourceName name } @@ -57,9 +54,9 @@ type CognitiveServicesBuilder() = member _.Api(state: CognitiveServicesConfig, api) = { state with Api = api } interface ITaggable with - member _.Add state tags = - { state with + member _.Add state tags = { + state with Tags = state.Tags |> Map.merge tags - } + } let cognitiveServices = CognitiveServicesBuilder() diff --git a/src/Farmer/Builders/Builders.CommunicationServices.fs b/src/Farmer/Builders/Builders.CommunicationServices.fs index 34cc683f5..919395326 100644 --- a/src/Farmer/Builders/Builders.CommunicationServices.fs +++ b/src/Farmer/Builders/Builders.CommunicationServices.fs @@ -24,12 +24,11 @@ type CommunicationServices = static member getConnectionString(name: ResourceName) = CommunicationServices.getConnectionString (communicationServices.resourceId name) -type CommunicationServicesConfig = - { - Name: ResourceName - Tags: Map - DataLocation: DataLocation - } +type CommunicationServicesConfig = { + Name: ResourceName + Tags: Map + DataLocation: DataLocation +} with /// Gets an ARM expression to the key of this Communication Services instance. member this.Key = @@ -42,37 +41,35 @@ type CommunicationServicesConfig = interface IBuilder with member this.ResourceId = communicationServices.resourceId this.Name - member this.BuildResources _ = - [ - { - CommunicationService.Name = this.Name - Tags = this.Tags - DataLocation = this.DataLocation - } - ] + member this.BuildResources _ = [ + { + CommunicationService.Name = this.Name + Tags = this.Tags + DataLocation = this.DataLocation + } + ] type CommunicationServicesBuilder() = - member _.Yield _ = - { - Name = ResourceName.Empty - Tags = Map.empty - // We default to UnitedStates because all of the features are available there. - DataLocation = DataLocation.UnitedStates - } + member _.Yield _ = { + Name = ResourceName.Empty + Tags = Map.empty + // We default to UnitedStates because all of the features are available there. + DataLocation = DataLocation.UnitedStates + } [] member _.Name(state: CommunicationServicesConfig, name) = { state with Name = ResourceName name } [] - member _.DataLocation(state: CommunicationServicesConfig, dataLocation) = - { state with + member _.DataLocation(state: CommunicationServicesConfig, dataLocation) = { + state with DataLocation = dataLocation - } + } interface ITaggable with - member _.Add state tags = - { state with + member _.Add state tags = { + state with Tags = state.Tags |> Map.merge tags - } + } let communicationService = CommunicationServicesBuilder() diff --git a/src/Farmer/Builders/Builders.ContainerApps.fs b/src/Farmer/Builders/Builders.ContainerApps.fs index 068c3e7a1..d8bca4106 100644 --- a/src/Farmer/Builders/Builders.ContainerApps.fs +++ b/src/Farmer/Builders/Builders.ContainerApps.fs @@ -8,45 +8,44 @@ open Farmer.ContainerAppValidation open Farmer.Arm.App open Farmer.Identity -type ContainerConfig = - { - ContainerName: string - DockerImage: Containers.DockerImage option - /// Volume mounts for the container - VolumeMounts: Map - Resources: {| CPU: float - Memory: float - EphemeralStorage: float option |} - } +type ContainerConfig = { + ContainerName: string + DockerImage: Containers.DockerImage option + /// Volume mounts for the container + VolumeMounts: Map + Resources: {| + CPU: float + Memory: float + EphemeralStorage: float option + |} +} with member internal this.BuildContainer: Container = match this.DockerImage with - | Some dockerImage -> - { - Name = this.ContainerName - DockerImage = dockerImage - Resources = this.Resources - VolumeMounts = this.VolumeMounts - } + | Some dockerImage -> { + Name = this.ContainerName + DockerImage = dockerImage + Resources = this.Resources + VolumeMounts = this.VolumeMounts + } | None -> raiseFarmer $"Container '{this.ContainerName}' requires a docker image." -type ContainerAppConfig = - { - Name: ResourceName - ActiveRevisionsMode: ActiveRevisionsMode - IngressMode: IngressMode option - ScaleRules: Map - Identity: ManagedIdentity - Replicas: {| Min: int; Max: int |} option - DaprConfig: {| AppId: string |} option - Secrets: Map - EnvironmentVariables: Map - Volumes: Map - /// Credentials for image registries used by containers in this environment. - ImageRegistryCredentials: ImageRegistryAuthentication list - Containers: ContainerConfig list - Dependencies: Set - } +type ContainerAppConfig = { + Name: ResourceName + ActiveRevisionsMode: ActiveRevisionsMode + IngressMode: IngressMode option + ScaleRules: Map + Identity: ManagedIdentity + Replicas: {| Min: int; Max: int |} option + DaprConfig: {| AppId: string |} option + Secrets: Map + EnvironmentVariables: Map + Volumes: Map + /// Credentials for image registries used by containers in this environment. + ImageRegistryCredentials: ImageRegistryAuthentication list + Containers: ContainerConfig list + Dependencies: Set +} with member this.ResourceId = containerApps.resourceId this.Name @@ -55,100 +54,97 @@ type ContainerAppConfig = .reference(containerApps, this.ResourceId) .Map(sprintf "%s.latestRevisionFqdn") -type ContainerEnvironmentConfig = - { - Name: ResourceName - InternalLoadBalancerState: FeatureFlag - ContainerApps: ContainerAppConfig list - AppInsights: AppInsightsConfig option - LogAnalytics: ResourceRef - Dependencies: Set - Tags: Map - } +type ContainerEnvironmentConfig = { + Name: ResourceName + InternalLoadBalancerState: FeatureFlag + ContainerApps: ContainerAppConfig list + AppInsights: AppInsightsConfig option + LogAnalytics: ResourceRef + Dependencies: Set + Tags: Map +} with interface IBuilder with member this.ResourceId = managedEnvironments.resourceId this.Name - member this.BuildResources location = - [ - let logAnalyticsResourceId = this.LogAnalytics.resourceId this + member this.BuildResources location = [ + let logAnalyticsResourceId = this.LogAnalytics.resourceId this - { - Name = this.Name - InternalLoadBalancerState = this.InternalLoadBalancerState - LogAnalytics = logAnalyticsResourceId - Location = location - AppInsightsInstrumentationKey = this.AppInsights |> Option.map (fun r -> r.InstrumentationKey) - Dependencies = this.Dependencies.Add logAnalyticsResourceId - Tags = this.Tags - } + { + Name = this.Name + InternalLoadBalancerState = this.InternalLoadBalancerState + LogAnalytics = logAnalyticsResourceId + Location = location + AppInsightsInstrumentationKey = this.AppInsights |> Option.map (fun r -> r.InstrumentationKey) + Dependencies = this.Dependencies.Add logAnalyticsResourceId + Tags = this.Tags + } - match this.LogAnalytics with - | DeployableResource this resourceId -> - let workspaceConfig = - { - Name = resourceId.Name - RetentionPeriod = None - IngestionSupport = None - QuerySupport = None - DailyCap = None - Tags = Map.empty - } - :> IBuilder - - yield! workspaceConfig.BuildResources location - | _ -> () - - for containerApp in this.ContainerApps do + match this.LogAnalytics with + | DeployableResource this resourceId -> + let workspaceConfig = { - Name = containerApp.Name - Environment = managedEnvironments.resourceId this.Name - ActiveRevisionsMode = containerApp.ActiveRevisionsMode - Identity = containerApp.Identity - IngressMode = containerApp.IngressMode - ScaleRules = containerApp.ScaleRules - Replicas = containerApp.Replicas - DaprConfig = containerApp.DaprConfig - Secrets = containerApp.Secrets - EnvironmentVariables = - let env = containerApp.EnvironmentVariables - - match this.AppInsights with - | Some resource -> - env.Add( - EnvVar.createSecureExpression - "APPINSIGHTS_INSTRUMENTATIONKEY" - resource.InstrumentationKey - ) - | None -> env - ImageRegistryCredentials = containerApp.ImageRegistryCredentials - Containers = containerApp.Containers |> List.map (fun c -> c.BuildContainer) - Location = location - Volumes = containerApp.Volumes - Dependencies = containerApp.Dependencies + Name = resourceId.Name + RetentionPeriod = None + IngestionSupport = None + QuerySupport = None + DailyCap = None + Tags = Map.empty } + :> IBuilder - for app in this.ContainerApps do - let uniqueVolumes = - app.Volumes - |> Seq.choose (ManagedEnvironmentStorage.from (managedEnvironments.resourceId this.Name)) - |> Seq.distinctBy (fun v -> v.Name) + yield! workspaceConfig.BuildResources location + | _ -> () - for volume in uniqueVolumes do - volume - ] + for containerApp in this.ContainerApps do + { + Name = containerApp.Name + Environment = managedEnvironments.resourceId this.Name + ActiveRevisionsMode = containerApp.ActiveRevisionsMode + Identity = containerApp.Identity + IngressMode = containerApp.IngressMode + ScaleRules = containerApp.ScaleRules + Replicas = containerApp.Replicas + DaprConfig = containerApp.DaprConfig + Secrets = containerApp.Secrets + EnvironmentVariables = + let env = containerApp.EnvironmentVariables + + match this.AppInsights with + | Some resource -> + env.Add( + EnvVar.createSecureExpression + "APPINSIGHTS_INSTRUMENTATIONKEY" + resource.InstrumentationKey + ) + | None -> env + ImageRegistryCredentials = containerApp.ImageRegistryCredentials + Containers = containerApp.Containers |> List.map (fun c -> c.BuildContainer) + Location = location + Volumes = containerApp.Volumes + Dependencies = containerApp.Dependencies + } + + for app in this.ContainerApps do + let uniqueVolumes = + app.Volumes + |> Seq.choose (ManagedEnvironmentStorage.from (managedEnvironments.resourceId this.Name)) + |> Seq.distinctBy (fun v -> v.Name) + + for volume in uniqueVolumes do + volume + ] type ContainerEnvironmentBuilder() = - member _.Yield _ = - { - Name = ResourceName.Empty - InternalLoadBalancerState = Disabled - ContainerApps = [] - AppInsights = None - LogAnalytics = derived (fun cfg -> Arm.LogAnalytics.workspaces.resourceId (cfg.Name - "workspace")) - Dependencies = Set.empty - Tags = Map.empty - } + member _.Yield _ = { + Name = ResourceName.Empty + InternalLoadBalancerState = Disabled + ContainerApps = [] + AppInsights = None + LogAnalytics = derived (fun cfg -> Arm.LogAnalytics.workspaces.resourceId (cfg.Name - "workspace")) + Dependencies = Set.empty + Tags = Map.empty + } /// Sets the name of the Azure Container App Environment. [] @@ -156,72 +152,70 @@ type ContainerEnvironmentBuilder() = /// Adds the instrumentation key to each container app and configures for Dapr. [] - member _.SetAppInsights(state: ContainerEnvironmentConfig, appInsights: AppInsightsConfig) = - { state with + member _.SetAppInsights(state: ContainerEnvironmentConfig, appInsights: AppInsightsConfig) = { + state with AppInsights = Some appInsights - } + } /// Sets the Log Analytics workspace of the Azure Container App. [] - member _.SetLogAnalytics(state: ContainerEnvironmentConfig, logAnalytics: WorkspaceConfig) = - { state with + member _.SetLogAnalytics(state: ContainerEnvironmentConfig, logAnalytics: WorkspaceConfig) = { + state with LogAnalytics = unmanaged (Arm.LogAnalytics.workspaces.resourceId logAnalytics.Name) - } + } /// Sets whether an internal load balancer should be used for load balancing traffic to container app replicas. [] - member _.SetInternalLoadBalancerState(state: ContainerEnvironmentConfig, internalLoadBalancerState: FeatureFlag) = - { state with + member _.SetInternalLoadBalancerState(state: ContainerEnvironmentConfig, internalLoadBalancerState: FeatureFlag) = { + state with InternalLoadBalancerState = internalLoadBalancerState - } + } /// Adds a container to the Azure Container App Environment. [] - member _.AddContainerApp(state: ContainerEnvironmentConfig, containerApp: ContainerAppConfig) = - { state with + member _.AddContainerApp(state: ContainerEnvironmentConfig, containerApp: ContainerAppConfig) = { + state with ContainerApps = containerApp :: state.ContainerApps - } + } /// Adds multiple containers to the Azure Container App Environment. [] - member _.AddContainerApps(state: ContainerEnvironmentConfig, containerApps: ContainerAppConfig list) = - { state with + member _.AddContainerApps(state: ContainerEnvironmentConfig, containerApps: ContainerAppConfig list) = { + state with ContainerApps = containerApps @ state.ContainerApps - } + } interface ITaggable with /// Adds a tag to this Container App Environment. - member _.Add state tags = - { state with + member _.Add state tags = { + state with Tags = state.Tags |> Map.merge tags - } + } interface IDependable with /// Adds an explicit dependency to this Container App Environment. - member _.Add state newDeps = - { state with + member _.Add state newDeps = { + state with Dependencies = state.Dependencies + newDeps - } + } let private supportedResourceCombinations = - Set - [ - 0.25, 0.5 - 0.5, 1.0 - 0.75, 1.5 - 1.0, 2.0 - 1.25, 2.5 - 1.5, 3.0 - 1.75, 3.5 - 2.0, 4. - ] - -let private defaultResources = - {| - CPU = 0.25 - Memory = 0.5 - EphemeralStorage = None - |} + Set [ + 0.25, 0.5 + 0.5, 1.0 + 0.75, 1.5 + 1.0, 2.0 + 1.25, 2.5 + 1.5, 3.0 + 1.75, 3.5 + 2.0, 4. + ] + +let private defaultResources = {| + CPU = 0.25 + Memory = 0.5 + EphemeralStorage = None +|} module Volume = let emptyDir volumeName = volumeName, Volume.EmptyDirectory @@ -230,22 +224,21 @@ module Volume = volumeName, Volume.AzureFileShare(shareName, storageAccount, accessMode) type ContainerAppBuilder() = - member _.Yield _ = - { - Name = ResourceName.Empty - ActiveRevisionsMode = ActiveRevisionsMode.Single - ImageRegistryCredentials = [] - Containers = [] - Replicas = None - ScaleRules = Map.empty - Secrets = Map.empty - IngressMode = None - Volumes = Map.empty - Identity = ManagedIdentity.Empty - EnvironmentVariables = Map.empty - DaprConfig = None - Dependencies = Set.empty - } + member _.Yield _ = { + Name = ResourceName.Empty + ActiveRevisionsMode = ActiveRevisionsMode.Single + ImageRegistryCredentials = [] + Containers = [] + Replicas = None + ScaleRules = Map.empty + Secrets = Map.empty + IngressMode = None + Volumes = Map.empty + Identity = ManagedIdentity.Empty + EnvironmentVariables = Map.empty + DaprConfig = None + Dependencies = Set.empty + } member _.Run(state: ContainerAppConfig) = let resourceTotals = @@ -268,10 +261,10 @@ type ContainerAppBuilder() = state interface IIdentity with - member _.Add state updater = - { state with + member _.Add state updater = { + state with Identity = updater state.Identity - } + } /// Sets the name of the Azure Container App. [] @@ -279,44 +272,44 @@ type ContainerAppBuilder() = /// Adds a scale rule to the Azure Container App. [] - member _.AddHttpScaleRule(state: ContainerAppConfig, name, rule: HttpScaleRule) = - { state with + member _.AddHttpScaleRule(state: ContainerAppConfig, name, rule: HttpScaleRule) = { + state with ScaleRules = state.ScaleRules.Add(name, ScaleRule.Http rule) - } + } [] - member _.AddServiceBusScaleRule(state: ContainerAppConfig, name, rule: ServiceBusScaleRule) = - { state with + member _.AddServiceBusScaleRule(state: ContainerAppConfig, name, rule: ServiceBusScaleRule) = { + state with ScaleRules = state.ScaleRules.Add(name, ScaleRule.ServiceBus rule) - } + } [] - member _.AddEventHubScaleRule(state: ContainerAppConfig, name, rule: EventHubScaleRule) = - { state with + member _.AddEventHubScaleRule(state: ContainerAppConfig, name, rule: EventHubScaleRule) = { + state with ScaleRules = state.ScaleRules.Add(name, ScaleRule.EventHub rule) - } + } [] - member _.AddCpuScaleRule(state: ContainerAppConfig, name, rule) = - { state with + member _.AddCpuScaleRule(state: ContainerAppConfig, name, rule) = { + state with ScaleRules = state.ScaleRules.Add(name, ScaleRule.CPU(Utilization rule)) - } + } - member _.AddCpuScaleRule(state: ContainerAppConfig, name, rule) = - { state with + member _.AddCpuScaleRule(state: ContainerAppConfig, name, rule) = { + state with ScaleRules = state.ScaleRules.Add(name, ScaleRule.CPU(AverageValue rule)) - } + } [] - member _.AddMemScaleRule(state: ContainerAppConfig, name, rule) = - { state with + member _.AddMemScaleRule(state: ContainerAppConfig, name, rule) = { + state with ScaleRules = state.ScaleRules.Add(name, ScaleRule.Memory(Utilization rule)) - } + } - member _.AddMemScaleRule(state: ContainerAppConfig, name, rule) = - { state with + member _.AddMemScaleRule(state: ContainerAppConfig, name, rule) = { + state with ScaleRules = state.ScaleRules.Add(name, ScaleRule.Memory(AverageValue rule)) - } + } [] member this.AddQueueScaleRule @@ -335,131 +328,131 @@ type ContainerAppBuilder() = let state: ContainerAppConfig = this.AddSecretExpression(state, secretRef, storageAccount.Key) - let queueRule = - { - QueueName = queueName - QueueLength = queueLength - StorageConnectionSecretRef = secretRef - AccountName = storageAccount.Name.ResourceName.Value - } + let queueRule = { + QueueName = queueName + QueueLength = queueLength + StorageConnectionSecretRef = secretRef + AccountName = storageAccount.Name.ResourceName.Value + } - { state with - ScaleRules = state.ScaleRules.Add(name, ScaleRule.StorageQueue queueRule) + { + state with + ScaleRules = state.ScaleRules.Add(name, ScaleRule.StorageQueue queueRule) } [] - member _.AddCustomScaleRule(state: ContainerAppConfig, name, rule) = - { state with + member _.AddCustomScaleRule(state: ContainerAppConfig, name, rule) = { + state with ScaleRules = state.ScaleRules.Add(name, ScaleRule.Custom rule) - } + } /// Actives or deactivates the ingress of the Azure Container App. [] - member _.SetIngressVisibility(state: ContainerAppConfig, enabled) = - { state with + member _.SetIngressVisibility(state: ContainerAppConfig, enabled) = { + state with IngressMode = match enabled with | Enabled -> External(80us, None) | Disabled -> InternalOnly |> Some - } + } /// Configures the ingress of the Azure Container App. [] - member _.SetIngressTargetPort(state: ContainerAppConfig, targetPort) = - { state with + member _.SetIngressTargetPort(state: ContainerAppConfig, targetPort) = { + state with IngressMode = let existingTransport = match state.IngressMode with - | Some (External (_, transport)) -> transport + | Some(External(_, transport)) -> transport | Some InternalOnly | None -> None Some(External(targetPort, existingTransport)) - } + } /// Configures the ingress of the Azure Container App. [] - member _.SetIngressTransport(state: ContainerAppConfig, transport) = - { state with + member _.SetIngressTransport(state: ContainerAppConfig, transport) = { + state with IngressMode = let existingPort = match state.IngressMode with - | Some (External (port, _)) -> port + | Some(External(port, _)) -> port | Some InternalOnly | None -> 80us Some(External(existingPort, Some transport)) - } + } /// Configures Dapr in the Azure Container App. [] - member _.SetDaprAppId(state: ContainerAppConfig, appId) = - { state with + member _.SetDaprAppId(state: ContainerAppConfig, appId) = { + state with DaprConfig = Some {| AppId = appId |} - } + } /// Sets the minimum and maximum replicas to scale the container app. [] - member _.SetReplicas(state: ContainerAppConfig, minReplicas: int, maxReplicas: int) = - { state with + member _.SetReplicas(state: ContainerAppConfig, minReplicas: int, maxReplicas: int) = { + state with Replicas = - Some - {| - Min = minReplicas - Max = maxReplicas - |} - } + Some {| + Min = minReplicas + Max = maxReplicas + |} + } /// Adds container image registry credentials for images in this container app. [] - member _.AddRegistryCredentials(state: ContainerAppConfig, credentials) = - { state with + member _.AddRegistryCredentials(state: ContainerAppConfig, credentials) = { + state with ImageRegistryCredentials = state.ImageRegistryCredentials @ (credentials |> List.map ImageRegistryAuthentication.Credential) - } + } /// Reference container registries to import their admin credential at deployment time. [] - member _.ReferenceRegistryCredentials(state: ContainerAppConfig, resourceIds) = - { state with + member _.ReferenceRegistryCredentials(state: ContainerAppConfig, resourceIds) = { + state with ImageRegistryCredentials = state.ImageRegistryCredentials @ (resourceIds |> List.map ImageRegistryAuthentication.ListCredentials) - } + } /// Adds container app registry managed identity credentials for images in this container app. [] - member _.ManagedIdentityRegistryCredentials(state: ContainerAppConfig, credentials) = - { state with + member _.ManagedIdentityRegistryCredentials(state: ContainerAppConfig, credentials) = { + state with ImageRegistryCredentials = state.ImageRegistryCredentials @ (credentials |> List.map ImageRegistryAuthentication.ManagedIdentityCredential) - } + } /// Adds one or more containers to the container app. [] - member _.AddContainers(state: ContainerAppConfig, containers: ContainerConfig list) = - { state with + member _.AddContainers(state: ContainerAppConfig, containers: ContainerConfig list) = { + state with Containers = state.Containers @ containers - } + } /// Sets the active revision mode of the Azure Container App. [] - member _.SetActiveRevisionsMode(state: ContainerAppConfig, mode: ActiveRevisionsMode) = - { state with + member _.SetActiveRevisionsMode(state: ContainerAppConfig, mode: ActiveRevisionsMode) = { + state with ActiveRevisionsMode = mode - } + } /// Adds an application secret to the Azure Container App. [] member _.AddSecretParameter(state: ContainerAppConfig, key) = let key = (ContainerAppSettingKey.Create key).OkValue - { state with - Secrets = state.Secrets.Add(key, ParameterSecret(SecureParameter key.Value)) - EnvironmentVariables = state.EnvironmentVariables.Add(EnvVar.createSecure key.Value key.Value) + { + state with + Secrets = state.Secrets.Add(key, ParameterSecret(SecureParameter key.Value)) + EnvironmentVariables = state.EnvironmentVariables.Add(EnvVar.createSecure key.Value key.Value) } /// Adds an application secrets to the Azure Container App. @@ -472,13 +465,14 @@ type ContainerAppBuilder() = member _.AddSecretExpression(state: ContainerAppConfig, key, expression) = let key = (ContainerAppSettingKey.Create key).OkValue - { state with - Secrets = state.Secrets.Add(key, ExpressionSecret expression) - EnvironmentVariables = state.EnvironmentVariables.Add(EnvVar.createSecure key.Value key.Value) - Dependencies = - match expression.Owner with - | Some owner -> state.Dependencies.Add owner - | None -> state.Dependencies + { + state with + Secrets = state.Secrets.Add(key, ExpressionSecret expression) + EnvironmentVariables = state.EnvironmentVariables.Add(EnvVar.createSecure key.Value key.Value) + Dependencies = + match expression.Owner with + | Some owner -> state.Dependencies.Add owner + | None -> state.Dependencies } /// Adds an application secrets to the Azure Container App. @@ -489,10 +483,10 @@ type ContainerAppBuilder() = /// Adds a public environment variable to the Azure Container App environment variables. [] - member _.AddEnvironmentVariable(state: ContainerAppConfig, name, value) = - { state with + member _.AddEnvironmentVariable(state: ContainerAppConfig, name, value) = { + state with EnvironmentVariables = state.EnvironmentVariables.Add(EnvVar.create name value) - } + } /// Adds a public environment variables to the Azure Container App environment variables. [] @@ -501,13 +495,12 @@ type ContainerAppBuilder() = [] member this.AddSimpleContainer(state: ContainerAppConfig, dockerImage, dockerVersion) = - let container = - { - ContainerConfig.ContainerName = state.Name.Value - DockerImage = Some(Containers.PublicImage(dockerImage, Some dockerVersion)) - Resources = defaultResources - VolumeMounts = Map.empty - } + let container = { + ContainerConfig.ContainerName = state.Name.Value + DockerImage = Some(Containers.PublicImage(dockerImage, Some dockerVersion)) + Resources = defaultResources + VolumeMounts = Map.empty + } this.AddContainers(state, [ container ]) @@ -524,19 +517,18 @@ type ContainerAppBuilder() = interface IDependable with /// Adds an explicit dependency to this Container App. - member _.Add state newDeps = - { state with + member _.Add state newDeps = { + state with Dependencies = state.Dependencies + newDeps - } + } type ContainerBuilder() = - member _.Yield _ = - { - ContainerName = "" - DockerImage = None - Resources = defaultResources - VolumeMounts = Map.empty - } + member _.Yield _ = { + ContainerName = "" + DockerImage = None + Resources = defaultResources + VolumeMounts = Map.empty + } /// Set docker credentials [] @@ -544,16 +536,16 @@ type ContainerBuilder() = /// Set docker credentials [] - member _.SetPrivateDockerImage(state: ContainerConfig, registry, containerName, version: string) = - { state with + member _.SetPrivateDockerImage(state: ContainerConfig, registry, containerName, version: string) = { + state with DockerImage = Some(Containers.PrivateImage(registry, containerName, Option.ofObj version)) - } + } [] - member _.SetPublicDockerImage(state: ContainerConfig, containerName, version: string) = - { state with + member _.SetPublicDockerImage(state: ContainerConfig, containerName, version: string) = { + state with DockerImage = Some(Containers.PublicImage(containerName, Option.ofObj version)) - } + } [] member _.CpuCores(state: ContainerConfig, cpuCount: float) = @@ -564,10 +556,11 @@ type ContainerBuilder() = let roundedCpuCount = System.Math.Round(numCores, 2) * 1. - { state with - Resources = - {| state.Resources with - CPU = roundedCpuCount + { + state with + Resources = {| + state.Resources with + CPU = roundedCpuCount |} } @@ -577,10 +570,11 @@ type ContainerBuilder() = let roundedSize = System.Math.Round(size, 2) * 1. - { state with - Resources = - {| state.Resources with - EphemeralStorage = Some roundedSize + { + state with + Resources = {| + state.Resources with + EphemeralStorage = Some roundedSize |} } @@ -593,20 +587,21 @@ type ContainerBuilder() = let roundedMemory = System.Math.Round(memory, 2) * 1. - { state with - Resources = - {| state.Resources with - Memory = roundedMemory + { + state with + Resources = {| + state.Resources with + Memory = roundedMemory |} } [] - member _.AddVolumeMounts(state: ContainerConfig, mounts: #seq<_>) = - { state with + member _.AddVolumeMounts(state: ContainerConfig, mounts: #seq<_>) = { + state with VolumeMounts = mounts |> Seq.fold (fun s (volumeName, mountPath) -> s |> Map.add volumeName mountPath) state.VolumeMounts - } + } let containerEnvironment = ContainerEnvironmentBuilder() diff --git a/src/Farmer/Builders/Builders.ContainerGroups.fs b/src/Farmer/Builders/Builders.ContainerGroups.fs index 691d84bf4..69188c370 100644 --- a/src/Farmer/Builders/Builders.ContainerGroups.fs +++ b/src/Farmer/Builders/Builders.ContainerGroups.fs @@ -42,84 +42,81 @@ type volume_mount = volumeName, Volume.Secret [ SecretFileParameter(file, SecureParameter secretParameterName) ] /// Represents configuration for a single Container. -type ContainerInstanceConfig = - { - /// The name of the container instance - Name: ResourceName - /// The container instance image - Image: Containers.DockerImage option - /// The commands to execute within the container instance in exec form - Command: string list - /// List of ports the container instance listens on - Ports: Map - /// Max number of CPU cores the container instance may use - Cpu: float - /// Max gigabytes of memory the container instance may use - Memory: float - // Container instances gpu - Gpu: ContainerInstanceGpu option - /// Environment variables for the container - EnvironmentVariables: Map - /// Liveness probe for checking the container's health. - LivenessProbe: ContainerProbe option - /// Readiness probe to wait for the container to be ready to accept requests. - ReadinessProbe: ContainerProbe option - /// Volume mounts for the container - VolumeMounts: Map - } +type ContainerInstanceConfig = { + /// The name of the container instance + Name: ResourceName + /// The container instance image + Image: Containers.DockerImage option + /// The commands to execute within the container instance in exec form + Command: string list + /// List of ports the container instance listens on + Ports: Map + /// Max number of CPU cores the container instance may use + Cpu: float + /// Max gigabytes of memory the container instance may use + Memory: float + // Container instances gpu + Gpu: ContainerInstanceGpu option + /// Environment variables for the container + EnvironmentVariables: Map + /// Liveness probe for checking the container's health. + LivenessProbe: ContainerProbe option + /// Readiness probe to wait for the container to be ready to accept requests. + ReadinessProbe: ContainerProbe option + /// Volume mounts for the container + VolumeMounts: Map +} /// Represents configuration for an init container that runs on container group startup. -type InitContainerConfig = - { - /// The name of the container instance - Name: ResourceName - /// The container instance image - Image: Containers.DockerImage option - /// The commands to execute within the container instance in exec form - Command: string list - /// Environment variables for the container - EnvironmentVariables: Map - /// Volume mounts for the container - VolumeMounts: Map - } - -type ContainerGroupConfig = - { - /// The name of the container group. - Name: ResourceName - /// Availability zone where the container group should be deployed. - AvailabilityZone: string option - /// Diagnostics and logging for the container group - Diagnostics: ContainerGroupDiagnostics option - /// DNS configuration for the container group - DnsConfig: ContainerGroupDnsConfiguration option - /// Container group OS. - OperatingSystem: OS - /// Restart policy for the container group. - RestartPolicy: RestartPolicy - /// Credentials for image registries used by containers in this group. - ImageRegistryCredentials: ImageRegistryAuthentication list - /// IP address for the container group. - IpAddress: ContainerGroupIpAddress option - /// The init containers in this container group. - InitContainers: InitContainerConfig list - /// The instances in this container group. - Instances: ContainerInstanceConfig list - /// Name of the network profile for this container's group - not supported when specifying the availability zone. - NetworkProfile: ResourceName option - /// Resource ID of the virtual network where this container group should be attached. - VirtualNetwork: LinkedResource option - /// Name of the subnet where this container group should be attached. - SubnetName: ResourceName option - /// Volumes to mount on the container group. - Volumes: Map - /// Managed identity for the container group. - Identity: ManagedIdentity - /// Tags for the container group. - Tags: Map - /// Additional dependencies. - Dependencies: Set - } +type InitContainerConfig = { + /// The name of the container instance + Name: ResourceName + /// The container instance image + Image: Containers.DockerImage option + /// The commands to execute within the container instance in exec form + Command: string list + /// Environment variables for the container + EnvironmentVariables: Map + /// Volume mounts for the container + VolumeMounts: Map +} + +type ContainerGroupConfig = { + /// The name of the container group. + Name: ResourceName + /// Availability zone where the container group should be deployed. + AvailabilityZone: string option + /// Diagnostics and logging for the container group + Diagnostics: ContainerGroupDiagnostics option + /// DNS configuration for the container group + DnsConfig: ContainerGroupDnsConfiguration option + /// Container group OS. + OperatingSystem: OS + /// Restart policy for the container group. + RestartPolicy: RestartPolicy + /// Credentials for image registries used by containers in this group. + ImageRegistryCredentials: ImageRegistryAuthentication list + /// IP address for the container group. + IpAddress: ContainerGroupIpAddress option + /// The init containers in this container group. + InitContainers: InitContainerConfig list + /// The instances in this container group. + Instances: ContainerInstanceConfig list + /// Name of the network profile for this container's group - not supported when specifying the availability zone. + NetworkProfile: ResourceName option + /// Resource ID of the virtual network where this container group should be attached. + VirtualNetwork: LinkedResource option + /// Name of the subnet where this container group should be attached. + SubnetName: ResourceName option + /// Volumes to mount on the container group. + Volumes: Map + /// Managed identity for the container group. + Identity: ManagedIdentity + /// Tags for the container group. + Tags: Map + /// Additional dependencies. + Dependencies: Set +} with member private this.ResourceId = containerGroups.resourceId this.Name member this.SystemIdentity = SystemIdentity this.ResourceId @@ -127,99 +124,95 @@ type ContainerGroupConfig = interface IBuilder with member this.ResourceId = this.ResourceId - member this.BuildResources location = - [ - { - Location = location - Name = this.Name - AvailabilityZone = - if this.AvailabilityZone.IsSome && this.NetworkProfile.IsSome then - raiseFarmer $"Cannot specify availability zone when using network profiles." - else - this.AvailabilityZone - ContainerInstances = - [ - for instance in this.Instances do - match instance.Image with - | None -> raiseFarmer $"Missing image tag for container named '{instance.Name}'." - | Some image -> - {| - Name = instance.Name - Image = image - Command = instance.Command - Ports = instance.Ports |> Map.toSeq |> Seq.map fst |> Set - Cpu = instance.Cpu - Memory = instance.Memory - Gpu = instance.Gpu - EnvironmentVariables = instance.EnvironmentVariables - LivenessProbe = instance.LivenessProbe - ReadinessProbe = instance.ReadinessProbe - VolumeMounts = instance.VolumeMounts - |} - ] - Diagnostics = this.Diagnostics - DnsConfig = - if this.DnsConfig.IsSome && this.NetworkProfile.IsNone then - raiseFarmer "DNS configuration can only be set when attached to a virtual network." - else - this.DnsConfig - OperatingSystem = this.OperatingSystem - RestartPolicy = this.RestartPolicy - Identity = this.Identity - ImageRegistryCredentials = this.ImageRegistryCredentials - InitContainers = - [ - for initContainer in this.InitContainers do - match initContainer.Image with - | None -> - raiseFarmer $"Missing image tag for initContainer named '{initContainer.Name}'." - | Some image -> - {| - Name = initContainer.Name - Image = image - Command = initContainer.Command - EnvironmentVariables = initContainer.EnvironmentVariables - VolumeMounts = initContainer.VolumeMounts - |} - ] - IpAddress = this.IpAddress - NetworkProfile = - if - this.NetworkProfile.IsSome - && (this.VirtualNetwork.IsSome || this.SubnetName.IsSome) - then - raiseFarmer - $"Should not set network profile on container group '{this.Name.Value}' when using vnet and subnet." - else - this.NetworkProfile - SubnetIds = - match this.VirtualNetwork, this.SubnetName with - | None, None -> [] - | Some (Managed vnetId), Some subnet -> - { vnetId with + member this.BuildResources location = [ + { + Location = location + Name = this.Name + AvailabilityZone = + if this.AvailabilityZone.IsSome && this.NetworkProfile.IsSome then + raiseFarmer $"Cannot specify availability zone when using network profiles." + else + this.AvailabilityZone + ContainerInstances = [ + for instance in this.Instances do + match instance.Image with + | None -> raiseFarmer $"Missing image tag for container named '{instance.Name}'." + | Some image -> {| + Name = instance.Name + Image = image + Command = instance.Command + Ports = instance.Ports |> Map.toSeq |> Seq.map fst |> Set + Cpu = instance.Cpu + Memory = instance.Memory + Gpu = instance.Gpu + EnvironmentVariables = instance.EnvironmentVariables + LivenessProbe = instance.LivenessProbe + ReadinessProbe = instance.ReadinessProbe + VolumeMounts = instance.VolumeMounts + |} + ] + Diagnostics = this.Diagnostics + DnsConfig = + if this.DnsConfig.IsSome && this.NetworkProfile.IsNone then + raiseFarmer "DNS configuration can only be set when attached to a virtual network." + else + this.DnsConfig + OperatingSystem = this.OperatingSystem + RestartPolicy = this.RestartPolicy + Identity = this.Identity + ImageRegistryCredentials = this.ImageRegistryCredentials + InitContainers = [ + for initContainer in this.InitContainers do + match initContainer.Image with + | None -> raiseFarmer $"Missing image tag for initContainer named '{initContainer.Name}'." + | Some image -> {| + Name = initContainer.Name + Image = image + Command = initContainer.Command + EnvironmentVariables = initContainer.EnvironmentVariables + VolumeMounts = initContainer.VolumeMounts + |} + ] + IpAddress = this.IpAddress + NetworkProfile = + if + this.NetworkProfile.IsSome + && (this.VirtualNetwork.IsSome || this.SubnetName.IsSome) + then + raiseFarmer + $"Should not set network profile on container group '{this.Name.Value}' when using vnet and subnet." + else + this.NetworkProfile + SubnetIds = + match this.VirtualNetwork, this.SubnetName with + | None, None -> [] + | Some(Managed vnetId), Some subnet -> + { + vnetId with Type = subnets Segments = [ subnet ] - } - |> Managed - |> List.singleton - | Some (Unmanaged vnetId), Some subnet -> - { vnetId with + } + |> Managed + |> List.singleton + | Some(Unmanaged vnetId), Some subnet -> + { + vnetId with Type = subnets Segments = [ subnet ] - } - |> Unmanaged - |> List.singleton - | Some vnetId, None -> - raiseFarmer - $"Missing subnet for attaching container group '{this.Name.Value}' to vnet '{vnetId.Name.Value}'." - | None, subnetName -> - raiseFarmer - $"Missing vnet for attaching container group '{this.Name.Value}' to subnet '{subnetName.Value}'." - Volumes = this.Volumes - Tags = this.Tags - Dependencies = this.Dependencies - } - ] + } + |> Unmanaged + |> List.singleton + | Some vnetId, None -> + raiseFarmer + $"Missing subnet for attaching container group '{this.Name.Value}' to vnet '{vnetId.Name.Value}'." + | None, subnetName -> + raiseFarmer + $"Missing vnet for attaching container group '{this.Name.Value}' to subnet '{subnetName.Value}'." + Volumes = this.Volumes + Tags = this.Tags + Dependencies = this.Dependencies + } + ] type ContainerGpuConfig = { Count: int; Sku: Gpu.Sku } @@ -227,70 +220,65 @@ type ContainerProbeType = | LivenessProbe | ReadinessProbe -type ContainerProbeConfig = - { - ProbeType: ContainerProbeType - Probe: ContainerProbe - } +type ContainerProbeConfig = { + ProbeType: ContainerProbeType + Probe: ContainerProbe +} type ContainerNetworkInterfaceIpConfig = { Name: ResourceName; Subnet: string } -type ContainerNetworkInterfaceConfiguration = - { - IpConfigs: ContainerNetworkInterfaceIpConfig list - } +type ContainerNetworkInterfaceConfiguration = { + IpConfigs: ContainerNetworkInterfaceIpConfig list +} -type NetworkProfileConfig = - { - Name: ResourceName - ContainerNetworkInterfaceConfigurations: ContainerNetworkInterfaceConfiguration list - VirtualNetwork: LinkedResource - Tags: Map - } +type NetworkProfileConfig = { + Name: ResourceName + ContainerNetworkInterfaceConfigurations: ContainerNetworkInterfaceConfiguration list + VirtualNetwork: LinkedResource + Tags: Map +} with interface IBuilder with member this.ResourceId = networkProfiles.resourceId this.Name - member this.BuildResources location = - [ - { - Name = this.Name - Location = location - Dependencies = - [ - match this.VirtualNetwork with - | Managed resId -> resId // Only generate dependency if this is managed by Farmer (same template) - | _ -> () - ] - |> Set.ofList - ContainerNetworkInterfaceConfigurations = - this.ContainerNetworkInterfaceConfigurations - |> List.map (fun ifconfig -> - {| - IpConfigs = - ifconfig.IpConfigs - |> List.map (fun ipConfig -> - {| - Name = ipConfig.Name - SubnetName = ResourceName ipConfig.Subnet - |}) - |}) - VirtualNetwork = + member this.BuildResources location = [ + { + Name = this.Name + Location = location + Dependencies = + [ match this.VirtualNetwork with - | Managed resId - | Unmanaged resId -> resId - Tags = this.Tags - } - ] + | Managed resId -> resId // Only generate dependency if this is managed by Farmer (same template) + | _ -> () + ] + |> Set.ofList + ContainerNetworkInterfaceConfigurations = + this.ContainerNetworkInterfaceConfigurations + |> List.map (fun ifconfig -> {| + IpConfigs = + ifconfig.IpConfigs + |> List.map (fun ipConfig -> {| + Name = ipConfig.Name + SubnetName = ResourceName ipConfig.Subnet + |}) + |}) + VirtualNetwork = + match this.VirtualNetwork with + | Managed resId + | Unmanaged resId -> resId + Tags = this.Tags + } + ] type ContainerGroupBuilder() = - member private _.AddPort(state, portType, port) : ContainerGroupConfig = - { state with + member private _.AddPort(state, portType, port) : ContainerGroupConfig = { + state with IpAddress = match state.IpAddress with | Some ipAddresses -> - { ipAddresses with - Ports = ipAddresses.Ports.Add {| Protocol = portType; Port = port |} + { + ipAddresses with + Ports = ipAddresses.Ports.Add {| Protocol = portType; Port = port |} } |> Some | None -> @@ -299,28 +287,27 @@ type ContainerGroupBuilder() = Ports = [ {| Protocol = portType; Port = port |} ] |> Set.ofList } |> Some - } + } - member _.Yield _ = - { - Name = ResourceName.Empty - Diagnostics = None - DnsConfig = None - OperatingSystem = Linux - RestartPolicy = AlwaysRestart - Identity = ManagedIdentity.Empty - ImageRegistryCredentials = [] - InitContainers = [] - IpAddress = None - NetworkProfile = None - SubnetName = None - VirtualNetwork = None - Instances = [] - Volumes = Map.empty - AvailabilityZone = None - Tags = Map.empty - Dependencies = Set.empty - } + member _.Yield _ = { + Name = ResourceName.Empty + Diagnostics = None + DnsConfig = None + OperatingSystem = Linux + RestartPolicy = AlwaysRestart + Identity = ManagedIdentity.Empty + ImageRegistryCredentials = [] + InitContainers = [] + IpAddress = None + NetworkProfile = None + SubnetName = None + VirtualNetwork = None + Instances = [] + Volumes = Map.empty + AvailabilityZone = None + Tags = Map.empty + Dependencies = Set.empty + } member this.Run(state: ContainerGroupConfig) = // Automatically apply all public-facing ports to the container group itself. @@ -347,13 +334,13 @@ type ContainerGroupBuilder() = /// Sets the restart policy (default Always) [] - member _.RestartPolicy(state: ContainerGroupConfig, restartPolicy) = - { state with + member _.RestartPolicy(state: ContainerGroupConfig, restartPolicy) = { + state with RestartPolicy = restartPolicy - } + } - member private _.SetIpAddress(state: ContainerGroupConfig, ipAddressType, ports) = - { state with + member private _.SetIpAddress(state: ContainerGroupConfig, ipAddressType, ports) = { + state with IpAddress = { Type = ipAddressType @@ -363,7 +350,7 @@ type ContainerGroupBuilder() = |> Set } |> Some - } + } /// Sets the IP address to a public address with a DNS label [] @@ -377,60 +364,60 @@ type ContainerGroupBuilder() = /// Sets a network profile for the container's group. [] - member _.NetworkProfile(state: ContainerGroupConfig, networkProfileName: string) = - { state with + member _.NetworkProfile(state: ContainerGroupConfig, networkProfileName: string) = { + state with NetworkProfile = Some(ResourceName networkProfileName) - } + } - member _.NetworkProfile(state: ContainerGroupConfig, networkProfile: NetworkProfileConfig) = - { state with + member _.NetworkProfile(state: ContainerGroupConfig, networkProfile: NetworkProfileConfig) = { + state with NetworkProfile = Some networkProfile.Name - } + } /// Sets the name of a virtual network where this container group should be attached. [] - member _.VNetId(state: ContainerGroupConfig, vnetId: ResourceId) = - { state with + member _.VNetId(state: ContainerGroupConfig, vnetId: ResourceId) = { + state with VirtualNetwork = Some(Managed vnetId) - } + } - member _.VNetId(state: ContainerGroupConfig, vnetName: string) = - { state with + member _.VNetId(state: ContainerGroupConfig, vnetName: string) = { + state with VirtualNetwork = Some(Managed(virtualNetworks.resourceId (ResourceName vnetName))) - } + } - member _.VNetId(state: ContainerGroupConfig, vnetName: ResourceName) = - { state with + member _.VNetId(state: ContainerGroupConfig, vnetName: ResourceName) = { + state with VirtualNetwork = Some(Managed(virtualNetworks.resourceId vnetName)) - } + } /// Sets the name of a virtual network where this container group should be attached. [] - member _.LinkToVNetId(state: ContainerGroupConfig, vnetId: ResourceId) = - { state with + member _.LinkToVNetId(state: ContainerGroupConfig, vnetId: ResourceId) = { + state with VirtualNetwork = Some(Unmanaged vnetId) - } + } - member _.LinkToVNetId(state: ContainerGroupConfig, vnetName: string) = - { state with + member _.LinkToVNetId(state: ContainerGroupConfig, vnetName: string) = { + state with VirtualNetwork = Some(Unmanaged(virtualNetworks.resourceId (ResourceName vnetName))) - } + } - member _.LinkToVNetId(state: ContainerGroupConfig, vnetName: ResourceName) = - { state with + member _.LinkToVNetId(state: ContainerGroupConfig, vnetName: ResourceName) = { + state with VirtualNetwork = Some(Unmanaged(virtualNetworks.resourceId vnetName)) - } + } [] - member _.Subnet(state: ContainerGroupConfig, subnetName: string) = - { state with + member _.Subnet(state: ContainerGroupConfig, subnetName: string) = { + state with SubnetName = Some(ResourceName subnetName) - } + } - member _.Subnet(state: ContainerGroupConfig, subnetName: ResourceName) = - { state with + member _.Subnet(state: ContainerGroupConfig, subnetName: ResourceName) = { + state with SubnetName = Some subnetName - } + } /// Adds a UDP port to be externally accessible [] @@ -438,44 +425,44 @@ type ContainerGroupBuilder() = /// Adds container image registry credentials for images in this container group. [] - member _.AddRegistryCredentials(state: ContainerGroupConfig, credentials) = - { state with + member _.AddRegistryCredentials(state: ContainerGroupConfig, credentials) = { + state with ImageRegistryCredentials = state.ImageRegistryCredentials @ (credentials |> List.map ImageRegistryAuthentication.Credential) - } + } /// References one or more container image registries to get credentials for images in this container group. [] - member _.ReferenceRegistryCredentials(state: ContainerGroupConfig, resourceIds) = - { state with + member _.ReferenceRegistryCredentials(state: ContainerGroupConfig, resourceIds) = { + state with ImageRegistryCredentials = state.ImageRegistryCredentials @ (resourceIds |> List.map ImageRegistryAuthentication.ListCredentials) - } + } /// Adds container image registry managed identity credentials for images in this container group. [] - member _.ManagedIdentityRegistryCredentials(state: ContainerGroupConfig, credentials) = - { state with + member _.ManagedIdentityRegistryCredentials(state: ContainerGroupConfig, credentials) = { + state with ImageRegistryCredentials = state.ImageRegistryCredentials @ (credentials |> List.map ImageRegistryAuthentication.ManagedIdentityCredential) - } + } /// Adds a collection of init containers to this group that run once on startup before other containers in the group. [] - member _.AddInitContainers(state: ContainerGroupConfig, initContainers) = - { state with + member _.AddInitContainers(state: ContainerGroupConfig, initContainers) = { + state with InitContainers = state.InitContainers @ (Seq.toList initContainers) - } + } /// Adds a collection of container instances to this group [] - member _.AddInstances(state: ContainerGroupConfig, instances) = - { state with + member _.AddInstances(state: ContainerGroupConfig, instances) = { + state with Instances = state.Instances @ (Seq.toList instances) - } + } /// Adds volumes to the container group so they can be mounted on containers. [] @@ -490,14 +477,14 @@ type ContainerGroupBuilder() = /// Specify the availability zone for the container group. [] - member _.AvailabilityZones(state: ContainerGroupConfig, zone: string) = - { state with + member _.AvailabilityZones(state: ContainerGroupConfig, zone: string) = { + state with AvailabilityZone = Some zone - } + } [] - member _.EnableDiagnostics(state: ContainerGroupConfig, logType: LogType, workspaceBuilder: WorkspaceConfig) = - { state with + member _.EnableDiagnostics(state: ContainerGroupConfig, logType: LogType, workspaceBuilder: WorkspaceConfig) = { + state with Diagnostics = { LogType = logType @@ -505,17 +492,17 @@ type ContainerGroupBuilder() = LogAnalyticsWorkspace.WorkspaceResourceId(Managed((workspaceBuilder :> IBuilder).ResourceId)) } |> Some - } + } - member _.EnableDiagnostics(state: ContainerGroupConfig, logType: LogType, workspaceResourceId: ResourceId) = - { state with + member _.EnableDiagnostics(state: ContainerGroupConfig, logType: LogType, workspaceResourceId: ResourceId) = { + state with Diagnostics = { LogType = logType Workspace = LogAnalyticsWorkspace.WorkspaceResourceId(Managed(workspaceResourceId)) } |> Some - } + } [] member _.EnableDiagnosticsWorkspace @@ -525,13 +512,14 @@ type ContainerGroupBuilder() = workspaceId: string, workspaceKey: string ) = - { state with - Diagnostics = - { - LogType = logType - Workspace = LogAnalyticsWorkspace.WorkspaceKey(workspaceId, workspaceKey) - } - |> Some + { + state with + Diagnostics = + { + LogType = logType + Workspace = LogAnalyticsWorkspace.WorkspaceKey(workspaceId, workspaceKey) + } + |> Some } [] @@ -541,13 +529,14 @@ type ContainerGroupBuilder() = logType: LogType, workspaceResourceId: ResourceId ) = - { state with - Diagnostics = - { - LogType = logType - Workspace = LogAnalyticsWorkspace.WorkspaceResourceId(Unmanaged(workspaceResourceId)) - } - |> Some + { + state with + Diagnostics = + { + LogType = logType + Workspace = LogAnalyticsWorkspace.WorkspaceResourceId(Unmanaged(workspaceResourceId)) + } + |> Some } /// Specify DNS nameservers for the containers in the container group. @@ -555,16 +544,15 @@ type ContainerGroupBuilder() = member _.DnsNameServers(state: ContainerGroupConfig, nameServers: string list) = let dns = match state.DnsConfig with - | None -> - { + | None -> { + NameServers = nameServers + Options = [] + SearchDomains = [] + } + | Some dnsConfig -> { + dnsConfig with NameServers = nameServers - Options = [] - SearchDomains = [] - } - | Some dnsConfig -> - { dnsConfig with - NameServers = nameServers - } + } { state with DnsConfig = Some dns } @@ -573,12 +561,11 @@ type ContainerGroupBuilder() = member _.DnsOptions(state: ContainerGroupConfig, options: string list) = let dns = match state.DnsConfig with - | None -> - { - NameServers = [] - Options = options - SearchDomains = [] - } + | None -> { + NameServers = [] + Options = options + SearchDomains = [] + } | Some dnsConfig -> { dnsConfig with Options = options } { state with DnsConfig = Some dns } @@ -588,61 +575,58 @@ type ContainerGroupBuilder() = member _.DnsSearchDomains(state: ContainerGroupConfig, searchDomains: string list) = let dns = match state.DnsConfig with - | None -> - { - NameServers = [] - Options = [] + | None -> { + NameServers = [] + Options = [] + SearchDomains = searchDomains + } + | Some dnsConfig -> { + dnsConfig with SearchDomains = searchDomains - } - | Some dnsConfig -> - { dnsConfig with - SearchDomains = searchDomains - } + } { state with DnsConfig = Some dns } interface IIdentity with - member _.Add state updater = - { state with + member _.Add state updater = { + state with Identity = updater state.Identity - } + } interface ITaggable with - member _.Add state tags = - { state with + member _.Add state tags = { + state with Tags = state.Tags |> Map.merge tags - } + } interface IDependable with - member _.Add state newDeps = - { state with + member _.Add state newDeps = { + state with Dependencies = state.Dependencies + newDeps - } + } /// Creates an image registry credential with a generated SecureParameter for the password. -let registry (server: string) (username: string) (managedIdentity: ManagedIdentity) = - { - Server = server - Username = username - Password = SecureParameter $"{server}-password" - Identity = managedIdentity - } +let registry (server: string) (username: string) (managedIdentity: ManagedIdentity) = { + Server = server + Username = username + Password = SecureParameter $"{server}-password" + Identity = managedIdentity +} type ContainerInstanceBuilder() = - member _.Yield _ = - { - Name = ResourceName.Empty - Image = None - Command = List.empty - Ports = Map.empty - Cpu = 1.0 - Memory = 1.5 - Gpu = None - EnvironmentVariables = Map.empty - LivenessProbe = None - ReadinessProbe = None - VolumeMounts = Map.empty - } + member _.Yield _ = { + Name = ResourceName.Empty + Image = None + Command = List.empty + Ports = Map.empty + Cpu = 1.0 + Memory = 1.5 + Gpu = None + EnvironmentVariables = Map.empty + LivenessProbe = None + ReadinessProbe = None + VolumeMounts = Map.empty + } /// Sets the name of the container instance. [] @@ -652,10 +636,10 @@ type ContainerInstanceBuilder() = /// Sets the image of the container instance as a docker image tag. [] - member _.Image(state: ContainerInstanceConfig, image: string) = - { state with + member _.Image(state: ContainerInstanceConfig, image: string) = { + state with Image = Some(Containers.DockerImage.Parse image) - } + } /// Sets the image to a private docker image. [] @@ -666,23 +650,24 @@ type ContainerInstanceBuilder() = containerName: string, version: string ) = - { state with - Image = - Containers.DockerImage.PrivateImage(registry, containerName, Some version) - |> Some + { + state with + Image = + Containers.DockerImage.PrivateImage(registry, containerName, Some version) + |> Some } /// Sets the image to a public docker image. [] - member _.PublicDockerImage(state: ContainerInstanceConfig, containerName: string, version: string) = - { state with + member _.PublicDockerImage(state: ContainerInstanceConfig, containerName: string, version: string) = { + state with Image = Containers.DockerImage.PublicImage(containerName, Some version) |> Some - } + } - static member private AddPorts(state: ContainerInstanceConfig, accessibility, ports) = - { state with + static member private AddPorts(state: ContainerInstanceConfig, accessibility, ports) = { + state with Ports = ports |> Seq.fold (fun all port -> all.Add(port, accessibility)) state.Ports - } + } /// Sets the ports the container instance exposes. These will automatically be applied to the container group. [] @@ -707,45 +692,45 @@ type ContainerInstanceBuilder() = /// Sets the maximum gigabytes of memory the container instance may use [] - member _.Memory(state: ContainerInstanceConfig, memory: float) = - { state with + member _.Memory(state: ContainerInstanceConfig, memory: float) = { + state with Memory = System.Math.Round(memory / 1., 1) * 1. - } + } /// Enables container instances with gpus [] - member _.Gpu(state: ContainerInstanceConfig, (gpu: ContainerGpuConfig)) = - { state with + member _.Gpu(state: ContainerInstanceConfig, (gpu: ContainerGpuConfig)) = { + state with Gpu = Some { Count = gpu.Count; Sku = gpu.Sku } - } + } [] - member _.EnvironmentVariables(state: ContainerInstanceConfig, envVars) = - { state with + member _.EnvironmentVariables(state: ContainerInstanceConfig, envVars) = { + state with EnvironmentVariables = Map.ofList envVars - } + } member this.EnvironmentVariables(state, envVars) = this.EnvironmentVariables(state, envVars |> List.map (fun (k, v) -> k, EnvValue v)) /// Adds a volume mount to the container [] - member _.AddVolumeMount(state: ContainerInstanceConfig, volumeName, mountPath) = - { state with + member _.AddVolumeMount(state: ContainerInstanceConfig, volumeName, mountPath) = { + state with VolumeMounts = state.VolumeMounts |> Map.add volumeName mountPath - } + } /// Adds commands to execute within the container instance [] - member _.CommandLine(state: ContainerInstanceConfig, command) = - { state with + member _.CommandLine(state: ContainerInstanceConfig, command) = { + state with Command = state.Command @ command - } + } /// Set readiness and liveness probes on the container. [] - member _.Probes(state: ContainerInstanceConfig, probes: (ContainerProbeConfig) seq) = - { state with + member _.Probes(state: ContainerInstanceConfig, probes: (ContainerProbeConfig) seq) = { + state with LivenessProbe = probes |> Seq.tryFind (fun p -> p.ProbeType = ContainerProbeType.LivenessProbe) @@ -754,95 +739,93 @@ type ContainerInstanceBuilder() = probes |> Seq.tryFind (fun p -> p.ProbeType = ContainerProbeType.ReadinessProbe) |> Option.map (fun p -> p.Probe) - } + } type ProbeBuilder(probeType: ContainerProbeType) = - member _.Yield _ = - { - ProbeType = probeType - Probe = - { - Exec = [] - HttpGet = None - InitialDelaySeconds = None - PeriodSeconds = None - FailureThreshold = None - SuccessThreshold = None - TimeoutSeconds = None - } + member _.Yield _ = { + ProbeType = probeType + Probe = { + Exec = [] + HttpGet = None + InitialDelaySeconds = None + PeriodSeconds = None + FailureThreshold = None + SuccessThreshold = None + TimeoutSeconds = None } + } /// The URI for a GET request for a health or readiness check on this container. The hostname in the URI is ignored. [] - member _.HttpGet(state: (ContainerProbeConfig), uri: string) = - { state with - Probe = - { state.Probe with + member _.HttpGet(state: (ContainerProbeConfig), uri: string) = { + state with + Probe = { + state.Probe with HttpGet = uri |> System.Uri |> Some - } - } + } + } /// A command to execute on this container to check its health or readiness. [] - member _.Exec(state: (ContainerProbeConfig), commands: string list) = - { state with + member _.Exec(state: (ContainerProbeConfig), commands: string list) = { + state with Probe = { state.Probe with Exec = commands } - } + } - member _.Exec(state: (ContainerProbeConfig), command: string) = - { state with + member _.Exec(state: (ContainerProbeConfig), command: string) = { + state with Probe = { state.Probe with Exec = [ command ] } - } + } /// The probe will not run until this delay after container startup. Default is 0 - runs immediately. [] - member _.InitialDelay(state: (ContainerProbeConfig), delay: int) = - { state with - Probe = - { state.Probe with + member _.InitialDelay(state: (ContainerProbeConfig), delay: int) = { + state with + Probe = { + state.Probe with InitialDelaySeconds = delay |> Some - } - } + } + } /// How often to execute the probe against the container - default is 10 seconds. [] - member _.PeriodSeconds(state: (ContainerProbeConfig), delay: int) = - { state with - Probe = - { state.Probe with + member _.PeriodSeconds(state: (ContainerProbeConfig), delay: int) = { + state with + Probe = { + state.Probe with PeriodSeconds = delay |> Some - } - } + } + } /// Number of failures before this container is considered unhealthy - default is 3. [] - member _.FailureThreshold(state: (ContainerProbeConfig), delay: int) = - { state with - Probe = - { state.Probe with + member _.FailureThreshold(state: (ContainerProbeConfig), delay: int) = { + state with + Probe = { + state.Probe with FailureThreshold = delay |> Some - } - } + } + } /// Number of successes before this container is considered healthy - default is 1. [] - member _.SuccessThreshold(state: (ContainerProbeConfig), delay: int) = - { state with - Probe = - { state.Probe with + member _.SuccessThreshold(state: (ContainerProbeConfig), delay: int) = { + state with + Probe = { + state.Probe with SuccessThreshold = delay |> Some - } - } + } + } /// Number of seconds for the probe to run before failing due to a timeout - default is 1 second. [] - member _.TimeoutSeconds(state: (ContainerProbeConfig), delay: int) = - { state with - Probe = - { state.Probe with + member _.TimeoutSeconds(state: (ContainerProbeConfig), delay: int) = { + state with + Probe = { + state.Probe with TimeoutSeconds = delay |> Some - } - } + } + } let liveness = ProbeBuilder(LivenessProbe) let readiness = ProbeBuilder(ReadinessProbe) @@ -862,14 +845,13 @@ type GpuBuilder() = let containerInstanceGpu = GpuBuilder() type InitContainerBuilder() = - member _.Yield _ = - { - Name = ResourceName.Empty - Image = None - Command = List.empty - EnvironmentVariables = Map.empty - VolumeMounts = Map.empty - } + member _.Yield _ = { + Name = ResourceName.Empty + Image = None + Command = List.empty + EnvironmentVariables = Map.empty + VolumeMounts = Map.empty + } /// Sets the name of the init container. [] @@ -879,63 +861,62 @@ type InitContainerBuilder() = /// Sets the image of the init container. [] - member _.Image(state: InitContainerConfig, image: string) = - { state with + member _.Image(state: InitContainerConfig, image: string) = { + state with Image = Some(Containers.DockerImage.Parse image) - } + } /// Sets the image to a private docker image. [] - member _.PrivateDockerImage(state: InitContainerConfig, registry: string, containerName: string, version: string) = - { state with + member _.PrivateDockerImage(state: InitContainerConfig, registry: string, containerName: string, version: string) = { + state with Image = Containers.DockerImage.PrivateImage(registry, containerName, Some version) |> Some - } + } /// Sets the image to a public docker image. [] - member _.PublicDockerImage(state: InitContainerConfig, containerName: string, version: string) = - { state with + member _.PublicDockerImage(state: InitContainerConfig, containerName: string, version: string) = { + state with Image = Containers.DockerImage.PublicImage(containerName, Some version) |> Some - } + } /// Sets the environment variables for the init container. [] - member _.EnvironmentVariables(state: InitContainerConfig, envVars) = - { state with + member _.EnvironmentVariables(state: InitContainerConfig, envVars) = { + state with EnvironmentVariables = Map.ofList envVars - } + } member this.EnvironmentVariables(state, envVars) = this.EnvironmentVariables(state, envVars |> List.map (fun (k, v) -> k, EnvValue v)) /// Adds a volume mount to the init container [] - member _.AddVolumeMount(state: InitContainerConfig, volumeName, mountPath) = - { state with + member _.AddVolumeMount(state: InitContainerConfig, volumeName, mountPath) = { + state with VolumeMounts = state.VolumeMounts |> Map.add volumeName mountPath - } + } /// Adds commands to execute within the init container [] - member _.CommandLine(state: InitContainerConfig, command) = - { state with + member _.CommandLine(state: InitContainerConfig, command) = { + state with Command = state.Command @ command - } + } let containerGroup = ContainerGroupBuilder() let containerInstance = ContainerInstanceBuilder() let initContainer = InitContainerBuilder() type NetworkProfileBuilder() = - member _.Yield _ = - { - Name = ResourceName.Empty - ContainerNetworkInterfaceConfigurations = [] - VirtualNetwork = Managed(virtualNetworks.resourceId ResourceName.Empty) - Tags = Map.empty - } + member _.Yield _ = { + Name = ResourceName.Empty + ContainerNetworkInterfaceConfigurations = [] + VirtualNetwork = Managed(virtualNetworks.resourceId ResourceName.Empty) + Tags = Map.empty + } /// Sets the name of the network profile instance [] @@ -943,70 +924,66 @@ type NetworkProfileBuilder() = /// Sets a single target subnet for the network profile (typical case of single subnet) [] - member _.SubnetName(state: NetworkProfileConfig, subnet) = - { state with - ContainerNetworkInterfaceConfigurations = - [ - { - IpConfigs = - [ - { - Name = ResourceName "ipconfig" - Subnet = subnet - } - ] - } - ] - } + member _.SubnetName(state: NetworkProfileConfig, subnet) = { + state with + ContainerNetworkInterfaceConfigurations = [ + { + IpConfigs = [ + { + Name = ResourceName "ipconfig" + Subnet = subnet + } + ] + } + ] + } /// Sets a single target named ip configuration for the network profile (typical case of single subnet) [] - member _.IpConfig(state: NetworkProfileConfig, ipConfigName: string, subnetName: string) = - { state with - ContainerNetworkInterfaceConfigurations = - [ - { - IpConfigs = - [ - { - Name = ResourceName ipConfigName - Subnet = subnetName - } - ] - } - ] - } + member _.IpConfig(state: NetworkProfileConfig, ipConfigName: string, subnetName: string) = { + state with + ContainerNetworkInterfaceConfigurations = [ + { + IpConfigs = [ + { + Name = ResourceName ipConfigName + Subnet = subnetName + } + ] + } + ] + } /// Sets multiple subnet IP configs for the network profile to connect to multiple subnets. [] - member _.AddIpConfigs(state: NetworkProfileConfig, configs) = - { state with + member _.AddIpConfigs(state: NetworkProfileConfig, configs) = { + state with ContainerNetworkInterfaceConfigurations = state.ContainerNetworkInterfaceConfigurations @ configs - } + } /// Sets the virtual network for the profile [] - member _.VirtualNetwork(state: NetworkProfileConfig, vnet) = - { state with + member _.VirtualNetwork(state: NetworkProfileConfig, vnet) = { + state with VirtualNetwork = Managed(virtualNetworks.resourceId (ResourceName vnet)) - } + } /// Links to an existing vnet. [] - member _.LinkToVirtualNetwork(state: NetworkProfileConfig, vnet) = - { state with + member _.LinkToVirtualNetwork(state: NetworkProfileConfig, vnet) = { + state with VirtualNetwork = Unmanaged(virtualNetworks.resourceId (ResourceName vnet)) - } + } - member _.LinkToVirtualNetwork(state: NetworkProfileConfig, resourceId) = - { state with + member _.LinkToVirtualNetwork(state: NetworkProfileConfig, resourceId) = { + state with VirtualNetwork = Unmanaged resourceId - } + } interface ITaggable with - member _.Add state tags = - { state with + member _.Add state tags = { + state with Tags = state.Tags |> Map.merge tags - } + } let networkProfile = NetworkProfileBuilder() diff --git a/src/Farmer/Builders/Builders.ContainerRegistry.fs b/src/Farmer/Builders/Builders.ContainerRegistry.fs index 343d558c2..c47baae3f 100644 --- a/src/Farmer/Builders/Builders.ContainerRegistry.fs +++ b/src/Farmer/Builders/Builders.ContainerRegistry.fs @@ -5,13 +5,12 @@ open Farmer open Farmer.ContainerRegistry open Farmer.Arm.ContainerRegistry -type ContainerRegistryConfig = - { - Name: ResourceName - Sku: Sku - AdminUserEnabled: bool - Tags: Map - } +type ContainerRegistryConfig = { + Name: ResourceName + Sku: Sku + AdminUserEnabled: bool + Tags: Map +} with member this.LoginServer = $"reference(resourceId('Microsoft.ContainerRegistry/registries', '{this.Name.Value}'),'2019-05-01').loginServer" @@ -35,39 +34,33 @@ type ContainerRegistryConfig = interface IBuilder with member this.ResourceId = registries.resourceId this.Name - member this.BuildResources location = - [ - { - Name = this.Name - Location = location - Sku = this.Sku - AdminUserEnabled = this.AdminUserEnabled - Tags = this.Tags - } - ] + member this.BuildResources location = [ + { + Name = this.Name + Location = location + Sku = this.Sku + AdminUserEnabled = this.AdminUserEnabled + Tags = this.Tags + } + ] type ContainerRegistryBuilder() = - member _.Yield _ = - { - Name = ResourceName.Empty - Sku = Basic - AdminUserEnabled = false - Tags = Map.empty - } + member _.Yield _ = { + Name = ResourceName.Empty + Sku = Basic + AdminUserEnabled = false + Tags = Map.empty + } [] /// Sets the name of the Azure Container Registry instance. - member _.Name(state: ContainerRegistryConfig, name: ResourceName) = - { state with + member _.Name(state: ContainerRegistryConfig, name: ResourceName) = { + state with Name = - ContainerRegistryValidation - .ContainerRegistryName - .Create( - name - ) - .OkValue - .ResourceName - } + ContainerRegistryValidation.ContainerRegistryName + .Create(name) + .OkValue.ResourceName + } member this.Name(state: ContainerRegistryConfig, name: string) = this.Name(state, ResourceName name) @@ -81,9 +74,9 @@ type ContainerRegistryBuilder() = member _.EnableAdminUser(state: ContainerRegistryConfig) = { state with AdminUserEnabled = true } interface ITaggable with - member _.Add state tags = - { state with + member _.Add state tags = { + state with Tags = state.Tags |> Map.merge tags - } + } let containerRegistry = ContainerRegistryBuilder() diff --git a/src/Farmer/Builders/Builders.ContainerService.fs b/src/Farmer/Builders/Builders.ContainerService.fs index 2575198bb..fbb0c92c5 100644 --- a/src/Farmer/Builders/Builders.ContainerService.fs +++ b/src/Farmer/Builders/Builders.ContainerService.fs @@ -9,53 +9,49 @@ open Farmer.Arm.RoleAssignment open Farmer.Identity open Farmer.Vm -type AgentPoolConfig = - { - Name: ResourceName - Count: int - MaxPods: int option - Mode: AgentPoolMode - OsDiskSize: int - OsType: OS - VmSize: VMSize - VirtualNetworkName: ResourceName option - SubnetName: ResourceName option +type AgentPoolConfig = { + Name: ResourceName + Count: int + MaxPods: int option + Mode: AgentPoolMode + OsDiskSize: int + OsType: OS + VmSize: VMSize + VirtualNetworkName: ResourceName option + SubnetName: ResourceName option +} with + + static member Default = { + Name = ResourceName.Empty + Count = 1 + // Default for CNI is 30, Kubenet default is 110 + // https://docs.microsoft.com/en-us/azure/aks/configure-azure-cni#maximum-pods-per-node + MaxPods = None + Mode = System + OsDiskSize = 0 + OsType = OS.Linux + VirtualNetworkName = None + SubnetName = None + VmSize = Standard_DS2_v2 } - static member Default = - { - Name = ResourceName.Empty - Count = 1 - // Default for CNI is 30, Kubenet default is 110 - // https://docs.microsoft.com/en-us/azure/aks/configure-azure-cni#maximum-pods-per-node - MaxPods = None - Mode = System - OsDiskSize = 0 - OsType = OS.Linux - VirtualNetworkName = None - SubnetName = None - VmSize = Standard_DS2_v2 - } - -type ApiServerAccessProfileConfig = - { - AuthorizedIPRanges: string list - EnablePrivateCluster: bool option - } - -type NetworkProfileConfig = - { - NetworkPlugin: ContainerService.NetworkPlugin option - /// If no address is specified, this will use the 2nd address in the service address CIDR - DnsServiceIP: System.Net.IPAddress option - /// Usually the default 172.17.0.1/16 is acceptable. - DockerBridgeCidr: IPAddressCidr option - /// Load balancer SKU (defaults to basic) - LoadBalancerSku: LoadBalancer.Sku option - /// Private IP address CIDR for services in the cluster which should not overlap with the vnet - /// for the cluster or peer vnets. Defaults to 10.244.0.0/16. - ServiceCidr: IPAddressCidr option - } +type ApiServerAccessProfileConfig = { + AuthorizedIPRanges: string list + EnablePrivateCluster: bool option +} + +type NetworkProfileConfig = { + NetworkPlugin: ContainerService.NetworkPlugin option + /// If no address is specified, this will use the 2nd address in the service address CIDR + DnsServiceIP: System.Net.IPAddress option + /// Usually the default 172.17.0.1/16 is acceptable. + DockerBridgeCidr: IPAddressCidr option + /// Load balancer SKU (defaults to basic) + LoadBalancerSku: LoadBalancer.Sku option + /// Private IP address CIDR for services in the cluster which should not overlap with the vnet + /// for the cluster or peer vnets. Defaults to 10.244.0.0/16. + ServiceCidr: IPAddressCidr option +} type AddonConfig = | AciConnectorLinux of FeatureFlag @@ -64,165 +60,155 @@ type AddonConfig = | KubeDashboard of FeatureFlag | OmsAgent of OmsAgent - static member BuildConfig(addons: AddonConfig list) : AddonProfileConfig = - { - // TODO: Clean up with active pattern - AciConnectorLinux = - addons - |> List.tryFind (function - | AciConnectorLinux _ -> true - | _ -> false) - |> function - | Some (AciConnectorLinux status) -> Some { AciConnectorLinux.Status = status } - | _ -> None - HttpApplicationRouting = - addons - |> List.tryFind (function - | HttpApplicationRouting _ -> true - | _ -> false) - |> function - | Some (HttpApplicationRouting status) -> - Some - { - HttpApplicationRouting.Status = status - } - | _ -> None - IngressApplicationGateway = - addons - |> List.tryFind (function - | IngressApplicationGateway _ -> true - | _ -> false) - |> function - | Some (IngressApplicationGateway gw) -> Some gw - | _ -> None - KubeDashboard = - addons - |> List.tryFind (function - | KubeDashboard _ -> true - | _ -> false) - |> function - | Some (KubeDashboard status) -> Some { KubeDashboard.Status = status } - | _ -> None - OmsAgent = - addons - |> List.tryFind (function - | OmsAgent _ -> true - | _ -> false) - |> function - | Some (OmsAgent oms) -> Some oms - | _ -> None - } - -type AksConfig = - { - Name: ResourceName - AddonProfiles: AddonConfig list - AgentPools: AgentPoolConfig list - Dependencies: ResourceId Set - DependencyExpressions: ArmExpression Set - DnsPrefix: string - EnableRBAC: bool - Identity: ManagedIdentity - IdentityProfile: ManagedClusterIdentityProfile option - ApiServerAccessProfile: ApiServerAccessProfileConfig option - LinuxProfile: (string * string list) option - NetworkProfile: NetworkProfileConfig option - ServicePrincipalClientID: string - WindowsProfileAdminUserName: string option + static member BuildConfig(addons: AddonConfig list) : AddonProfileConfig = { + // TODO: Clean up with active pattern + AciConnectorLinux = + addons + |> List.tryFind (function + | AciConnectorLinux _ -> true + | _ -> false) + |> function + | Some(AciConnectorLinux status) -> Some { AciConnectorLinux.Status = status } + | _ -> None + HttpApplicationRouting = + addons + |> List.tryFind (function + | HttpApplicationRouting _ -> true + | _ -> false) + |> function + | Some(HttpApplicationRouting status) -> + Some { + HttpApplicationRouting.Status = status + } + | _ -> None + IngressApplicationGateway = + addons + |> List.tryFind (function + | IngressApplicationGateway _ -> true + | _ -> false) + |> function + | Some(IngressApplicationGateway gw) -> Some gw + | _ -> None + KubeDashboard = + addons + |> List.tryFind (function + | KubeDashboard _ -> true + | _ -> false) + |> function + | Some(KubeDashboard status) -> Some { KubeDashboard.Status = status } + | _ -> None + OmsAgent = + addons + |> List.tryFind (function + | OmsAgent _ -> true + | _ -> false) + |> function + | Some(OmsAgent oms) -> Some oms + | _ -> None } +type AksConfig = { + Name: ResourceName + AddonProfiles: AddonConfig list + AgentPools: AgentPoolConfig list + Dependencies: ResourceId Set + DependencyExpressions: ArmExpression Set + DnsPrefix: string + EnableRBAC: bool + Identity: ManagedIdentity + IdentityProfile: ManagedClusterIdentityProfile option + ApiServerAccessProfile: ApiServerAccessProfileConfig option + LinuxProfile: (string * string list) option + NetworkProfile: NetworkProfileConfig option + ServicePrincipalClientID: string + WindowsProfileAdminUserName: string option +} with + member private this.ResourceId = managedClusters.resourceId this.Name member this.SystemIdentity = SystemIdentity this.ResourceId interface IBuilder with member this.ResourceId = this.ResourceId - member this.BuildResources location = - [ - { - Name = this.Name - Location = location - AddOnProfiles = - match this.AddonProfiles with - | [] -> None - | addons -> addons |> AddonConfig.BuildConfig |> Some - Dependencies = this.Dependencies - DependencyExpressions = this.DependencyExpressions - DnsPrefix = - if String.IsNullOrWhiteSpace this.DnsPrefix then - $"{this.Name.Value}-%x{this.Name.Value.GetHashCode()}" - else - this.DnsPrefix - EnableRBAC = this.EnableRBAC - Identity = this.Identity - IdentityProfile = this.IdentityProfile - AgentPoolProfiles = - match this.AgentPools with - | [] -> - [ - { AgentPoolConfig.Default with - Count = 3 - } - ] - | agentPools -> agentPools - |> List.map (fun agentPool -> - {| - Name = agentPool.Name - Count = agentPool.Count - MaxPods = agentPool.MaxPods - Mode = agentPool.Mode - OsDiskSize = agentPool.OsDiskSize - OsType = agentPool.OsType - SubnetName = agentPool.SubnetName - VmSize = agentPool.VmSize - VirtualNetworkName = agentPool.VirtualNetworkName - |}) - ApiServerAccessProfile = - this.ApiServerAccessProfile - |> Option.map (fun apiAccess -> - {| - AuthorizedIPRanges = apiAccess.AuthorizedIPRanges - EnablePrivateCluster = apiAccess.EnablePrivateCluster - |}) - LinuxProfile = - this.LinuxProfile - |> Option.map (fun (username, keys) -> - {| - AdminUserName = username - PublicKeys = keys - |}) - NetworkProfile = - this.NetworkProfile - |> Option.map (fun netProfile -> - {| - NetworkPlugin = netProfile.NetworkPlugin - DnsServiceIP = - match netProfile.DnsServiceIP with - | Some ip -> Some ip - | None -> - netProfile.ServiceCidr - |> Option.map (IPAddressCidr.addresses >> Seq.skip 2 >> Seq.head) - DockerBridgeCidr = netProfile.DockerBridgeCidr - LoadBalancerSku = netProfile.LoadBalancerSku - ServiceCidr = netProfile.ServiceCidr - |}) - ServicePrincipalProfile = - {| - ClientId = this.ServicePrincipalClientID - ClientSecret = - match this.ServicePrincipalClientID with - | "msi" -> None - | _ -> Some(SecureParameter $"client-secret-for-{this.Name.Value}") - |} - WindowsProfile = - this.WindowsProfileAdminUserName - |> Option.map (fun username -> - {| - AdminUserName = username - AdminPassword = SecureParameter $"admin-password-for-{this.Name.Value}" - |}) - } - ] + member this.BuildResources location = [ + { + Name = this.Name + Location = location + AddOnProfiles = + match this.AddonProfiles with + | [] -> None + | addons -> addons |> AddonConfig.BuildConfig |> Some + Dependencies = this.Dependencies + DependencyExpressions = this.DependencyExpressions + DnsPrefix = + if String.IsNullOrWhiteSpace this.DnsPrefix then + $"{this.Name.Value}-%x{this.Name.Value.GetHashCode()}" + else + this.DnsPrefix + EnableRBAC = this.EnableRBAC + Identity = this.Identity + IdentityProfile = this.IdentityProfile + AgentPoolProfiles = + match this.AgentPools with + | [] -> [ + { + AgentPoolConfig.Default with + Count = 3 + } + ] + | agentPools -> agentPools + |> List.map (fun agentPool -> {| + Name = agentPool.Name + Count = agentPool.Count + MaxPods = agentPool.MaxPods + Mode = agentPool.Mode + OsDiskSize = agentPool.OsDiskSize + OsType = agentPool.OsType + SubnetName = agentPool.SubnetName + VmSize = agentPool.VmSize + VirtualNetworkName = agentPool.VirtualNetworkName + |}) + ApiServerAccessProfile = + this.ApiServerAccessProfile + |> Option.map (fun apiAccess -> {| + AuthorizedIPRanges = apiAccess.AuthorizedIPRanges + EnablePrivateCluster = apiAccess.EnablePrivateCluster + |}) + LinuxProfile = + this.LinuxProfile + |> Option.map (fun (username, keys) -> {| + AdminUserName = username + PublicKeys = keys + |}) + NetworkProfile = + this.NetworkProfile + |> Option.map (fun netProfile -> {| + NetworkPlugin = netProfile.NetworkPlugin + DnsServiceIP = + match netProfile.DnsServiceIP with + | Some ip -> Some ip + | None -> + netProfile.ServiceCidr + |> Option.map (IPAddressCidr.addresses >> Seq.skip 2 >> Seq.head) + DockerBridgeCidr = netProfile.DockerBridgeCidr + LoadBalancerSku = netProfile.LoadBalancerSku + ServiceCidr = netProfile.ServiceCidr + |}) + ServicePrincipalProfile = {| + ClientId = this.ServicePrincipalClientID + ClientSecret = + match this.ServicePrincipalClientID with + | "msi" -> None + | _ -> Some(SecureParameter $"client-secret-for-{this.Name.Value}") + |} + WindowsProfile = + this.WindowsProfileAdminUserName + |> Option.map (fun username -> {| + AdminUserName = username + AdminPassword = SecureParameter $"admin-password-for-{this.Name.Value}" + |}) + } + ] type AgentPoolBuilder() = member _.Yield _ = AgentPoolConfig.Default @@ -252,10 +238,10 @@ type AgentPoolBuilder() = /// Sets the name of a virtual network subnet where this AKS cluster should be attached. [] - member _.SubnetName(state: AgentPoolConfig, subnetName) = - { state with + member _.SubnetName(state: AgentPoolConfig, subnetName) = { + state with SubnetName = Some(ResourceName subnetName) - } + } /// Sets the size of the VM's in the agent pool. [] @@ -263,10 +249,10 @@ type AgentPoolBuilder() = /// Sets the name of a virtual network in the same region where this AKS cluster should be attached. [] - member _.VNetName(state: AgentPoolConfig, vnetName) = - { state with + member _.VNetName(state: AgentPoolConfig, vnetName) = { + state with VirtualNetworkName = Some(ResourceName vnetName) - } + } /// Builds an AKS cluster agent pool ARM resource definition let agentPool = AgentPoolBuilder() @@ -274,23 +260,22 @@ let agentPool = AgentPoolBuilder() type NetworkProfileBuilder() = /// Sets the SKU to be used for the load balancer. [] - member _.LoadBalancerSku(state: NetworkProfileConfig, sku: LoadBalancer.Sku) = - { state with + member _.LoadBalancerSku(state: NetworkProfileConfig, sku: LoadBalancer.Sku) = { + state with LoadBalancerSku = Some sku - } + } /// Builds a configuration for using the Azure CNI plugin. type KubenetBuilder() = inherit NetworkProfileBuilder() - member _.Yield _ = - { - NetworkPlugin = Some ContainerService.NetworkPlugin.Kubenet - LoadBalancerSku = None - DnsServiceIP = None - DockerBridgeCidr = None - ServiceCidr = None - } + member _.Yield _ = { + NetworkPlugin = Some ContainerService.NetworkPlugin.Kubenet + LoadBalancerSku = None + DnsServiceIP = None + DockerBridgeCidr = None + ServiceCidr = None + } let kubenetNetworkProfile = KubenetBuilder() @@ -298,45 +283,44 @@ let kubenetNetworkProfile = KubenetBuilder() type AzureCniBuilder() = inherit NetworkProfileBuilder() - member _.Yield _ = - { - NetworkPlugin = Some ContainerService.NetworkPlugin.AzureCni - LoadBalancerSku = None - DnsServiceIP = None - DockerBridgeCidr = IPAddressCidr.parse "172.17.0.1/16" |> Some - ServiceCidr = IPAddressCidr.parse "10.224.0.0/16" |> Some - } + member _.Yield _ = { + NetworkPlugin = Some ContainerService.NetworkPlugin.AzureCni + LoadBalancerSku = None + DnsServiceIP = None + DockerBridgeCidr = IPAddressCidr.parse "172.17.0.1/16" |> Some + ServiceCidr = IPAddressCidr.parse "10.224.0.0/16" |> Some + } - member _.Run(config: NetworkProfileConfig) = - { config with + member _.Run(config: NetworkProfileConfig) = { + config with DnsServiceIP = match config.DnsServiceIP with | Some ip -> Some ip | None -> config.ServiceCidr |> Option.map (IPAddressCidr.addresses >> Seq.skip 2 >> Seq.head) - } + } /// Sets the docker bridge CIDR to a network other than the default 17.17.0.1/16. [] - member _.DockerBridge(state: NetworkProfileConfig, dockerBridge) = - { state with + member _.DockerBridge(state: NetworkProfileConfig, dockerBridge) = { + state with DockerBridgeCidr = IPAddressCidr.parse dockerBridge |> Some - } + } /// Sets the DNS service IP - must be within the service CIDR, default is the second address in the service CIDR. [] - member _.DnsServiceIP(state: NetworkProfileConfig, dnsIp: string) = - { state with + member _.DnsServiceIP(state: NetworkProfileConfig, dnsIp: string) = { + state with DnsServiceIP = System.Net.IPAddress.Parse dnsIp |> Some - } + } /// Sets the service cidr to a network other than the default 10.224.0.0/16. [] - member _.ServiceCidr(state: NetworkProfileConfig, serviceCidr) = - { state with + member _.ServiceCidr(state: NetworkProfileConfig, serviceCidr) = { + state with ServiceCidr = IPAddressCidr.parse serviceCidr |> Some - } + } let azureCniNetworkProfile = AzureCniBuilder() @@ -361,23 +345,22 @@ let private (|PrivateClusterEnabled|_|) = | _ -> None) type AksBuilder() = - member _.Yield _ = - { - Name = ResourceName.Empty - Dependencies = Set.empty - DependencyExpressions = Set.empty - AddonProfiles = [] - AgentPools = [] - DnsPrefix = "" - EnableRBAC = false - Identity = ManagedIdentity.Empty - IdentityProfile = None - ApiServerAccessProfile = None - LinuxProfile = None - NetworkProfile = None - ServicePrincipalClientID = "" - WindowsProfileAdminUserName = None - } + member _.Yield _ = { + Name = ResourceName.Empty + Dependencies = Set.empty + DependencyExpressions = Set.empty + AddonProfiles = [] + AgentPools = [] + DnsPrefix = "" + EnableRBAC = false + Identity = ManagedIdentity.Empty + IdentityProfile = None + ApiServerAccessProfile = None + LinuxProfile = None + NetworkProfile = None + ServicePrincipalClientID = "" + WindowsProfileAdminUserName = None + } member _.Run(config: AksConfig) = match config.NetworkProfile, config.ApiServerAccessProfile with @@ -404,54 +387,54 @@ type AksBuilder() = member _.EnableRBAC(state: AksConfig) = { state with EnableRBAC = true } /// Sets the managed identity on this cluster. interface IIdentity with - member _.Add state updater = - { state with + member _.Add state updater = { + state with Identity = updater state.Identity - } + } /// Support for "depends_on" interface IDependable with - member _.Add state newDeps = - { state with + member _.Add state newDeps = { + state with Dependencies = state.Dependencies + newDeps - } + } [] - member _.DependencyExpressions(state: AksConfig, dependencyExpr: ArmExpression) = - { state with + member _.DependencyExpressions(state: AksConfig, dependencyExpr: ArmExpression) = { + state with DependencyExpressions = state.DependencyExpressions.Add dependencyExpr - } + } /// Adds agent pools to the AKS cluster. [] - member _.AddAgentPools(state: AksConfig, pools) = - { state with + member _.AddAgentPools(state: AksConfig, pools) = { + state with AgentPools = state.AgentPools @ pools - } + } /// Adds an agent pool to the AKS cluster. [] - member _.AddAgentPool(state: AksConfig, pool) = - { state with + member _.AddAgentPool(state: AksConfig, pool) = { + state with AgentPools = state.AgentPools @ [ pool ] - } + } /// Enables a private cluster so it is not publicly accessible - only accessed from a virtual network. [] member _.EnablePrivateCluster(state: AksConfig, enabled: bool) = let accessProfile = match state.ApiServerAccessProfile with - | None -> - { - AuthorizedIPRanges = [] - EnablePrivateCluster = Some true - } - | Some profile -> - { profile with + | None -> { + AuthorizedIPRanges = [] + EnablePrivateCluster = Some true + } + | Some profile -> { + profile with EnablePrivateCluster = Some true - } + } - { state with - ApiServerAccessProfile = Some accessProfile + { + state with + ApiServerAccessProfile = Some accessProfile } /// Sets the range of Authorized IP addresses that can access the cluster's API server. @@ -459,18 +442,18 @@ type AksBuilder() = member _.AddApiServerAuthorizedIP(state: AksConfig, range: string list) = let accessProfile = match state.ApiServerAccessProfile with - | None -> - { - AuthorizedIPRanges = range - EnablePrivateCluster = None - } - | Some profile -> - { profile with + | None -> { + AuthorizedIPRanges = range + EnablePrivateCluster = None + } + | Some profile -> { + profile with AuthorizedIPRanges = profile.AuthorizedIPRanges @ range - } + } - { state with - ApiServerAccessProfile = Some accessProfile + { + state with + ApiServerAccessProfile = Some accessProfile } /// Enables any addons. @@ -481,59 +464,59 @@ type AksBuilder() = [] member _.KubeletIdentity(state: AksConfig, identity: ResourceId) = match state.IdentityProfile with - | None -> - { state with + | None -> { + state with IdentityProfile = Some { KubeletIdentity = Some identity } - } - | Some identityProfile -> - { state with + } + | Some identityProfile -> { + state with IdentityProfile = - Some - { identityProfile with + Some { + identityProfile with KubeletIdentity = Some identity - } - } + } + } member this.KubeletIdentity(state: AksConfig, identity: UserAssignedIdentity.UserAssignedIdentityConfig) = this.KubeletIdentity(state, identity.ResourceId) /// Sets the network profile for the AKS cluster. [] - member _.NetworkProfile(state: AksConfig, networkProfile) = - { state with + member _.NetworkProfile(state: AksConfig, networkProfile) = { + state with NetworkProfile = Some networkProfile - } + } /// Sets the linux profile for the AKS cluster. [] - member _.LinuxProfile(state: AksConfig, username: string, sshKeys: string list) = - { state with + member _.LinuxProfile(state: AksConfig, username: string, sshKeys: string list) = { + state with LinuxProfile = Some(username, sshKeys) - } + } member this.LinuxProfile(state: AksConfig, username: string, sshKey: string) = this.LinuxProfile(state, username, [ sshKey ]) /// Sets the client id of the service principal for the AKS cluster. [] - member _.ServicePrincipalClientID(state: AksConfig, clientId) = - { state with + member _.ServicePrincipalClientID(state: AksConfig, clientId) = { + state with ServicePrincipalClientID = clientId - } + } /// Uses the managed identity of this resource for the service principal. [] - member _.ServicePrincipalUseMsi(state: AksConfig) = - { state with + member _.ServicePrincipalUseMsi(state: AksConfig) = { + state with ServicePrincipalClientID = "msi" - } + } /// Sets the windows admin username for the AKS cluster. [] - member _.WindowsUsername(state: AksConfig, username) = - { state with + member _.WindowsUsername(state: AksConfig, username) = { + state with WindowsProfileAdminUserName = Some username - } + } /// Builds an AKS cluster ARM resource definition let aksBuilder = AksBuilder() diff --git a/src/Farmer/Builders/Builders.Cosmos.fs b/src/Farmer/Builders/Builders.Cosmos.fs index f53120ca7..60ce4c8b7 100644 --- a/src/Farmer/Builders/Builders.Cosmos.fs +++ b/src/Farmer/Builders/Builders.Cosmos.fs @@ -56,28 +56,26 @@ type CosmosDb = static member getConnectionString(name: ResourceName, connectionStringKind) = CosmosDb.getConnectionString (databaseAccounts.resourceId name, connectionStringKind) -type CosmosDbContainerConfig = - { - Name: ResourceName - PartitionKey: string list * IndexKind - Indexes: (string * (IndexDataType * IndexKind) list) list - UniqueKeys: Set - ExcludedPaths: string list - } - -type CosmosDbConfig = - { - AccountName: ResourceRef - AccountConsistencyPolicy: ConsistencyPolicy - AccountFailoverPolicy: FailoverPolicy - DbName: ResourceName - DbThroughput: Throughput - Containers: CosmosDbContainerConfig list - PublicNetworkAccess: FeatureFlag - FreeTier: bool - Tags: Map - Kind: DatabaseKind - } +type CosmosDbContainerConfig = { + Name: ResourceName + PartitionKey: string list * IndexKind + Indexes: (string * (IndexDataType * IndexKind) list) list + UniqueKeys: Set + ExcludedPaths: string list +} + +type CosmosDbConfig = { + AccountName: ResourceRef + AccountConsistencyPolicy: ConsistencyPolicy + AccountFailoverPolicy: FailoverPolicy + DbName: ResourceName + DbThroughput: Throughput + Containers: CosmosDbContainerConfig list + PublicNetworkAccess: FeatureFlag + FreeTier: bool + Tags: Map + Kind: DatabaseKind +} with member private this.AccountResourceId = this.AccountName.resourceId this @@ -107,79 +105,72 @@ type CosmosDbConfig = interface IBuilder with member this.ResourceId = this.AccountResourceId - member this.BuildResources location = - [ - // Account - match this.AccountName with - | DeployableResource this _ -> - { - Name = this.AccountResourceId.Name - Location = location - Kind = this.Kind - ConsistencyPolicy = this.AccountConsistencyPolicy - Serverless = - match this.DbThroughput with - | Serverless -> Enabled - | Provisioned _ -> Disabled - PublicNetworkAccess = this.PublicNetworkAccess - FailoverPolicy = this.AccountFailoverPolicy - FreeTier = this.FreeTier - Tags = this.Tags - } - | _ -> () - - // Database + member this.BuildResources location = [ + // Account + match this.AccountName with + | DeployableResource this _ -> { + Name = this.AccountResourceId.Name + Location = location + Kind = this.Kind + ConsistencyPolicy = this.AccountConsistencyPolicy + Serverless = + match this.DbThroughput with + | Serverless -> Enabled + | Provisioned _ -> Disabled + PublicNetworkAccess = this.PublicNetworkAccess + FailoverPolicy = this.AccountFailoverPolicy + FreeTier = this.FreeTier + Tags = this.Tags + } + | _ -> () + + // Database + { + Name = this.DbName + Account = this.AccountResourceId.Name + Throughput = this.DbThroughput + Kind = this.Kind + } + + // Containers + for container in this.Containers do { - Name = this.DbName + Name = container.Name Account = this.AccountResourceId.Name - Throughput = this.DbThroughput - Kind = this.Kind + Database = this.DbName + PartitionKey = {| + Paths = fst container.PartitionKey + Kind = snd container.PartitionKey + |} + UniqueKeyPolicy = {| + UniqueKeys = + container.UniqueKeys + |> Set.map (fun uniqueKeyPath -> {| Paths = uniqueKeyPath |}) + |} + IndexingPolicy = {| + ExcludedPaths = container.ExcludedPaths + IncludedPaths = [ + for (path, indexes) in container.Indexes do + {| Path = path; Indexes = indexes |} + ] + |} } - - // Containers - for container in this.Containers do - { - Name = container.Name - Account = this.AccountResourceId.Name - Database = this.DbName - PartitionKey = - {| - Paths = fst container.PartitionKey - Kind = snd container.PartitionKey - |} - UniqueKeyPolicy = - {| - UniqueKeys = - container.UniqueKeys - |> Set.map (fun uniqueKeyPath -> {| Paths = uniqueKeyPath |}) - |} - IndexingPolicy = - {| - ExcludedPaths = container.ExcludedPaths - IncludedPaths = - [ - for (path, indexes) in container.Indexes do - {| Path = path; Indexes = indexes |} - ] - |} - } - ] + ] type CosmosDbContainerBuilder() = - member _.Yield _ = - { - Name = ResourceName "" - PartitionKey = [], Hash - Indexes = [] - UniqueKeys = Set.empty - ExcludedPaths = [] - } + member _.Yield _ = { + Name = ResourceName "" + PartitionKey = [], Hash + Indexes = [] + UniqueKeys = Set.empty + ExcludedPaths = [] + } member _.Run state = match state.PartitionKey with | [], _ -> raiseFarmer $"You must set a partition key on CosmosDB container '{state.Name.Value}'." - | partitions, indexKind -> - { state with + | partitions, indexKind -> { + state with PartitionKey = [ for partition in partitions do @@ -189,7 +180,7 @@ type CosmosDbContainerBuilder() = "/" + partition ], indexKind - } + } /// Sets the name of the container. [] @@ -197,62 +188,61 @@ type CosmosDbContainerBuilder() = /// Sets the partition key of the container. [] - member _.PartitionKey(state: CosmosDbContainerConfig, partitions, indexKind) = - { state with + member _.PartitionKey(state: CosmosDbContainerConfig, partitions, indexKind) = { + state with PartitionKey = partitions, indexKind - } + } /// Adds an index to the container. [] - member _.AddIndex(state: CosmosDbContainerConfig, path, indexes) = - { state with + member _.AddIndex(state: CosmosDbContainerConfig, path, indexes) = { + state with Indexes = (path, indexes) :: state.Indexes - } + } /// Adds a unique key constraint to the container (ensures uniqueness within the logical partition). [] - member _.AddUniqueKey(state: CosmosDbContainerConfig, uniqueKeyPaths) = - { state with + member _.AddUniqueKey(state: CosmosDbContainerConfig, uniqueKeyPaths) = { + state with UniqueKeys = state.UniqueKeys.Add(uniqueKeyPaths) - } + } /// Excludes a path from the container index. [] - member _.ExcludePath(state: CosmosDbContainerConfig, path) = - { state with + member _.ExcludePath(state: CosmosDbContainerConfig, path) = { + state with ExcludedPaths = path :: state.ExcludedPaths - } + } type CosmosDbBuilder() = - member _.Yield _ = - { - DbName = ResourceName.Empty - AccountName = - derived (fun config -> - let dbName = config.DbName.Value.ToLower() - let maxLength = 36 // 44 less "-account" - - if config.DbName.Value.Length > maxLength then - dbName.Substring maxLength - else - dbName - |> sprintf "%s-account" - |> ResourceName - |> databaseAccounts.resourceId) - AccountConsistencyPolicy = Eventual - AccountFailoverPolicy = NoFailover - DbThroughput = Provisioned 400 - Containers = [] - PublicNetworkAccess = Enabled - FreeTier = false - Tags = Map.empty - Kind = DatabaseKind.Document - } + member _.Yield _ = { + DbName = ResourceName.Empty + AccountName = + derived (fun config -> + let dbName = config.DbName.Value.ToLower() + let maxLength = 36 // 44 less "-account" + + if config.DbName.Value.Length > maxLength then + dbName.Substring maxLength + else + dbName + |> sprintf "%s-account" + |> ResourceName + |> databaseAccounts.resourceId) + AccountConsistencyPolicy = Eventual + AccountFailoverPolicy = NoFailover + DbThroughput = Provisioned 400 + Containers = [] + PublicNetworkAccess = Enabled + FreeTier = false + Tags = Map.empty + Kind = DatabaseKind.Document + } /// Sets the name of the CosmosDB server. [] - member _.AccountName(state: CosmosDbConfig, accountName: ResourceName) = - { state with + member _.AccountName(state: CosmosDbConfig, accountName: ResourceName) = { + state with AccountName = AutoGeneratedResource( Named( @@ -261,17 +251,17 @@ type CosmosDbBuilder() = ) ) ) - } + } member this.AccountName(state: CosmosDbConfig, accountName: string) = this.AccountName(state, ResourceName accountName) /// Links the database to an existing server [] - member _.LinkToAccount(state: CosmosDbConfig, accountConfig: CosmosDbConfig) = - { state with + member _.LinkToAccount(state: CosmosDbConfig, accountConfig: CosmosDbConfig) = { + state with AccountName = LinkedResource(Managed(accountConfig.AccountName.resourceId accountConfig)) - } + } /// Sets the name of the database. [] @@ -281,27 +271,26 @@ type CosmosDbBuilder() = /// Sets the consistency policy of the database. [] - member _.ConsistencyPolicy(state: CosmosDbConfig, consistency: ConsistencyPolicy) = - { state with + member _.ConsistencyPolicy(state: CosmosDbConfig, consistency: ConsistencyPolicy) = { + state with AccountConsistencyPolicy = consistency - } + } /// Sets the failover policy of the database. [] - member _.FailoverPolicy(state: CosmosDbConfig, failoverPolicy: FailoverPolicy) = - { state with + member _.FailoverPolicy(state: CosmosDbConfig, failoverPolicy: FailoverPolicy) = { + state with AccountFailoverPolicy = failoverPolicy - } + } /// Sets the throughput of the server. [] - member _.Throughput(state: CosmosDbConfig, throughput) = - { state with DbThroughput = throughput } + member _.Throughput(state: CosmosDbConfig, throughput) = { state with DbThroughput = throughput } - member _.Throughput(state: CosmosDbConfig, throughput) = - { state with + member _.Throughput(state: CosmosDbConfig, throughput) = { + state with DbThroughput = Provisioned throughput - } + } /// Sets the storage kind [] @@ -309,34 +298,34 @@ type CosmosDbBuilder() = /// Adds a list of containers to the database. [] - member _.AddContainers(state: CosmosDbConfig, containers) = - { state with + member _.AddContainers(state: CosmosDbConfig, containers) = { + state with Containers = state.Containers @ containers - } + } /// Enables public network access [] - member _.PublicNetworkAccess(state: CosmosDbConfig) = - { state with + member _.PublicNetworkAccess(state: CosmosDbConfig) = { + state with PublicNetworkAccess = Enabled - } + } /// Disables public network access [] - member _.PrivateNetworkAccess(state: CosmosDbConfig) = - { state with + member _.PrivateNetworkAccess(state: CosmosDbConfig) = { + state with PublicNetworkAccess = Disabled - } + } /// Enables the use of CosmosDB free tier (one per subscription). [] member _.FreeTier(state: CosmosDbConfig) = { state with FreeTier = true } interface ITaggable with - member _.Add state tags = - { state with + member _.Add state tags = { + state with Tags = state.Tags |> Map.merge tags - } + } let cosmosDb = CosmosDbBuilder() let cosmosContainer = CosmosDbContainerBuilder() diff --git a/src/Farmer/Builders/Builders.Dashboard.fs b/src/Farmer/Builders/Builders.Dashboard.fs index d40080a4e..d8a6049e7 100644 --- a/src/Farmer/Builders/Builders.Dashboard.fs +++ b/src/Farmer/Builders/Builders.Dashboard.fs @@ -4,39 +4,36 @@ module Farmer.Builders.Dashboard open Farmer open Farmer.Arm.Dashboard -type DashboardConfig = - { - Name: ResourceName - Title: string option - Metadata: DashboardMetadata - LensParts: LensPart list - Dependencies: Set - } +type DashboardConfig = { + Name: ResourceName + Title: string option + Metadata: DashboardMetadata + LensParts: LensPart list + Dependencies: Set +} with interface IBuilder with member this.ResourceId = dashboard.resourceId this.Name - member this.BuildResources location = - [ - { - Name = this.Name - Title = this.Title - Location = location - Metadata = this.Metadata - LensParts = this.LensParts - Dependencies = this.Dependencies - } - ] + member this.BuildResources location = [ + { + Name = this.Name + Title = this.Title + Location = location + Metadata = this.Metadata + LensParts = this.LensParts + Dependencies = this.Dependencies + } + ] type DashboardBuilder() = - member __.Yield _ = - { - Name = ResourceName.Empty - Title = None - Metadata = DashboardMetadata.EmptyMetadata - LensParts = List.empty - Dependencies = Set.empty - } + member __.Yield _ = { + Name = ResourceName.Empty + Title = None + Metadata = DashboardMetadata.EmptyMetadata + LensParts = List.empty + Dependencies = Set.empty + } [] /// Sets the name of the dashboard. @@ -52,23 +49,24 @@ type DashboardBuilder() = [] /// Create your own lens part for the dashboard - member __.CustomLens(state: DashboardConfig, lens) = - { state with + member __.CustomLens(state: DashboardConfig, lens) = { + state with LensParts = lens :: state.LensParts - } + } [] /// Create markdown lens part for the dashboard member __.MarkdownPart(state: DashboardConfig, (position, markdownPart)) = let markdown = generateMarkdownPart markdownPart - { state with - LensParts = - ({ - position = position - metadata = markdown - }) - :: state.LensParts + { + state with + LensParts = + ({ + position = position + metadata = markdown + }) + :: state.LensParts } [] @@ -76,13 +74,14 @@ type DashboardBuilder() = member __.VideoPart(state: DashboardConfig, (position, videoPart)) = let videopart = generateVideoPart videoPart - { state with - LensParts = - ({ - position = position - metadata = videopart - }) - :: state.LensParts + { + state with + LensParts = + ({ + position = position + metadata = videopart + }) + :: state.LensParts } [] @@ -90,13 +89,14 @@ type DashboardBuilder() = member __.VirtualMachinePart(state: DashboardConfig, (position, virtualMachineId)) = let vmPart = generateVirtualMachinePart virtualMachineId - { state with - LensParts = - ({ - position = position - metadata = vmPart - }) - :: state.LensParts + { + state with + LensParts = + ({ + position = position + metadata = vmPart + }) + :: state.LensParts } [] @@ -104,13 +104,14 @@ type DashboardBuilder() = member __.WebtestResultPart(state: DashboardConfig, (position, applicationInsightsName)) = let vmPart = generateWebtestResultPart applicationInsightsName - { state with - LensParts = - ({ - position = position - metadata = vmPart - }) - :: state.LensParts + { + state with + LensParts = + ({ + position = position + metadata = vmPart + }) + :: state.LensParts } [] @@ -118,13 +119,14 @@ type DashboardBuilder() = member __.MetricsChartPart(state: DashboardConfig, (position, metricsChart)) = let metricsChartPart = generateMetricsChartPart metricsChart - { state with - LensParts = - ({ - position = position - metadata = metricsChartPart - }) - :: state.LensParts + { + state with + LensParts = + ({ + position = position + metadata = metricsChartPart + }) + :: state.LensParts } [] @@ -132,20 +134,21 @@ type DashboardBuilder() = member __.MonitorChartPart(state: DashboardConfig, (position, monitorChart)) = let monitorChartPart = generateMonitorChartPart monitorChart - { state with - LensParts = - ({ - position = position - metadata = monitorChartPart - }) - :: state.LensParts + { + state with + LensParts = + ({ + position = position + metadata = monitorChartPart + }) + :: state.LensParts } /// Enable support for additional dependencies. interface IDependable with - member _.Add state newDeps = - { state with + member _.Add state newDeps = { + state with Dependencies = state.Dependencies + newDeps - } + } let dashboard = DashboardBuilder() diff --git a/src/Farmer/Builders/Builders.DataLake.fs b/src/Farmer/Builders/Builders.DataLake.fs index daac8d2b4..5c1cdae9f 100644 --- a/src/Farmer/Builders/Builders.DataLake.fs +++ b/src/Farmer/Builders/Builders.DataLake.fs @@ -5,52 +5,48 @@ open Farmer open Farmer.DataLake open Farmer.Arm.DataLakeStore -type DataLakeConfig = - { - Name: ResourceName - EncryptionState: FeatureFlag - Sku: Sku - Tags: Map - } +type DataLakeConfig = { + Name: ResourceName + EncryptionState: FeatureFlag + Sku: Sku + Tags: Map +} with interface IBuilder with member this.ResourceId = accounts.resourceId this.Name - member this.BuildResources location = - [ - { - Name = this.Name - Location = location - EncryptionState = this.EncryptionState - Sku = this.Sku - Tags = this.Tags - } - ] + member this.BuildResources location = [ + { + Name = this.Name + Location = location + EncryptionState = this.EncryptionState + Sku = this.Sku + Tags = this.Tags + } + ] type DataLakeBuilder() = - member _.Yield _ = - { - Name = ResourceName "" - EncryptionState = Disabled - Sku = Sku.Consumption - Tags = Map.empty - } + member _.Yield _ = { + Name = ResourceName "" + EncryptionState = Disabled + Sku = Sku.Consumption + Tags = Map.empty + } /// Sets the name of the data lake. [] member _.Name(state: DataLakeConfig, name) = { state with Name = ResourceName name } [] - member _.EncryptionState(state: DataLakeConfig) = - { state with EncryptionState = Enabled } + member _.EncryptionState(state: DataLakeConfig) = { state with EncryptionState = Enabled } [] member _.Sku(state: DataLakeConfig, sku) = { state with Sku = sku } interface ITaggable with - member _.Add state tags = - { state with + member _.Add state tags = { + state with Tags = state.Tags |> Map.merge tags - } + } let dataLake = DataLakeBuilder() diff --git a/src/Farmer/Builders/Builders.Databricks.fs b/src/Farmer/Builders/Builders.Databricks.fs index 46cfc83ae..93ec3669c 100644 --- a/src/Farmer/Builders/Builders.Databricks.fs +++ b/src/Farmer/Builders/Builders.Databricks.fs @@ -8,56 +8,51 @@ open Farmer.Arm.Network open Farmer.Arm.KeyVault open System -type DatabricksConfig = - { - Name: ResourceName - ManagedResourceGroupId: ResourceName option - Sku: Sku - EnablePublicIp: FeatureFlag - KeyEncryption: KeyEncryption option - VnetConfig: VnetConfig option - Tags: Map - } +type DatabricksConfig = { + Name: ResourceName + ManagedResourceGroupId: ResourceName option + Sku: Sku + EnablePublicIp: FeatureFlag + KeyEncryption: KeyEncryption option + VnetConfig: VnetConfig option + Tags: Map +} with interface IBuilder with member this.ResourceId = workspaces.resourceId this.Name - member this.BuildResources location = - [ - { - Name = this.Name - Location = location - ManagedResourceGroupId = - this.ManagedResourceGroupId |> Option.defaultWith (fun () -> this.Name - "rg") - Sku = this.Sku - EnablePublicIp = this.EnablePublicIp - KeyEncryption = this.KeyEncryption - VnetConfig = this.VnetConfig - Tags = this.Tags - Dependencies = - Set - [ - match this.KeyEncryption with - | Some (CustomerManaged config) -> config.Vault - | Some InfrastructureManaged - | None -> () - - yield! this.VnetConfig |> Option.mapList (fun vnet -> vnet.Vnet) - ] - } - ] + member this.BuildResources location = [ + { + Name = this.Name + Location = location + ManagedResourceGroupId = this.ManagedResourceGroupId |> Option.defaultWith (fun () -> this.Name - "rg") + Sku = this.Sku + EnablePublicIp = this.EnablePublicIp + KeyEncryption = this.KeyEncryption + VnetConfig = this.VnetConfig + Tags = this.Tags + Dependencies = + Set [ + match this.KeyEncryption with + | Some(CustomerManaged config) -> config.Vault + | Some InfrastructureManaged + | None -> () + + yield! this.VnetConfig |> Option.mapList (fun vnet -> vnet.Vnet) + ] + } + ] type WorkspaceBuilder() = - member _.Yield _ = - { - Name = ResourceName.Empty - ManagedResourceGroupId = None - Sku = Standard - EnablePublicIp = Enabled - KeyEncryption = None - VnetConfig = None - Tags = Map.empty - } + member _.Yield _ = { + Name = ResourceName.Empty + ManagedResourceGroupId = None + Sku = Standard + EnablePublicIp = Enabled + KeyEncryption = None + VnetConfig = None + Tags = Map.empty + } member _.Run(state: DatabricksConfig) = match state with @@ -81,10 +76,10 @@ type WorkspaceBuilder() = /// Sets the managed resource group name. If not set, defaults to the name of the databricks resource + "-rg". [] - member _.ManagedResourceGroupId(state: DatabricksConfig, resourceGroupName) = - { state with + member _.ManagedResourceGroupId(state: DatabricksConfig, resourceGroupName) = { + state with ManagedResourceGroupId = Some(ResourceName resourceGroupName) - } + } /// Sets the workspace pricing tier. Defaults to Standard. [] @@ -98,12 +93,11 @@ type WorkspaceBuilder() = [] member this.KeyVaultEncryption(state: DatabricksConfig, keyVault: ResourceId, keyName: string) = let encryption = - CustomerManaged - {| - Vault = keyVault - Key = keyName - KeyVersion = None - |} + CustomerManaged {| + Vault = keyVault + Key = keyName + KeyVersion = None + |} this.Encrypt(state, encryption) @@ -115,16 +109,16 @@ type WorkspaceBuilder() = /// Specifies the version of the key vault key to use; if this is not specified, the latest version of the key is used. [] - member _.KeyVaultKeyVersion(state: DatabricksConfig, keyVersion) = - { state with + member _.KeyVaultKeyVersion(state: DatabricksConfig, keyVersion) = { + state with KeyEncryption = match state.KeyEncryption with - | Some (CustomerManaged config) -> + | Some(CustomerManaged config) -> Some( - CustomerManaged - {| config with + CustomerManaged {| + config with KeyVersion = Some keyVersion - |} + |} ) | Some InfrastructureManaged -> raiseFarmer @@ -132,7 +126,7 @@ type WorkspaceBuilder() = | None -> raiseFarmer "No key vault has been specified. First activate keyvault secret integration using key_vault_secret_management." - } + } /// Use Databricks itself for the key store. [] @@ -141,22 +135,21 @@ type WorkspaceBuilder() = /// Specify the secret scope of the workspace programmatically. [] - member _.Encrypt(state: DatabricksConfig, encryption) = - { state with + member _.Encrypt(state: DatabricksConfig, encryption) = { + state with KeyEncryption = Some encryption - } + } [] - member _.AttachToVnet(state: DatabricksConfig, vnet: ResourceId, publicSubnet, privateSubnet) = - { state with + member _.AttachToVnet(state: DatabricksConfig, vnet: ResourceId, publicSubnet, privateSubnet) = { + state with VnetConfig = - Some - { - Vnet = vnet - PublicSubnet = publicSubnet - PrivateSubnet = privateSubnet - } - } + Some { + Vnet = vnet + PublicSubnet = publicSubnet + PrivateSubnet = privateSubnet + } + } member this.AttachToVnet(state, vnet: ResourceName, publicSubnet: SubnetBuildSpec, privateSubnet: SubnetBuildSpec) = this.AttachToVnet( @@ -173,9 +166,9 @@ type WorkspaceBuilder() = this.AttachToVnet(state, virtualNetworks.resourceId vnet, ResourceName publicSubnet, ResourceName privateSubnet) interface ITaggable with - member _.Add state tags = - { state with + member _.Add state tags = { + state with Tags = state.Tags |> Map.merge tags - } + } let databricks = WorkspaceBuilder() diff --git a/src/Farmer/Builders/Builders.DeploymentScript.fs b/src/Farmer/Builders/Builders.DeploymentScript.fs index 55ec20d13..ffbbec0cb 100644 --- a/src/Farmer/Builders/Builders.DeploymentScript.fs +++ b/src/Farmer/Builders/Builders.DeploymentScript.fs @@ -15,93 +15,90 @@ type OutputCollection(owner) = .reference(deploymentScripts, ResourceId.create (deploymentScripts, owner)) .Map(fun v -> v + ".outputs." + key) -type DeploymentScriptConfig = - { - Name: ResourceName - Dependencies: ResourceId Set - Arguments: string list - CleanupPreference: Cleanup - Cli: CliVersion - EnvironmentVariables: Map - ForceUpdate: bool - CustomIdentity: UserAssignedIdentity option - ScriptSource: ScriptSource - SupportingScriptUris: Uri list - Tags: Map - Timeout: TimeSpan option - } +type DeploymentScriptConfig = { + Name: ResourceName + Dependencies: ResourceId Set + Arguments: string list + CleanupPreference: Cleanup + Cli: CliVersion + EnvironmentVariables: Map + ForceUpdate: bool + CustomIdentity: UserAssignedIdentity option + ScriptSource: ScriptSource + SupportingScriptUris: Uri list + Tags: Map + Timeout: TimeSpan option +} with member this.Outputs = OutputCollection this.Name interface IBuilder with member this.ResourceId = deploymentScripts.resourceId this.Name - member this.BuildResources location = - [ - let generatedIdentityId = - let generatedIdentityName = ResourceName $"{this.Name.Value}-identity" - ResourceId.create (userAssignedIdentities, generatedIdentityName) - - // User Assigned Identity - create one if none was supplied. - if this.CustomIdentity.IsNone then - { - Name = generatedIdentityId.Name - Location = location - Tags = Map.empty - } - - let identity = - this.CustomIdentity - |> Option.defaultValue (UserAssignedIdentity generatedIdentityId) - - // Assignment - { - Name = - ArmExpression - .create($"guid(concat(resourceGroup().id, '{Roles.Contributor.Id}'))") - .Eval() - |> ResourceName - RoleDefinitionId = Roles.Contributor - PrincipalId = identity.PrincipalId - PrincipalType = PrincipalType.ServicePrincipal - Scope = ResourceGroup - Dependencies = Set.empty - } + member this.BuildResources location = [ + let generatedIdentityId = + let generatedIdentityName = ResourceName $"{this.Name.Value}-identity" + ResourceId.create (userAssignedIdentities, generatedIdentityName) - // Deployment Script + // User Assigned Identity - create one if none was supplied. + if this.CustomIdentity.IsNone then { + Name = generatedIdentityId.Name Location = location - Name = this.Name - Dependencies = this.Dependencies - Arguments = this.Arguments - CleanupPreference = this.CleanupPreference - Cli = this.Cli - EnvironmentVariables = this.EnvironmentVariables - ForceUpdateTag = if this.ForceUpdate then Some(Guid.NewGuid()) else None - Identity = identity - ScriptSource = this.ScriptSource - SupportingScriptUris = this.SupportingScriptUris - Tags = this.Tags - Timeout = this.Timeout + Tags = Map.empty } - ] + + let identity = + this.CustomIdentity + |> Option.defaultValue (UserAssignedIdentity generatedIdentityId) + + // Assignment + { + Name = + ArmExpression + .create($"guid(concat(resourceGroup().id, '{Roles.Contributor.Id}'))") + .Eval() + |> ResourceName + RoleDefinitionId = Roles.Contributor + PrincipalId = identity.PrincipalId + PrincipalType = PrincipalType.ServicePrincipal + Scope = ResourceGroup + Dependencies = Set.empty + } + + // Deployment Script + { + Location = location + Name = this.Name + Dependencies = this.Dependencies + Arguments = this.Arguments + CleanupPreference = this.CleanupPreference + Cli = this.Cli + EnvironmentVariables = this.EnvironmentVariables + ForceUpdateTag = if this.ForceUpdate then Some(Guid.NewGuid()) else None + Identity = identity + ScriptSource = this.ScriptSource + SupportingScriptUris = this.SupportingScriptUris + Tags = this.Tags + Timeout = this.Timeout + } + ] type DeploymentScriptBuilder() = - member _.Yield _ = - { - Name = ResourceName.Empty - Dependencies = Set.empty - Arguments = [] - CleanupPreference = Cleanup.Always - Cli = AzCli "2.9.1" - EnvironmentVariables = Map.empty - ForceUpdate = false - CustomIdentity = None - ScriptSource = Content "" - SupportingScriptUris = [] - Tags = Map.empty - Timeout = None - } + member _.Yield _ = { + Name = ResourceName.Empty + Dependencies = Set.empty + Arguments = [] + CleanupPreference = Cleanup.Always + Cli = AzCli "2.9.1" + EnvironmentVariables = Map.empty + ForceUpdate = false + CustomIdentity = None + ScriptSource = Content "" + SupportingScriptUris = [] + Tags = Map.empty + Timeout = None + } /// Sets the name of the container instance. [] @@ -111,17 +108,17 @@ type DeploymentScriptBuilder() = /// Arguments which will become a space separated string of arguments passed to the script. [] - member _.Arguments(state: DeploymentScriptConfig, arguments) = - { state with + member _.Arguments(state: DeploymentScriptConfig, arguments) = { + state with Arguments = state.Arguments @ arguments - } + } /// Specify deployment script should only be cleaned up if it succeeds so failures cn be inspected. [] - member _.CleanupPreference(state: DeploymentScriptConfig) = - { state with + member _.CleanupPreference(state: DeploymentScriptConfig) = { + state with CleanupPreference = Cleanup.OnSuccess - } + } /// Specify the CLI type and version to use - defaults to the 'az cli' version 2.12.1. [] @@ -129,27 +126,27 @@ type DeploymentScriptBuilder() = /// The contents of the script to execute. [] - member _.Content(state: DeploymentScriptConfig, content) = - { state with + member _.Content(state: DeploymentScriptConfig, content) = { + state with ScriptSource = Content content - } + } /// URI to download the primary script to execute. [] - member _.PrimaryScriptUri(state: DeploymentScriptConfig, primaryScriptUri) = - { state with + member _.PrimaryScriptUri(state: DeploymentScriptConfig, primaryScriptUri) = { + state with ScriptSource = Remote primaryScriptUri - } + } member this.PrimaryScriptUri(state, primaryScriptUri) = this.PrimaryScriptUri(state, Uri primaryScriptUri) /// Environment variables for the script. [] - member _.EnvironmentVariables(state: DeploymentScriptConfig, envVars) = - { state with + member _.EnvironmentVariables(state: DeploymentScriptConfig, envVars) = { + state with EnvironmentVariables = Map envVars - } + } member this.EnvironmentVariables(state, envVars) = this.EnvironmentVariables(state, envVars |> List.map (fun (k, v) -> k, EnvValue v)) @@ -162,10 +159,10 @@ type DeploymentScriptBuilder() = /// Sets the user assigned managed identity under which this deployment script runs. If none is supplied, a new identity will be automatically created. [] - member _.Identity(state: DeploymentScriptConfig, identity) = - { state with + member _.Identity(state: DeploymentScriptConfig, identity) = { + state with CustomIdentity = Some identity - } + } member this.Identity(state, identity: UserAssignedIdentityConfig) = this.Identity(state, identity.UserAssignedIdentity) @@ -176,37 +173,38 @@ type DeploymentScriptBuilder() = if retentionInterval > 26 then raiseFarmer $"Max retention interval is 26 hours, but was set as {retentionInterval}" - { state with - CleanupPreference = Cleanup.OnExpiration(TimeSpan.FromHours(float retentionInterval)) + { + state with + CleanupPreference = Cleanup.OnExpiration(TimeSpan.FromHours(float retentionInterval)) } /// Additional URIs to download scripts that the primary script relies on. [] - member _.SupportingScriptUris(state: DeploymentScriptConfig, supportingScriptUris) = - { state with + member _.SupportingScriptUris(state: DeploymentScriptConfig, supportingScriptUris) = { + state with SupportingScriptUris = supportingScriptUris - } + } /// Timeout for script execution. [] member _.Timeout(state: DeploymentScriptConfig, timeout) = { state with Timeout = Some timeout } /// Timeout for script execution in ISO 8601 format, e.g. PT30M. - member _.Timeout(state: DeploymentScriptConfig, timeout) = - { state with + member _.Timeout(state: DeploymentScriptConfig, timeout) = { + state with Timeout = Some(Xml.XmlConvert.ToTimeSpan timeout) - } + } interface ITaggable with - member _.Add state tags = - { state with + member _.Add state tags = { + state with Tags = state.Tags |> Map.merge tags - } + } interface IDependable with - member _.Add state newDeps = - { state with + member _.Add state newDeps = { + state with Dependencies = state.Dependencies + newDeps - } + } let deploymentScript = DeploymentScriptBuilder() diff --git a/src/Farmer/Builders/Builders.DiagnosticSetting.fs b/src/Farmer/Builders/Builders.DiagnosticSetting.fs index 3d7ef491c..4b340f934 100644 --- a/src/Farmer/Builders/Builders.DiagnosticSetting.fs +++ b/src/Farmer/Builders/Builders.DiagnosticSetting.fs @@ -9,52 +9,48 @@ open Farmer.Arm.DiagnosticSetting open Farmer.Builders.Storage open Farmer.DiagnosticSettings -type DiagnosticSettingsConfig = - { - Name: ResourceName - MetricsSource: ResourceId - Sinks: SinkInformation - Metrics: MetricSetting Set - Logs: LogSetting Set - - Dependencies: ResourceId Set - Tags: Map - } +type DiagnosticSettingsConfig = { + Name: ResourceName + MetricsSource: ResourceId + Sinks: SinkInformation + Metrics: MetricSetting Set + Logs: LogSetting Set + + Dependencies: ResourceId Set + Tags: Map +} with interface IBuilder with member this.ResourceId = diagnosticSettingsType(this.MetricsSource.Type).resourceId this.Name - member this.BuildResources location = - [ - { - Name = this.Name - Location = location - MetricsSource = this.MetricsSource - Sinks = this.Sinks - Logs = this.Logs - Metrics = this.Metrics - Dependencies = this.Dependencies - Tags = this.Tags - } - ] + member this.BuildResources location = [ + { + Name = this.Name + Location = location + MetricsSource = this.MetricsSource + Sinks = this.Sinks + Logs = this.Logs + Metrics = this.Metrics + Dependencies = this.Dependencies + Tags = this.Tags + } + ] type DiagnosticSettingsBuilder() = - member _.Yield _ = - { - Name = ResourceName.Empty - Sinks = - { - StorageAccount = None - EventHub = None - LogAnalyticsWorkspace = None - } - Metrics = Set.empty - Logs = Set.empty - MetricsSource = ResourceId.create (ResourceType("", ""), ResourceName "") - Dependencies = Set.empty - Tags = Map.empty + member _.Yield _ = { + Name = ResourceName.Empty + Sinks = { + StorageAccount = None + EventHub = None + LogAnalyticsWorkspace = None } + Metrics = Set.empty + Logs = Set.empty + MetricsSource = ResourceId.create (ResourceType("", ""), ResourceName "") + Dependencies = Set.empty + Tags = Map.empty + } member _.Run(state: DiagnosticSettingsConfig) = let (|EmptySet|_|) theSet = @@ -72,18 +68,18 @@ type DiagnosticSettingsBuilder() = /// Sets the name of the diagnostic settings. [] - member _.Name(state: DiagnosticSettingsConfig, resourceName: string) = - { state with + member _.Name(state: DiagnosticSettingsConfig, resourceName: string) = { + state with Name = ResourceName resourceName - } + } /// The source resource of diagnostic metrics. [] - member _.MetricsSource(state: DiagnosticSettingsConfig, metricsSource: ResourceId) = - { state with + member _.MetricsSource(state: DiagnosticSettingsConfig, metricsSource: ResourceId) = { + state with MetricsSource = metricsSource Dependencies = state.Dependencies.Add metricsSource - } + } member this.MetricsSource(state: DiagnosticSettingsConfig, builder: IBuilder) = this.MetricsSource(state, builder.ResourceId) @@ -91,30 +87,30 @@ type DiagnosticSettingsBuilder() = static member AddDestinationPrivate(state: DiagnosticSettingsConfig, resourceId, ?dependency) = let dependency = defaultArg dependency resourceId - { state with - Sinks = - match resourceId with - | HasResourceType storageAccounts -> - { state.Sinks with - StorageAccount = Some resourceId - } - | HasResourceType workspaces -> - { state.Sinks with - LogAnalyticsWorkspace = Some(resourceId, AzureDiagnostics) - } - | HasResourceType Namespaces.authorizationRules -> - { state.Sinks with - EventHub = - Some - {| + { + state with + Sinks = + match resourceId with + | HasResourceType storageAccounts -> { + state.Sinks with + StorageAccount = Some resourceId + } + | HasResourceType workspaces -> { + state.Sinks with + LogAnalyticsWorkspace = Some(resourceId, AzureDiagnostics) + } + | HasResourceType Namespaces.authorizationRules -> { + state.Sinks with + EventHub = + Some {| AuthorizationRuleId = resourceId EventHubName = None |} - } - | _ -> - raiseFarmer - $"Unsupported resource type '{resourceId.Type}'. Supported types are {[ storageAccounts; workspaces ]}" - Dependencies = state.Dependencies.Add dependency + } + | _ -> + raiseFarmer + $"Unsupported resource type '{resourceId.Type}'. Supported types are {[ storageAccounts; workspaces ]}" + Dependencies = state.Dependencies.Add dependency } /// Adds a destination sink (either a storage account, log analytics workspace or event hub authorization rule) @@ -141,34 +137,35 @@ type DiagnosticSettingsBuilder() = let state = DiagnosticSettingsBuilder.AddDestinationPrivate(state, ruleId, (hub :> IBuilder).ResourceId) - { state with - Sinks = - { state.Sinks with - EventHub = - state.Sinks.EventHub - |> Option.map (fun h -> - {| h with - EventHubName = Some hub.Name + { + state with + Sinks = { + state.Sinks with + EventHub = + state.Sinks.EventHub + |> Option.map (fun h -> {| + h with + EventHubName = Some hub.Name |}) } } /// The name of the event hub. If none is specified, the default event hub will be selected. [] - member _.EventHubName(state: DiagnosticSettingsConfig, eventHubName) = - { state with - Sinks = - { state.Sinks with + member _.EventHubName(state: DiagnosticSettingsConfig, eventHubName) = { + state with + Sinks = { + state.Sinks with EventHub = match state.Sinks.EventHub with | Some hub -> - Some - {| hub with + Some {| + hub with EventHubName = Some eventHubName - |} + |} | None -> raiseFarmer "You must set the Authorization Rule Id before setting the event hub name" - } - } + } + } member this.EventHubName(state, eventHubName: string) = this.EventHubName(state, ResourceName eventHubName) @@ -177,13 +174,13 @@ type DiagnosticSettingsBuilder() = [] member _.DedicatedLogAnalyticsDestination(state: DiagnosticSettingsConfig, outputType) = match state.Sinks.LogAnalyticsWorkspace with - | Some (resourceId, _) -> - { state with - Sinks = - { state.Sinks with + | Some(resourceId, _) -> { + state with + Sinks = { + state.Sinks with LogAnalyticsWorkspace = Some(resourceId, outputType) - } - } + } + } | None -> raiseFarmer "You must first specify a Log Analytics sink before enabling dedicated outputs." /// Add metric settings to the resource. @@ -204,15 +201,15 @@ type DiagnosticSettingsBuilder() = this.Logs(state, logs |> Seq.map LogSetting.Create) interface IDependable with - member _.Add state newDeps = - { state with + member _.Add state newDeps = { + state with Dependencies = state.Dependencies + newDeps - } + } interface ITaggable with - member _.Add state tags = - { state with + member _.Add state tags = { + state with Tags = state.Tags |> Map.merge tags - } + } let diagnosticSettings = DiagnosticSettingsBuilder() diff --git a/src/Farmer/Builders/Builders.Disk.fs b/src/Farmer/Builders/Builders.Disk.fs index 001505ad2..a708312c5 100644 --- a/src/Farmer/Builders/Builders.Disk.fs +++ b/src/Farmer/Builders/Builders.Disk.fs @@ -4,96 +4,92 @@ module Farmer.Builders.Disk open Farmer open Farmer.Arm.Disk -type DiskConfig = - { - Name: ResourceName - Sku: Vm.DiskType option - Zones: string list - OsType: OS - CreationData: DiskCreation - Tags: Map - Dependencies: ResourceId Set - } +type DiskConfig = { + Name: ResourceName + Sku: Vm.DiskType option + Zones: string list + OsType: OS + CreationData: DiskCreation + Tags: Map + Dependencies: ResourceId Set +} with interface IBuilder with member this.ResourceId = disks.resourceId this.Name - member this.BuildResources location = - [ - { - Name = this.Name - Location = location - Sku = this.Sku - Zones = this.Zones - OsType = this.OsType - CreationData = this.CreationData - Tags = this.Tags - Dependencies = this.Dependencies - } - ] + member this.BuildResources location = [ + { + Name = this.Name + Location = location + Sku = this.Sku + Zones = this.Zones + OsType = this.OsType + CreationData = this.CreationData + Tags = this.Tags + Dependencies = this.Dependencies + } + ] type DiskBuilder() = // Default yields a 1 terabyte disk partitioned for Windows. - member _.Yield _ = - { - Name = ResourceName.Empty - Sku = None - Zones = [] - OsType = OS.Windows - CreationData = Empty 1024 - Tags = Map.empty - Dependencies = Set.empty - } + member _.Yield _ = { + Name = ResourceName.Empty + Sku = None + Zones = [] + OsType = OS.Windows + CreationData = Empty 1024 + Tags = Map.empty + Dependencies = Set.empty + } [] - member _.Name(config: DiskConfig, name) = - { config with Name = ResourceName name } + member _.Name(config: DiskConfig, name) = { config with Name = ResourceName name } [] member _.Name(config: DiskConfig, diskType) = { config with Sku = Some diskType } [] - member _.AddAvailabilityZone(state: DiskConfig, az: string) = - { state with + member _.AddAvailabilityZone(state: DiskConfig, az: string) = { + state with Zones = state.Zones @ [ az ] - } + } [] member _.OsType(config: DiskConfig, os) = { config with OsType = os } [] - member _.CreateEmpty(config: DiskConfig, size: int) = - { config with + member _.CreateEmpty(config: DiskConfig, size: int) = { + config with CreationData = Empty size - } + } [] - member _.Import(config: DiskConfig, sourceVhd: System.Uri, storageAccountId: ResourceId) = - { config with + member _.Import(config: DiskConfig, sourceVhd: System.Uri, storageAccountId: ResourceId) = { + config with CreationData = Import(sourceVhd, storageAccountId) - } + } - member _.Import(config: DiskConfig, sourceVhd: System.Uri, storageAccount: StorageAccountConfig) = - { config with + member _.Import(config: DiskConfig, sourceVhd: System.Uri, storageAccount: StorageAccountConfig) = { + config with CreationData = Import(sourceVhd, (storageAccount :> IBuilder).ResourceId) - } + } - member _.Import(config: DiskConfig, sourceVhd: System.Uri, storageAccountName: ResourceName) = - { config with + member _.Import(config: DiskConfig, sourceVhd: System.Uri, storageAccountName: ResourceName) = { + config with CreationData = Import(sourceVhd, Farmer.Arm.Storage.storageAccounts.resourceId storageAccountName) - } + } interface ITaggable with - member _.Add state tags = - { state with + member _.Add state tags = { + state with Tags = state.Tags |> Map.merge tags - } + } interface IDependable with - member _.Add state newDeps = - { state with + member _.Add state newDeps = { + state with Dependencies = state.Dependencies + newDeps - } + } let disk = DiskBuilder() diff --git a/src/Farmer/Builders/Builders.Dns.fs b/src/Farmer/Builders/Builders.Dns.fs index bc9af9f42..8009dd5d9 100644 --- a/src/Farmer/Builders/Builders.Dns.fs +++ b/src/Farmer/Builders/Builders.Dns.fs @@ -8,33 +8,31 @@ open Farmer.Dns open Farmer.Arm.Dns open DnsRecords -type DnsZoneRecordConfig = - { - Name: ResourceName - Dependencies: Set - Type: DnsRecordType - TTL: int - Zone: LinkedResource option - DnsZoneType: DnsZoneType +type DnsZoneRecordConfig = { + Name: ResourceName + Dependencies: Set + Type: DnsRecordType + TTL: int + Zone: LinkedResource option + DnsZoneType: DnsZoneType +} with + + static member Create(name, ttl, zone, recordType, ?dependencies: Set, ?zoneType) = { + Name = + if name = ResourceName.Empty then + raiseFarmer "You must set a DNS zone name" + + name + Dependencies = dependencies |> Option.defaultValue Set.empty + TTL = + match ttl with + | Some ttl -> ttl + | None -> raiseFarmer "You must set a TTL" + Zone = zone + DnsZoneType = zoneType |> Option.defaultValue Public + Type = recordType } - static member Create(name, ttl, zone, recordType, ?dependencies: Set, ?zoneType) = - { - Name = - if name = ResourceName.Empty then - raiseFarmer "You must set a DNS zone name" - - name - Dependencies = dependencies |> Option.defaultValue Set.empty - TTL = - match ttl with - | Some ttl -> ttl - | None -> raiseFarmer "You must set a TTL" - Zone = zone - DnsZoneType = zoneType |> Option.defaultValue Public - Type = recordType - } - interface IBuilder with member this.ResourceId = match this.Zone with @@ -43,116 +41,106 @@ type DnsZoneRecordConfig = member this.BuildResources _ = match this.Zone with - | Some zone -> - [ - { - DnsRecord.Name = this.Name - Dependencies = this.Dependencies - Zone = zone - ZoneType = this.DnsZoneType - TTL = this.TTL - Type = this.Type - } - ] + | Some zone -> [ + { + DnsRecord.Name = this.Name + Dependencies = this.Dependencies + Zone = zone + ZoneType = this.DnsZoneType + TTL = this.TTL + Type = this.Type + } + ] | None -> raiseFarmer "DNS record must be linked to a zone." -type CNameRecordProperties = - { - Name: ResourceName - Dependencies: Set - CName: string option - TTL: int option - Zone: LinkedResource option - TargetResource: ResourceId option - ZoneType: DnsZoneType - } - -type ARecordProperties = - { - Name: ResourceName - Dependencies: Set - Ipv4Addresses: string list - TTL: int option - Zone: LinkedResource option - TargetResource: ResourceId option - ZoneType: DnsZoneType - } - -type AaaaRecordProperties = - { - Name: ResourceName - Dependencies: Set - Ipv6Addresses: string list - TTL: int option - Zone: LinkedResource option - TargetResource: ResourceId option - ZoneType: DnsZoneType - } - -type NsRecordProperties = - { - Name: ResourceName - Dependencies: Set - NsdNames: NsRecords - TTL: int option - Zone: LinkedResource option - } - -type PtrRecordProperties = - { - Name: ResourceName - Dependencies: Set - PtrdNames: string list - TTL: int option - Zone: LinkedResource option - ZoneType: DnsZoneType - } - -type TxtRecordProperties = - { - Name: ResourceName - Dependencies: Set - TxtValues: string list - TTL: int option - Zone: LinkedResource option - ZoneType: DnsZoneType - } - -type MxRecordProperties = - { - Name: ResourceName - Dependencies: Set - MxValues: {| Preference: int; Exchange: string |} list - TTL: int option - Zone: LinkedResource option - ZoneType: DnsZoneType - } - -type SrvRecordProperties = - { - Name: ResourceName - Dependencies: Set - SrvValues: SrvRecord list - TTL: int option - Zone: LinkedResource option - ZoneType: DnsZoneType - } - -type SoaRecordProperties = - { - Name: ResourceName - Dependencies: Set - Host: string option - Email: string option - SerialNumber: int64 option - RefreshTime: int64 option - RetryTime: int64 option - ExpireTime: int64 option - MinimumTTL: int64 option - TTL: int option - Zone: LinkedResource option - ZoneType: DnsZoneType - } +type CNameRecordProperties = { + Name: ResourceName + Dependencies: Set + CName: string option + TTL: int option + Zone: LinkedResource option + TargetResource: ResourceId option + ZoneType: DnsZoneType +} + +type ARecordProperties = { + Name: ResourceName + Dependencies: Set + Ipv4Addresses: string list + TTL: int option + Zone: LinkedResource option + TargetResource: ResourceId option + ZoneType: DnsZoneType +} + +type AaaaRecordProperties = { + Name: ResourceName + Dependencies: Set + Ipv6Addresses: string list + TTL: int option + Zone: LinkedResource option + TargetResource: ResourceId option + ZoneType: DnsZoneType +} + +type NsRecordProperties = { + Name: ResourceName + Dependencies: Set + NsdNames: NsRecords + TTL: int option + Zone: LinkedResource option +} + +type PtrRecordProperties = { + Name: ResourceName + Dependencies: Set + PtrdNames: string list + TTL: int option + Zone: LinkedResource option + ZoneType: DnsZoneType +} + +type TxtRecordProperties = { + Name: ResourceName + Dependencies: Set + TxtValues: string list + TTL: int option + Zone: LinkedResource option + ZoneType: DnsZoneType +} + +type MxRecordProperties = { + Name: ResourceName + Dependencies: Set + MxValues: {| Preference: int; Exchange: string |} list + TTL: int option + Zone: LinkedResource option + ZoneType: DnsZoneType +} + +type SrvRecordProperties = { + Name: ResourceName + Dependencies: Set + SrvValues: SrvRecord list + TTL: int option + Zone: LinkedResource option + ZoneType: DnsZoneType +} + +type SoaRecordProperties = { + Name: ResourceName + Dependencies: Set + Host: string option + Email: string option + SerialNumber: int64 option + RefreshTime: int64 option + RetryTime: int64 option + ExpireTime: int64 option + MinimumTTL: int64 option + TTL: int option + Zone: LinkedResource option + ZoneType: DnsZoneType +} type DnsZone = static member getNameServers(resourceId: ResourceId) = @@ -165,13 +153,12 @@ type DnsZone = static member getNameServers(name: ResourceName, ?resourceGroup) = DnsZone.getNameServers (ResourceId.create (zones, name, ?group = resourceGroup)) -type DnsZoneConfig = - { - Name: ResourceName - Dependencies: Set - ZoneType: DnsZoneType - Records: DnsZoneRecordConfig list - } +type DnsZoneConfig = { + Name: ResourceName + Dependencies: Set + ZoneType: DnsZoneType + Records: DnsZoneRecordConfig list +} with /// Gets the ARM expression path to the NameServers. When evaluated, will return a JSON array as string. E.g.: """["ns1-01.azure-dns.com.","ns2-01.azure-dns.net.","ns3-01.azure-dns.org.","ns4-01.azure-dns.info."]""" member this.NameServers = DnsZone.getNameServers this.Name @@ -182,41 +169,39 @@ type DnsZoneConfig = | Public -> zones.resourceId this.Name | Private -> privateZones.resourceId this.Name - member this.BuildResources _ = - [ + member this.BuildResources _ = [ + { + DnsZone.Name = this.Name + Dependencies = this.Dependencies + Properties = {| ZoneType = this.ZoneType |> string |} + } + + for record in this.Records do { - DnsZone.Name = this.Name - Dependencies = this.Dependencies - Properties = {| ZoneType = this.ZoneType |> string |} + DnsRecord.Name = record.Name + Dependencies = record.Dependencies + Zone = + Managed( + match this.ZoneType with + | Public -> zones.resourceId this.Name + | Private -> privateZones.resourceId this.Name + ) + ZoneType = this.ZoneType + TTL = record.TTL + Type = record.Type } - - for record in this.Records do - { - DnsRecord.Name = record.Name - Dependencies = record.Dependencies - Zone = - Managed( - match this.ZoneType with - | Public -> zones.resourceId this.Name - | Private -> privateZones.resourceId this.Name - ) - ZoneType = this.ZoneType - TTL = record.TTL - Type = record.Type - } - ] + ] type DnsCNameRecordBuilder() = - member _.Yield _ = - { - CNameRecordProperties.CName = None - Name = ResourceName.Empty - Dependencies = Set.empty - TTL = None - Zone = None - TargetResource = None - ZoneType = Public - } + member _.Yield _ = { + CNameRecordProperties.CName = None + Name = ResourceName.Empty + Dependencies = Set.empty + TTL = None + Zone = None + TargetResource = None + ZoneType = Public + } member _.Run(state: CNameRecordProperties) = DnsZoneRecordConfig.Create( @@ -245,20 +230,20 @@ type DnsCNameRecordBuilder() = /// Sets the target resource of the record. [] - member _.RecordTargetResource(state: CNameRecordProperties, targetResource: ResourceId) = - { state with + member _.RecordTargetResource(state: CNameRecordProperties, targetResource: ResourceId) = { + state with TargetResource = Some targetResource - } + } - member _.RecordTargetResource(state: CNameRecordProperties, targetResource: IArmResource) = - { state with + member _.RecordTargetResource(state: CNameRecordProperties, targetResource: IArmResource) = { + state with TargetResource = Some targetResource.ResourceId - } + } - member _.RecordTargetResource(state: CNameRecordProperties, targetResource: IBuilder) = - { state with + member _.RecordTargetResource(state: CNameRecordProperties, targetResource: IBuilder) = { + state with TargetResource = Some targetResource.ResourceId - } + } /// Sets the zone_type of the record. [] @@ -266,44 +251,42 @@ type DnsCNameRecordBuilder() = /// Builds a record for an existing DNS zone that is not managed by this Farmer deployment. [] - member _.LinkToUnmanagedDnsZone(state: CNameRecordProperties, zone: ResourceId) = - { state with + member _.LinkToUnmanagedDnsZone(state: CNameRecordProperties, zone: ResourceId) = { + state with Zone = Some(Unmanaged zone) - } + } /// Builds a record for an existing DNS zone that is managed by this Farmer deployment. [] - member _.LinkToDnsZone(state: CNameRecordProperties, zone: ResourceId) = - { state with Zone = Some(Managed zone) } + member _.LinkToDnsZone(state: CNameRecordProperties, zone: ResourceId) = { state with Zone = Some(Managed zone) } - member _.LinkToDnsZone(state: CNameRecordProperties, zone: IArmResource) = - { state with + member _.LinkToDnsZone(state: CNameRecordProperties, zone: IArmResource) = { + state with Zone = Some(Managed zone.ResourceId) - } + } - member _.LinkToDnsZone(state: CNameRecordProperties, zone: IBuilder) = - { state with + member _.LinkToDnsZone(state: CNameRecordProperties, zone: IBuilder) = { + state with Zone = Some(Managed zone.ResourceId) - } + } /// Enable support for additional dependencies. interface IDependable with - member _.Add state newDeps = - { state with + member _.Add state newDeps = { + state with Dependencies = state.Dependencies + newDeps - } + } type DnsARecordBuilder() = - member _.Yield _ = - { - ARecordProperties.Ipv4Addresses = [] - Name = ResourceName "@" - Dependencies = Set.empty - TTL = None - Zone = None - TargetResource = None - ZoneType = Public - } + member _.Yield _ = { + ARecordProperties.Ipv4Addresses = [] + Name = ResourceName "@" + Dependencies = Set.empty + TTL = None + Zone = None + TargetResource = None + ZoneType = Public + } member _.Run(state: ARecordProperties) = DnsZoneRecordConfig.Create( @@ -324,10 +307,10 @@ type DnsARecordBuilder() = /// Sets the ipv4 address. [] - member _.RecordAddress(state: ARecordProperties, ipv4Addresses) = - { state with + member _.RecordAddress(state: ARecordProperties, ipv4Addresses) = { + state with Ipv4Addresses = state.Ipv4Addresses @ ipv4Addresses - } + } /// Sets the TTL of the record. [] @@ -335,42 +318,41 @@ type DnsARecordBuilder() = /// Sets the target resource of the record. [] - member _.RecordTargetResource(state: ARecordProperties, targetResource: ResourceId) = - { state with + member _.RecordTargetResource(state: ARecordProperties, targetResource: ResourceId) = { + state with TargetResource = Some targetResource - } + } - member _.RecordTargetResource(state: ARecordProperties, targetResource: IArmResource) = - { state with + member _.RecordTargetResource(state: ARecordProperties, targetResource: IArmResource) = { + state with TargetResource = Some targetResource.ResourceId - } + } - member _.RecordTargetResource(state: ARecordProperties, targetResource: IBuilder) = - { state with + member _.RecordTargetResource(state: ARecordProperties, targetResource: IBuilder) = { + state with TargetResource = Some targetResource.ResourceId - } + } /// Builds a record for an existing DNS zone. [] - member _.LinkToUnmanagedDnsZone(state: ARecordProperties, zone: ResourceId) = - { state with + member _.LinkToUnmanagedDnsZone(state: ARecordProperties, zone: ResourceId) = { + state with Zone = Some(Unmanaged zone) - } + } /// Builds a record for an existing DNS zone that is managed by this Farmer deployment. [] - member _.LinkToDnsZone(state: ARecordProperties, zone: ResourceId) = - { state with Zone = Some(Managed zone) } + member _.LinkToDnsZone(state: ARecordProperties, zone: ResourceId) = { state with Zone = Some(Managed zone) } - member _.LinkToDnsZone(state: ARecordProperties, zone: IArmResource) = - { state with + member _.LinkToDnsZone(state: ARecordProperties, zone: IArmResource) = { + state with Zone = Some(Managed zone.ResourceId) - } + } - member _.LinkToDnsZone(state: ARecordProperties, zone: IBuilder) = - { state with + member _.LinkToDnsZone(state: ARecordProperties, zone: IBuilder) = { + state with Zone = Some(Managed zone.ResourceId) - } + } /// Sets the zone_type of the record. [] @@ -378,22 +360,21 @@ type DnsARecordBuilder() = /// Enable support for additional dependencies. interface IDependable with - member _.Add state newDeps = - { state with + member _.Add state newDeps = { + state with Dependencies = state.Dependencies + newDeps - } + } type DnsAaaaRecordBuilder() = - member _.Yield _ = - { - AaaaRecordProperties.Ipv6Addresses = [] - Name = ResourceName "@" - Dependencies = Set.empty - TTL = None - Zone = None - TargetResource = None - ZoneType = Public - } + member _.Yield _ = { + AaaaRecordProperties.Ipv6Addresses = [] + Name = ResourceName "@" + Dependencies = Set.empty + TTL = None + Zone = None + TargetResource = None + ZoneType = Public + } member _.Run(state: AaaaRecordProperties) = DnsZoneRecordConfig.Create( @@ -414,10 +395,10 @@ type DnsAaaaRecordBuilder() = /// Sets the ipv6 address. [] - member _.RecordAddress(state: AaaaRecordProperties, ipv6Addresses) = - { state with + member _.RecordAddress(state: AaaaRecordProperties, ipv6Addresses) = { + state with Ipv6Addresses = state.Ipv6Addresses @ ipv6Addresses - } + } /// Sets the TTL of the record. [] @@ -425,42 +406,41 @@ type DnsAaaaRecordBuilder() = /// Sets the target resource of the record. [] - member _.RecordTargetResource(state: AaaaRecordProperties, targetResource: ResourceId) = - { state with + member _.RecordTargetResource(state: AaaaRecordProperties, targetResource: ResourceId) = { + state with TargetResource = Some targetResource - } + } - member _.RecordTargetResource(state: AaaaRecordProperties, targetResource: IArmResource) = - { state with + member _.RecordTargetResource(state: AaaaRecordProperties, targetResource: IArmResource) = { + state with TargetResource = Some targetResource.ResourceId - } + } - member _.RecordTargetResource(state: AaaaRecordProperties, targetResource: IBuilder) = - { state with + member _.RecordTargetResource(state: AaaaRecordProperties, targetResource: IBuilder) = { + state with TargetResource = Some targetResource.ResourceId - } + } /// Builds a record for an existing DNS zone. [] - member _.LinkToUnmanagedDnsZone(state: AaaaRecordProperties, zone: ResourceId) = - { state with + member _.LinkToUnmanagedDnsZone(state: AaaaRecordProperties, zone: ResourceId) = { + state with Zone = Some(Unmanaged zone) - } + } /// Builds a record for an existing DNS zone that is managed by this Farmer deployment. [] - member _.LinkToDnsZone(state: AaaaRecordProperties, zone: ResourceId) = - { state with Zone = Some(Managed zone) } + member _.LinkToDnsZone(state: AaaaRecordProperties, zone: ResourceId) = { state with Zone = Some(Managed zone) } - member _.LinkToDnsZone(state: AaaaRecordProperties, zone: IArmResource) = - { state with + member _.LinkToDnsZone(state: AaaaRecordProperties, zone: IArmResource) = { + state with Zone = Some(Managed zone.ResourceId) - } + } - member _.LinkToDnsZone(state: AaaaRecordProperties, zone: IBuilder) = - { state with + member _.LinkToDnsZone(state: AaaaRecordProperties, zone: IBuilder) = { + state with Zone = Some(Managed zone.ResourceId) - } + } /// Sets the zone_type of the record. [] @@ -468,20 +448,19 @@ type DnsAaaaRecordBuilder() = /// Enable support for additional dependencies. interface IDependable with - member _.Add state newDeps = - { state with + member _.Add state newDeps = { + state with Dependencies = state.Dependencies + newDeps - } + } type DnsNsRecordBuilder() = - member _.Yield _ = - { - NsRecordProperties.NsdNames = NsRecords.Records [] - Name = ResourceName "@" - Dependencies = Set.empty - TTL = None - Zone = None - } + member _.Yield _ = { + NsRecordProperties.NsdNames = NsRecords.Records [] + Name = ResourceName "@" + Dependencies = Set.empty + TTL = None + Zone = None + } member _.Run(state: NsRecordProperties) = DnsZoneRecordConfig.Create(state.Name, state.TTL, state.Zone, NS state.NsdNames, state.Dependencies) @@ -500,10 +479,10 @@ type DnsNsRecordBuilder() = | NsRecords.SourceZone _ -> raiseFarmer "Cannot add 'add_nsd_names' when using 'add_nsd_reference' to reference another zone's nameservers." - | NsRecords.Records existingNsdNames -> - { state with + | NsRecords.Records existingNsdNames -> { + state with NsdNames = NsRecords.Records(existingNsdNames @ nsdNames) - } + } /// Ensure no nsd records were already added that will be overwritten by the reference. member private this.validateNsdReference(state: NsRecordProperties) = @@ -517,22 +496,25 @@ type DnsNsRecordBuilder() = member this.RecordNsdNameReference(state: NsRecordProperties, dnsZoneResourceId: ResourceId) = this.validateNsdReference state - { state with - NsdNames = NsRecords.SourceZone dnsZoneResourceId + { + state with + NsdNames = NsRecords.SourceZone dnsZoneResourceId } member this.RecordNsdNameReference(state: NsRecordProperties, dnsZoneResourceId: IArmResource) = this.validateNsdReference state - { state with - NsdNames = NsRecords.SourceZone dnsZoneResourceId.ResourceId + { + state with + NsdNames = NsRecords.SourceZone dnsZoneResourceId.ResourceId } member this.RecordNsdNameReference(state: NsRecordProperties, dnsZoneConfig: DnsZoneConfig) = this.validateNsdReference state - { state with - NsdNames = NsRecords.SourceZone (dnsZoneConfig :> IBuilder).ResourceId + { + state with + NsdNames = NsRecords.SourceZone (dnsZoneConfig :> IBuilder).ResourceId } /// Sets the TTL of the record. @@ -541,43 +523,41 @@ type DnsNsRecordBuilder() = /// Builds a record for an existing DNS zone. [] - member _.LinkToUnmanagedDnsZone(state: NsRecordProperties, zone: ResourceId) = - { state with + member _.LinkToUnmanagedDnsZone(state: NsRecordProperties, zone: ResourceId) = { + state with Zone = Some(Unmanaged zone) - } + } /// Builds a record for an existing DNS zone that is managed by this Farmer deployment. [] - member _.LinkToDnsZone(state: NsRecordProperties, zone: ResourceId) = - { state with Zone = Some(Managed zone) } + member _.LinkToDnsZone(state: NsRecordProperties, zone: ResourceId) = { state with Zone = Some(Managed zone) } - member _.LinkToDnsZone(state: NsRecordProperties, zone: IArmResource) = - { state with + member _.LinkToDnsZone(state: NsRecordProperties, zone: IArmResource) = { + state with Zone = Some(Managed zone.ResourceId) - } + } - member _.LinkToDnsZone(state: NsRecordProperties, zone: IBuilder) = - { state with + member _.LinkToDnsZone(state: NsRecordProperties, zone: IBuilder) = { + state with Zone = Some(Managed zone.ResourceId) - } + } /// Enable support for additional dependencies. interface IDependable with - member _.Add state newDeps = - { state with + member _.Add state newDeps = { + state with Dependencies = state.Dependencies + newDeps - } + } type DnsPtrRecordBuilder() = - member _.Yield _ = - { - PtrRecordProperties.PtrdNames = [] - Name = ResourceName "@" - Dependencies = Set.empty - TTL = None - Zone = None - ZoneType = Public - } + member _.Yield _ = { + PtrRecordProperties.PtrdNames = [] + Name = ResourceName "@" + Dependencies = Set.empty + TTL = None + Zone = None + ZoneType = Public + } member _.Run(state: PtrRecordProperties) = DnsZoneRecordConfig.Create( @@ -598,10 +578,10 @@ type DnsPtrRecordBuilder() = /// Add PTR names [] - member _.RecordPtrdNames(state: PtrRecordProperties, ptrdNames) = - { state with + member _.RecordPtrdNames(state: PtrRecordProperties, ptrdNames) = { + state with PtrdNames = state.PtrdNames @ ptrdNames - } + } /// Sets the TTL of the record. [] @@ -609,25 +589,24 @@ type DnsPtrRecordBuilder() = /// Builds a record for an existing DNS zone. [] - member _.LinkToUnmanagedDnsZone(state: PtrRecordProperties, zone: ResourceId) = - { state with + member _.LinkToUnmanagedDnsZone(state: PtrRecordProperties, zone: ResourceId) = { + state with Zone = Some(Unmanaged zone) - } + } /// Builds a record for an existing DNS zone that is managed by this Farmer deployment. [] - member _.LinkToDnsZone(state: PtrRecordProperties, zone: ResourceId) = - { state with Zone = Some(Managed zone) } + member _.LinkToDnsZone(state: PtrRecordProperties, zone: ResourceId) = { state with Zone = Some(Managed zone) } - member _.LinkToDnsZone(state: PtrRecordProperties, zone: IArmResource) = - { state with + member _.LinkToDnsZone(state: PtrRecordProperties, zone: IArmResource) = { + state with Zone = Some(Managed zone.ResourceId) - } + } - member _.LinkToDnsZone(state: PtrRecordProperties, zone: IBuilder) = - { state with + member _.LinkToDnsZone(state: PtrRecordProperties, zone: IBuilder) = { + state with Zone = Some(Managed zone.ResourceId) - } + } /// Sets the zone_type of the record. [] @@ -635,21 +614,20 @@ type DnsPtrRecordBuilder() = /// Enable support for additional dependencies. interface IDependable with - member _.Add state newDeps = - { state with + member _.Add state newDeps = { + state with Dependencies = state.Dependencies + newDeps - } + } type DnsTxtRecordBuilder() = - member _.Yield _ = - { - TxtRecordProperties.Name = ResourceName "@" - Dependencies = Set.empty - TxtValues = [] - TTL = None - Zone = None - ZoneType = Public - } + member _.Yield _ = { + TxtRecordProperties.Name = ResourceName "@" + Dependencies = Set.empty + TxtValues = [] + TTL = None + Zone = None + ZoneType = Public + } member _.Run(state: TxtRecordProperties) = DnsZoneRecordConfig.Create( @@ -670,10 +648,10 @@ type DnsTxtRecordBuilder() = /// Add TXT values [] - member _.RecordValues(state: TxtRecordProperties, txtValues) = - { state with + member _.RecordValues(state: TxtRecordProperties, txtValues) = { + state with TxtValues = state.TxtValues @ txtValues - } + } /// Sets the TTL of the record. [] @@ -681,25 +659,24 @@ type DnsTxtRecordBuilder() = /// Builds a record for an existing DNS zone. [] - member _.LinkToUnmanagedDnsZone(state: TxtRecordProperties, zone: ResourceId) = - { state with + member _.LinkToUnmanagedDnsZone(state: TxtRecordProperties, zone: ResourceId) = { + state with Zone = Some(Unmanaged zone) - } + } /// Builds a record for an existing DNS zone that is managed by this Farmer deployment. [] - member _.LinkToDnsZone(state: TxtRecordProperties, zone: ResourceId) = - { state with Zone = Some(Managed zone) } + member _.LinkToDnsZone(state: TxtRecordProperties, zone: ResourceId) = { state with Zone = Some(Managed zone) } - member _.LinkToDnsZone(state: TxtRecordProperties, zone: IArmResource) = - { state with + member _.LinkToDnsZone(state: TxtRecordProperties, zone: IArmResource) = { + state with Zone = Some(Managed zone.ResourceId) - } + } - member _.LinkToDnsZone(state: TxtRecordProperties, zone: IBuilder) = - { state with + member _.LinkToDnsZone(state: TxtRecordProperties, zone: IBuilder) = { + state with Zone = Some(Managed zone.ResourceId) - } + } /// Sets the zone_type of the record. [] @@ -707,21 +684,20 @@ type DnsTxtRecordBuilder() = /// Enable support for additional dependencies. interface IDependable with - member _.Add state newDeps = - { state with + member _.Add state newDeps = { + state with Dependencies = state.Dependencies + newDeps - } + } type DnsMxRecordBuilder() = - member _.Yield _ = - { - MxRecordProperties.Name = ResourceName "@" - Dependencies = Set.empty - MxValues = [] - TTL = None - Zone = None - ZoneType = Public - } + member _.Yield _ = { + MxRecordProperties.Name = ResourceName "@" + Dependencies = Set.empty + MxValues = [] + TTL = None + Zone = None + ZoneType = Public + } member _.Run(state: MxRecordProperties) = DnsZoneRecordConfig.Create( @@ -742,17 +718,16 @@ type DnsMxRecordBuilder() = /// Add MX records. [] - member _.RecordValue(state: MxRecordProperties, mxValues: (int * string) list) = - { state with + member _.RecordValue(state: MxRecordProperties, mxValues: (int * string) list) = { + state with MxValues = state.MxValues @ (mxValues - |> List.map (fun x -> - {| - Preference = fst x - Exchange = snd x - |})) - } + |> List.map (fun x -> {| + Preference = fst x + Exchange = snd x + |})) + } /// Sets the TTL of the record. [] @@ -760,25 +735,24 @@ type DnsMxRecordBuilder() = /// Builds a record for an existing DNS zone. [] - member _.LinkToUnmanagedDnsZone(state: MxRecordProperties, zone: ResourceId) = - { state with + member _.LinkToUnmanagedDnsZone(state: MxRecordProperties, zone: ResourceId) = { + state with Zone = Some(Unmanaged zone) - } + } /// Builds a record for an existing DNS zone that is managed by this Farmer deployment. [] - member _.LinkToDnsZone(state: MxRecordProperties, zone: ResourceId) = - { state with Zone = Some(Managed zone) } + member _.LinkToDnsZone(state: MxRecordProperties, zone: ResourceId) = { state with Zone = Some(Managed zone) } - member _.LinkToDnsZone(state: MxRecordProperties, zone: IArmResource) = - { state with + member _.LinkToDnsZone(state: MxRecordProperties, zone: IArmResource) = { + state with Zone = Some(Managed zone.ResourceId) - } + } - member _.LinkToDnsZone(state: MxRecordProperties, zone: IBuilder) = - { state with + member _.LinkToDnsZone(state: MxRecordProperties, zone: IBuilder) = { + state with Zone = Some(Managed zone.ResourceId) - } + } /// Sets the zone_type of the record. [] @@ -786,21 +760,20 @@ type DnsMxRecordBuilder() = /// Enable support for additional dependencies. interface IDependable with - member _.Add state newDeps = - { state with + member _.Add state newDeps = { + state with Dependencies = state.Dependencies + newDeps - } + } type DnsSrvRecordBuilder() = - member _.Yield _ = - { - SrvRecordProperties.Name = ResourceName "@" - Dependencies = Set.empty - SrvValues = [] - TTL = None - Zone = None - ZoneType = Public - } + member _.Yield _ = { + SrvRecordProperties.Name = ResourceName "@" + Dependencies = Set.empty + SrvValues = [] + TTL = None + Zone = None + ZoneType = Public + } member _.Run(state: SrvRecordProperties) = DnsZoneRecordConfig.Create( @@ -821,10 +794,10 @@ type DnsSrvRecordBuilder() = /// Add SRV records. [] - member _.RecordValue(state: SrvRecordProperties, srvValues: SrvRecord list) = - { state with + member _.RecordValue(state: SrvRecordProperties, srvValues: SrvRecord list) = { + state with SrvValues = state.SrvValues @ srvValues - } + } /// Sets the TTL of the record. [] @@ -832,25 +805,24 @@ type DnsSrvRecordBuilder() = /// Builds a record for an existing DNS zone. [] - member _.LinkToUnmanagedDnsZone(state: SrvRecordProperties, zone: ResourceId) = - { state with + member _.LinkToUnmanagedDnsZone(state: SrvRecordProperties, zone: ResourceId) = { + state with Zone = Some(Unmanaged zone) - } + } /// Builds a record for an existing DNS zone that is managed by this Farmer deployment. [] - member _.LinkToDnsZone(state: SrvRecordProperties, zone: ResourceId) = - { state with Zone = Some(Managed zone) } + member _.LinkToDnsZone(state: SrvRecordProperties, zone: ResourceId) = { state with Zone = Some(Managed zone) } - member _.LinkToDnsZone(state: SrvRecordProperties, zone: IArmResource) = - { state with + member _.LinkToDnsZone(state: SrvRecordProperties, zone: IArmResource) = { + state with Zone = Some(Managed zone.ResourceId) - } + } - member _.LinkToDnsZone(state: SrvRecordProperties, zone: IBuilder) = - { state with + member _.LinkToDnsZone(state: SrvRecordProperties, zone: IBuilder) = { + state with Zone = Some(Managed zone.ResourceId) - } + } /// Sets the zone_type of the record. [] @@ -858,39 +830,37 @@ type DnsSrvRecordBuilder() = /// Enable support for additional dependencies. interface IDependable with - member _.Add state newDeps = - { state with + member _.Add state newDeps = { + state with Dependencies = state.Dependencies + newDeps - } + } type DnsSoaRecordBuilder() = - member _.Yield _ = - { - SoaRecordProperties.Name = ResourceName "@" - Dependencies = Set.empty - Host = None - Email = None - SerialNumber = None - RefreshTime = None - RetryTime = None - ExpireTime = None - MinimumTTL = None - TTL = None - Zone = None - ZoneType = Public - } + member _.Yield _ = { + SoaRecordProperties.Name = ResourceName "@" + Dependencies = Set.empty + Host = None + Email = None + SerialNumber = None + RefreshTime = None + RetryTime = None + ExpireTime = None + MinimumTTL = None + TTL = None + Zone = None + ZoneType = Public + } member _.Run(state: SoaRecordProperties) = - let value = - { - Host = state.Host - Email = state.Email - SerialNumber = state.SerialNumber - RefreshTime = state.RefreshTime - RetryTime = state.RetryTime - ExpireTime = state.ExpireTime - MinimumTTL = state.MinimumTTL - } + let value = { + Host = state.Host + Email = state.Email + SerialNumber = state.SerialNumber + RefreshTime = state.RefreshTime + RetryTime = state.RetryTime + ExpireTime = state.ExpireTime + MinimumTTL = state.MinimumTTL + } DnsZoneRecordConfig.Create(state.Name, state.TTL, state.Zone, SOA value, state.Dependencies, state.ZoneType) @@ -912,10 +882,10 @@ type DnsSoaRecordBuilder() = /// Sets the expire time for this SOA record in seconds. /// Defaults to 2419200 (28 days). [] - member _.RecordExpireTime(state: SoaRecordProperties, expireTime: int64) = - { state with + member _.RecordExpireTime(state: SoaRecordProperties, expireTime: int64) = { + state with ExpireTime = Some expireTime - } + } /// Sets the minimum time to live for this SOA record in seconds. /// Defaults to 300. @@ -925,25 +895,25 @@ type DnsSoaRecordBuilder() = /// Sets the refresh time for this SOA record in seconds. /// Defaults to 3600 (1 hour) [] - member _.RecordRefreshTime(state: SoaRecordProperties, refreshTime: int64) = - { state with + member _.RecordRefreshTime(state: SoaRecordProperties, refreshTime: int64) = { + state with RefreshTime = Some refreshTime - } + } /// Sets the retry time for this SOA record in seconds. /// Defaults to 300 seconds. [] - member _.RetryTime(state: SoaRecordProperties, retryTime: int64) = - { state with + member _.RetryTime(state: SoaRecordProperties, retryTime: int64) = { + state with RetryTime = Some retryTime - } + } /// Sets the serial number for this SOA record (required). [] - member _.RecordSerialNumber(state: SoaRecordProperties, serialNo: int64) = - { state with + member _.RecordSerialNumber(state: SoaRecordProperties, serialNo: int64) = { + state with SerialNumber = Some serialNo - } + } /// Sets the TTL of the record. [] @@ -951,25 +921,24 @@ type DnsSoaRecordBuilder() = /// Builds a record for an existing DNS zone. [] - member _.LinkToUnmanagedDnsZone(state: SoaRecordProperties, zone: ResourceId) = - { state with + member _.LinkToUnmanagedDnsZone(state: SoaRecordProperties, zone: ResourceId) = { + state with Zone = Some(Unmanaged zone) - } + } /// Builds a record for an existing DNS zone that is managed by this Farmer deployment. [] - member _.LinkToDnsZone(state: SoaRecordProperties, zone: ResourceId) = - { state with Zone = Some(Managed zone) } + member _.LinkToDnsZone(state: SoaRecordProperties, zone: ResourceId) = { state with Zone = Some(Managed zone) } - member _.LinkToDnsZone(state: SoaRecordProperties, zone: IArmResource) = - { state with + member _.LinkToDnsZone(state: SoaRecordProperties, zone: IArmResource) = { + state with Zone = Some(Managed zone.ResourceId) - } + } - member _.LinkToDnsZone(state: SoaRecordProperties, zone: IBuilder) = - { state with + member _.LinkToDnsZone(state: SoaRecordProperties, zone: IBuilder) = { + state with Zone = Some(Managed zone.ResourceId) - } + } /// Sets the zone_type of the record. [] @@ -977,28 +946,27 @@ type DnsSoaRecordBuilder() = /// Enable support for additional dependencies. interface IDependable with - member _.Add state newDeps = - { state with + member _.Add state newDeps = { + state with Dependencies = state.Dependencies + newDeps - } + } type DnsZoneBuilder() = - member _.Yield _ = - { - DnsZoneConfig.Name = ResourceName "" - Dependencies = Set.empty - Records = [] - ZoneType = Public - } + member _.Yield _ = { + DnsZoneConfig.Name = ResourceName "" + Dependencies = Set.empty + Records = [] + ZoneType = Public + } - member _.Run(state) : DnsZoneConfig = - { state with + member _.Run(state) : DnsZoneConfig = { + state with Name = if state.Name = ResourceName.Empty then raiseFarmer "You must set a DNS zone name" else state.Name - } + } /// Sets the name of the DNS Zone. [] @@ -1013,17 +981,17 @@ type DnsZoneBuilder() = /// Add DNS records to the DNS Zone. [] - member _.AddRecords(state: DnsZoneConfig, records) = - { state with + member _.AddRecords(state: DnsZoneConfig, records) = { + state with Records = state.Records @ records - } + } /// Enable support for additional dependencies. interface IDependable with - member _.Add state newDeps = - { state with + member _.Add state newDeps = { + state with Dependencies = state.Dependencies + newDeps - } + } let dnsZone = DnsZoneBuilder() let cnameRecord = DnsCNameRecordBuilder() diff --git a/src/Farmer/Builders/Builders.DnsResolver.fs b/src/Farmer/Builders/Builders.DnsResolver.fs index 7706144b3..7bec47ee6 100644 --- a/src/Farmer/Builders/Builders.DnsResolver.fs +++ b/src/Farmer/Builders/Builders.DnsResolver.fs @@ -7,81 +7,76 @@ open Farmer.Arm.Dns open Farmer.Arm.Dns.DnsResolver open Farmer.Arm.Dns.DnsForwardingRuleset -type DnsResolverInboundEndpointConfig = - { - Name: ResourceName - DnsResolverId: LinkedResource option - SubnetId: LinkedResource option - PrivateIpAllocations: AllocationMethod list - Dependencies: Set - Tags: Map - } +type DnsResolverInboundEndpointConfig = { + Name: ResourceName + DnsResolverId: LinkedResource option + SubnetId: LinkedResource option + PrivateIpAllocations: AllocationMethod list + Dependencies: Set + Tags: Map +} with interface IBuilder with - member this.BuildResources location = - [ - { - Name = this.Name - Location = location - DnsResolverId = - this.DnsResolverId - |> Option.defaultWith (fun _ -> raiseFarmer "Must set 'dns_resolver' for inboundEndpoint.") - SubnetId = - this.SubnetId - |> Option.defaultWith (fun _ -> raiseFarmer "Must set 'subnet' for inboundEndpoint.") - PrivateIpAllocations = - if this.PrivateIpAllocations.IsEmpty then - [ DynamicPrivateIp ] - else - this.PrivateIpAllocations - Dependencies = this.Dependencies - Tags = this.Tags - } - ] + member this.BuildResources location = [ + { + Name = this.Name + Location = location + DnsResolverId = + this.DnsResolverId + |> Option.defaultWith (fun _ -> raiseFarmer "Must set 'dns_resolver' for inboundEndpoint.") + SubnetId = + this.SubnetId + |> Option.defaultWith (fun _ -> raiseFarmer "Must set 'subnet' for inboundEndpoint.") + PrivateIpAllocations = + if this.PrivateIpAllocations.IsEmpty then + [ DynamicPrivateIp ] + else + this.PrivateIpAllocations + Dependencies = this.Dependencies + Tags = this.Tags + } + ] member this.ResourceId = dnsResolverInboundEndpoints.resourceId (this.DnsResolverId.Value.Name, this.Name) -type DnsResolverOutboundEndpointConfig = - { - Name: ResourceName - DnsResolverId: LinkedResource option - SubnetId: LinkedResource option - Dependencies: Set - Tags: Map - } +type DnsResolverOutboundEndpointConfig = { + Name: ResourceName + DnsResolverId: LinkedResource option + SubnetId: LinkedResource option + Dependencies: Set + Tags: Map +} with interface IBuilder with - member this.BuildResources location = - [ - { - Name = this.Name - Location = location - DnsResolverId = - this.DnsResolverId - |> Option.defaultWith (fun _ -> raiseFarmer "Must set 'dns_resolver' for inboundEndpoint.") - SubnetId = - this.SubnetId - |> Option.defaultWith (fun _ -> raiseFarmer "Must set 'subnet' for inboundEndpoint.") - Dependencies = this.Dependencies - Tags = this.Tags - } - ] + member this.BuildResources location = [ + { + Name = this.Name + Location = location + DnsResolverId = + this.DnsResolverId + |> Option.defaultWith (fun _ -> raiseFarmer "Must set 'dns_resolver' for inboundEndpoint.") + SubnetId = + this.SubnetId + |> Option.defaultWith (fun _ -> raiseFarmer "Must set 'subnet' for inboundEndpoint.") + Dependencies = this.Dependencies + Tags = this.Tags + } + ] member this.ResourceId = dnsResolverOutboundEndpoints.resourceId (this.DnsResolverId.Value.Name, this.Name) -type DnsResolverConfig = - { - Name: ResourceName - VirtualNetworkId: LinkedResource option - InboundEndpoints: DnsResolverInboundEndpointConfig list - InboundSubnetName: ResourceName option - OutboundEndpoints: DnsResolverOutboundEndpointConfig list - OutboundSubnetName: ResourceName option - Dependencies: Set - Tags: Map - } +type DnsResolverConfig = { + Name: ResourceName + VirtualNetworkId: LinkedResource option + InboundEndpoints: DnsResolverInboundEndpointConfig list + InboundSubnetName: ResourceName option + OutboundEndpoints: DnsResolverOutboundEndpointConfig list + OutboundSubnetName: ResourceName option + Dependencies: Set + Tags: Map +} with interface IBuilder with member this.BuildResources location = @@ -105,11 +100,11 @@ type DnsResolverConfig = Location = location DnsResolverId = Managed(dnsResolvers.resourceId this.Name) SubnetId = - Unmanaged - { vnetId.ResourceId with + Unmanaged { + vnetId.ResourceId with Type = Arm.Network.subnets Segments = [ subnet ] - } + } PrivateIpAllocations = [ DynamicPrivateIp ] Dependencies = Set.empty Tags = Map.empty @@ -122,26 +117,26 @@ type DnsResolverConfig = Location = location DnsResolverId = Managed(dnsResolvers.resourceId this.Name) SubnetId = - Unmanaged - { vnetId.ResourceId with + Unmanaged { + vnetId.ResourceId with Type = Arm.Network.subnets Segments = [ subnet ] - } + } Dependencies = Set.empty Tags = Map.empty } for inbound in this.InboundEndpoints do - let inbound = - { inbound with + let inbound = { + inbound with DnsResolverId = Some(Managed(dnsResolvers.resourceId this.Name)) - } + } yield! (inbound :> IBuilder).BuildResources location for outbound in this.OutboundEndpoints do - let outbound = - { outbound with + let outbound = { + outbound with DnsResolverId = Some(Managed(dnsResolvers.resourceId this.Name)) - } + } yield! (outbound :> IBuilder).BuildResources location ] @@ -149,281 +144,274 @@ type DnsResolverConfig = member this.ResourceId = dnsResolvers.resourceId this.Name type DnsResolverBuilder() = - member _.Yield _ = - { - Name = ResourceName.Empty - VirtualNetworkId = None - InboundEndpoints = [] - InboundSubnetName = None - OutboundEndpoints = [] - OutboundSubnetName = None - Dependencies = Set.empty - Tags = Map.empty - } + member _.Yield _ = { + Name = ResourceName.Empty + VirtualNetworkId = None + InboundEndpoints = [] + InboundSubnetName = None + OutboundEndpoints = [] + OutboundSubnetName = None + Dependencies = Set.empty + Tags = Map.empty + } [] - member _.Name(config: DnsResolverConfig, name: string) = - { config with Name = ResourceName name } + member _.Name(config: DnsResolverConfig, name: string) = { config with Name = ResourceName name } [] - member _.VirtualNetwork(config: DnsResolverConfig, id: ResourceId) = - { config with + member _.VirtualNetwork(config: DnsResolverConfig, id: ResourceId) = { + config with VirtualNetworkId = Some(Managed id) - } + } - member _.VirtualNetwork(config: DnsResolverConfig, name: string) = - { config with + member _.VirtualNetwork(config: DnsResolverConfig, name: string) = { + config with VirtualNetworkId = Some(Managed(Farmer.Arm.Network.virtualNetworks.resourceId (ResourceName name))) - } + } [] - member _.LinkToVirtualNetwork(config: DnsResolverConfig, id: ResourceId) = - { config with + member _.LinkToVirtualNetwork(config: DnsResolverConfig, id: ResourceId) = { + config with VirtualNetworkId = Some(Unmanaged id) - } + } - member _.LinkToVirtualNetwork(config: DnsResolverConfig, name: string) = - { config with + member _.LinkToVirtualNetwork(config: DnsResolverConfig, name: string) = { + config with VirtualNetworkId = Some(Unmanaged(Farmer.Arm.Network.virtualNetworks.resourceId (ResourceName name))) - } + } [] - member _.InboundSubnet(config: DnsResolverConfig, subnetName: string) = - { config with + member _.InboundSubnet(config: DnsResolverConfig, subnetName: string) = { + config with InboundSubnetName = Some(ResourceName subnetName) - } + } [] - member _.AddInboundEndpoints(config: DnsResolverConfig, inboundEndpoints) = - { config with + member _.AddInboundEndpoints(config: DnsResolverConfig, inboundEndpoints) = { + config with InboundEndpoints = config.InboundEndpoints @ inboundEndpoints - } + } [] - member _.OutboundSubnet(config: DnsResolverConfig, subnetName: string) = - { config with + member _.OutboundSubnet(config: DnsResolverConfig, subnetName: string) = { + config with OutboundSubnetName = Some(ResourceName subnetName) - } + } [] - member _.AddOutboundEndpoints(config: DnsResolverConfig, outboundEndpoints) = - { config with + member _.AddOutboundEndpoints(config: DnsResolverConfig, outboundEndpoints) = { + config with OutboundEndpoints = config.OutboundEndpoints @ outboundEndpoints - } + } interface ITaggable with - member _.Add state tags = - { state with + member _.Add state tags = { + state with Tags = state.Tags |> Map.merge tags - } + } interface IDependable with - member _.Add state newDeps = - { state with + member _.Add state newDeps = { + state with Dependencies = state.Dependencies + newDeps - } + } let dnsResolver = DnsResolverBuilder() type DnsResolverInboundEndpointBuilder() = - member _.Yield _ = - { - Name = ResourceName.Empty - DnsResolverId = None - SubnetId = None - PrivateIpAllocations = [] - Dependencies = Set.empty - Tags = Map.empty - } + member _.Yield _ = { + Name = ResourceName.Empty + DnsResolverId = None + SubnetId = None + PrivateIpAllocations = [] + Dependencies = Set.empty + Tags = Map.empty + } member _.Run(config: DnsResolverInboundEndpointConfig) = if config.SubnetId.IsNone then raiseFarmer "inboundEndpoint requires a 'subnet'." if config.PrivateIpAllocations.Length = 0 then - { config with - PrivateIpAllocations = [ DynamicPrivateIp ] + { + config with + PrivateIpAllocations = [ DynamicPrivateIp ] } else config [] - member _.Name(config: DnsResolverInboundEndpointConfig, name: string) = - { config with Name = ResourceName name } + member _.Name(config: DnsResolverInboundEndpointConfig, name: string) = { config with Name = ResourceName name } [] - member _.DnsResolverId(config: DnsResolverInboundEndpointConfig, id: ResourceId) = - { config with + member _.DnsResolverId(config: DnsResolverInboundEndpointConfig, id: ResourceId) = { + config with DnsResolverId = Some(Managed id) - } + } - member _.DnsResolverId(config: DnsResolverInboundEndpointConfig, dnsResolver: DnsResolverConfig) = - { config with + member _.DnsResolverId(config: DnsResolverInboundEndpointConfig, dnsResolver: DnsResolverConfig) = { + config with DnsResolverId = Some(Managed (dnsResolver :> IBuilder).ResourceId) - } + } - member _.DnsResolverId(config: DnsResolverInboundEndpointConfig, name: string) = - { config with + member _.DnsResolverId(config: DnsResolverInboundEndpointConfig, name: string) = { + config with DnsResolverId = Some(Managed(dnsResolvers.resourceId (ResourceName name))) - } + } [] - member _.LinkToDnsResolverId(config: DnsResolverInboundEndpointConfig, id: ResourceId) = - { config with + member _.LinkToDnsResolverId(config: DnsResolverInboundEndpointConfig, id: ResourceId) = { + config with DnsResolverId = Some(Unmanaged id) - } + } - member _.LinkToDnsResolverId(config: DnsResolverInboundEndpointConfig, dnsResolver: DnsResolverConfig) = - { config with + member _.LinkToDnsResolverId(config: DnsResolverInboundEndpointConfig, dnsResolver: DnsResolverConfig) = { + config with DnsResolverId = Some(Unmanaged (dnsResolver :> IBuilder).ResourceId) - } + } - member _.LinkToDnsResolverId(config: DnsResolverInboundEndpointConfig, name: string) = - { config with + member _.LinkToDnsResolverId(config: DnsResolverInboundEndpointConfig, name: string) = { + config with DnsResolverId = Some(Unmanaged(dnsResolvers.resourceId (ResourceName name))) - } + } [] - member _.Subnet(config: DnsResolverInboundEndpointConfig, id: ResourceId) = - { config with + member _.Subnet(config: DnsResolverInboundEndpointConfig, id: ResourceId) = { + config with SubnetId = Some(Managed id) - } + } [] - member _.LinkToSubnet(config: DnsResolverInboundEndpointConfig, id: ResourceId) = - { config with + member _.LinkToSubnet(config: DnsResolverInboundEndpointConfig, id: ResourceId) = { + config with SubnetId = Some(Unmanaged id) - } + } [] - member _.DynamicIpAllocation(state: DnsResolverInboundEndpointConfig) = - { state with + member _.DynamicIpAllocation(state: DnsResolverInboundEndpointConfig) = { + state with PrivateIpAllocations = state.PrivateIpAllocations @ [ AllocationMethod.DynamicPrivateIp ] - } + } [] - member _.StaticIpAllocation(state: DnsResolverInboundEndpointConfig, addr: System.Net.IPAddress) = - { state with + member _.StaticIpAllocation(state: DnsResolverInboundEndpointConfig, addr: System.Net.IPAddress) = { + state with PrivateIpAllocations = state.PrivateIpAllocations @ [ AllocationMethod.StaticPrivateIp addr ] - } + } - member _.StaticIpAllocation(state: DnsResolverInboundEndpointConfig, addr: string) = - { state with + member _.StaticIpAllocation(state: DnsResolverInboundEndpointConfig, addr: string) = { + state with PrivateIpAllocations = state.PrivateIpAllocations @ [ AllocationMethod.StaticPrivateIp(System.Net.IPAddress.Parse addr) ] - } + } interface ITaggable with - member _.Add state tags = - { state with + member _.Add state tags = { + state with Tags = state.Tags |> Map.merge tags - } + } interface IDependable with - member _.Add state newDeps = - { state with + member _.Add state newDeps = { + state with Dependencies = state.Dependencies + newDeps - } + } let dnsInboundEndpoint = DnsResolverInboundEndpointBuilder() type DnsResolverOutboundEndpointBuilder() = - member _.Yield _ = - { - Name = ResourceName.Empty - DnsResolverId = None - SubnetId = None - Dependencies = Set.empty - Tags = Map.empty - } + member _.Yield _ = { + Name = ResourceName.Empty + DnsResolverId = None + SubnetId = None + Dependencies = Set.empty + Tags = Map.empty + } [] - member _.Name(config: DnsResolverOutboundEndpointConfig, name: string) = - { config with Name = ResourceName name } + member _.Name(config: DnsResolverOutboundEndpointConfig, name: string) = { config with Name = ResourceName name } [] - member _.DnsResolverId(config: DnsResolverOutboundEndpointConfig, id: ResourceId) = - { config with + member _.DnsResolverId(config: DnsResolverOutboundEndpointConfig, id: ResourceId) = { + config with DnsResolverId = Some(Managed id) - } + } - member _.DnsResolverId(config: DnsResolverOutboundEndpointConfig, dnsResolver: DnsResolverConfig) = - { config with + member _.DnsResolverId(config: DnsResolverOutboundEndpointConfig, dnsResolver: DnsResolverConfig) = { + config with DnsResolverId = Some(Managed (dnsResolver :> IBuilder).ResourceId) - } + } - member _.DnsResolverId(config: DnsResolverOutboundEndpointConfig, name: string) = - { config with + member _.DnsResolverId(config: DnsResolverOutboundEndpointConfig, name: string) = { + config with DnsResolverId = Some(Managed(dnsResolvers.resourceId (ResourceName name))) - } + } [] - member _.LinkToDnsResolverId(config: DnsResolverOutboundEndpointConfig, id: ResourceId) = - { config with + member _.LinkToDnsResolverId(config: DnsResolverOutboundEndpointConfig, id: ResourceId) = { + config with DnsResolverId = Some(Unmanaged id) - } + } - member _.LinkToDnsResolverId(config: DnsResolverOutboundEndpointConfig, dnsResolver: DnsResolverConfig) = - { config with + member _.LinkToDnsResolverId(config: DnsResolverOutboundEndpointConfig, dnsResolver: DnsResolverConfig) = { + config with DnsResolverId = Some(Unmanaged (dnsResolver :> IBuilder).ResourceId) - } + } - member _.LinkToDnsResolverId(config: DnsResolverOutboundEndpointConfig, name: string) = - { config with + member _.LinkToDnsResolverId(config: DnsResolverOutboundEndpointConfig, name: string) = { + config with DnsResolverId = Some(Unmanaged(dnsResolvers.resourceId (ResourceName name))) - } + } [] - member _.Subnet(config: DnsResolverOutboundEndpointConfig, id: ResourceId) = - { config with + member _.Subnet(config: DnsResolverOutboundEndpointConfig, id: ResourceId) = { + config with SubnetId = Some(Managed id) - } + } [] - member _.LinkToSubnet(config: DnsResolverOutboundEndpointConfig, id: ResourceId) = - { config with + member _.LinkToSubnet(config: DnsResolverOutboundEndpointConfig, id: ResourceId) = { + config with SubnetId = Some(Unmanaged id) - } + } interface ITaggable with - member _.Add state tags = - { state with + member _.Add state tags = { + state with Tags = state.Tags |> Map.merge tags - } + } interface IDependable with - member _.Add state newDeps = - { state with + member _.Add state newDeps = { + state with Dependencies = state.Dependencies + newDeps - } + } let dnsOutboundEndpoint = DnsResolverOutboundEndpointBuilder() -type DnsForwardingRuleConfig = - { - Name: ResourceName - ForwardingRulesetId: LinkedResource option - DomainName: string option - ForwardingRuleState: FeatureFlag option - TargetDnsServers: System.Net.IPEndPoint list - } +type DnsForwardingRuleConfig = { + Name: ResourceName + ForwardingRulesetId: LinkedResource option + DomainName: string option + ForwardingRuleState: FeatureFlag option + TargetDnsServers: System.Net.IPEndPoint list +} with interface IBuilder with - member this.BuildResources _ = - [ - { - ForwardingRule.Name = this.Name - ForwardingRulesetId = - this.ForwardingRulesetId - |> Option.defaultWith (fun _ -> raiseFarmer "DNS forwarding rule must be linked to a ruleset.") - DomainName = - this.DomainName - |> Option.defaultWith (fun () -> raiseFarmer "DNS forwarding rule requires a domain.") - ForwardingRuleState = this.ForwardingRuleState - TargetDnsServers = this.TargetDnsServers - } - ] + member this.BuildResources _ = [ + { + ForwardingRule.Name = this.Name + ForwardingRulesetId = + this.ForwardingRulesetId + |> Option.defaultWith (fun _ -> raiseFarmer "DNS forwarding rule must be linked to a ruleset.") + DomainName = + this.DomainName + |> Option.defaultWith (fun () -> raiseFarmer "DNS forwarding rule requires a domain.") + ForwardingRuleState = this.ForwardingRuleState + TargetDnsServers = this.TargetDnsServers + } + ] member this.ResourceId = match this.ForwardingRulesetId with @@ -431,14 +419,13 @@ type DnsForwardingRuleConfig = | Some ruleset -> dnsForwardingRulesetForwardingRules.resourceId (ruleset.Name / this.Name) type DnsForwardingRuleBuilder() = - member _.Yield _ = - { - Name = ResourceName.Empty - ForwardingRulesetId = None - DomainName = None - ForwardingRuleState = None - TargetDnsServers = [] - } + member _.Yield _ = { + Name = ResourceName.Empty + ForwardingRulesetId = None + DomainName = None + ForwardingRuleState = None + TargetDnsServers = [] + } member this.Run config = if config.DomainName.IsNone then @@ -447,19 +434,18 @@ type DnsForwardingRuleBuilder() = config [] - member _.Name(config: DnsForwardingRuleConfig, name: string) = - { config with Name = ResourceName name } + member _.Name(config: DnsForwardingRuleConfig, name: string) = { config with Name = ResourceName name } [] - member _.ForwardingRulesetId(config: DnsForwardingRuleConfig, id: ResourceId) = - { config with + member _.ForwardingRulesetId(config: DnsForwardingRuleConfig, id: ResourceId) = { + config with ForwardingRulesetId = Some(Unmanaged id) - } + } - member _.ForwardingRulesetId(config: DnsForwardingRuleConfig, name: string) = - { config with + member _.ForwardingRulesetId(config: DnsForwardingRuleConfig, name: string) = { + config with ForwardingRulesetId = Some(Unmanaged(dnsForwardingRulesets.resourceId (ResourceName name))) - } + } [] member _.DomainName(config: DnsForwardingRuleConfig, domainName: string) = @@ -469,21 +455,22 @@ type DnsForwardingRuleBuilder() = else $"{domainName}." - { config with - DomainName = Some domainNameWithDot + { + config with + DomainName = Some domainNameWithDot } [] - member _.State(config: DnsForwardingRuleConfig, state) = - { config with + member _.State(config: DnsForwardingRuleConfig, state) = { + config with ForwardingRuleState = Some state - } + } [] - member _.AddTargetDnsServers(config: DnsForwardingRuleConfig, targetDnsServers: System.Net.IPEndPoint list) = - { config with + member _.AddTargetDnsServers(config: DnsForwardingRuleConfig, targetDnsServers: System.Net.IPEndPoint list) = { + config with TargetDnsServers = config.TargetDnsServers @ targetDnsServers - } + } member _.AddTargetDnsServers(config: DnsForwardingRuleConfig, targetDnsServers: string list) = let parseIpEndpoint (s: string) = @@ -501,21 +488,21 @@ type DnsForwardingRuleBuilder() = let targetDnsServers = targetDnsServers |> List.map parseIpEndpoint // Need to fulfill this since it's not in netstandard2.0 - { config with - TargetDnsServers = config.TargetDnsServers @ targetDnsServers + { + config with + TargetDnsServers = config.TargetDnsServers @ targetDnsServers } let dnsForwardingRule = DnsForwardingRuleBuilder() -type DnsForwardingRulesetConfig = - { - Name: ResourceName - DnsResolverOutboundEndpointIds: ResourceId Set - Rules: DnsForwardingRuleConfig list - VnetLinks: LinkedResource list - Dependencies: Set - Tags: Map - } +type DnsForwardingRulesetConfig = { + Name: ResourceName + DnsResolverOutboundEndpointIds: ResourceId Set + Rules: DnsForwardingRuleConfig list + VnetLinks: LinkedResource list + Dependencies: Set + Tags: Map +} with interface IBuilder with member this.BuildResources location = @@ -531,46 +518,43 @@ type DnsForwardingRulesetConfig = :> IArmResource for ruleConfig in this.Rules do - let ruleWithRuleset = - { ruleConfig with + let ruleWithRuleset = { + ruleConfig with ForwardingRulesetId = Some(Managed (this :> IBuilder).ResourceId) - } + } for rule in (ruleWithRuleset :> IBuilder).BuildResources location do yield rule for vnetLink in this.VnetLinks do - yield - { - VirtualNetworkLink.Name = this.Name / vnetLink.Name - ForwardingRulesetId = Managed (this :> IBuilder).ResourceId - VirtualNetworkId = vnetLink - } + yield { + VirtualNetworkLink.Name = this.Name / vnetLink.Name + ForwardingRulesetId = Managed (this :> IBuilder).ResourceId + VirtualNetworkId = vnetLink + } } |> List.ofSeq member this.ResourceId = dnsForwardingRulesets.resourceId this.Name type DnsForwardingRulesetBuilder() = - member _.Yield _ = - { - Name = ResourceName.Empty - DnsResolverOutboundEndpointIds = Set.empty - Rules = [] - VnetLinks = [] - Dependencies = Set.empty - Tags = Map.empty - } + member _.Yield _ = { + Name = ResourceName.Empty + DnsResolverOutboundEndpointIds = Set.empty + Rules = [] + VnetLinks = [] + Dependencies = Set.empty + Tags = Map.empty + } [] - member _.Name(config: DnsForwardingRulesetConfig, name: string) = - { config with Name = ResourceName name } + member _.Name(config: DnsForwardingRulesetConfig, name: string) = { config with Name = ResourceName name } [] - member _.AddResolverOutboundEndpoints(config: DnsForwardingRulesetConfig, outboundIds: ResourceId list) = - { config with + member _.AddResolverOutboundEndpoints(config: DnsForwardingRulesetConfig, outboundIds: ResourceId list) = { + config with DnsResolverOutboundEndpointIds = config.DnsResolverOutboundEndpointIds |> Set.union (Set.ofList outboundIds) - } + } member _.AddResolverOutboundEndpoints ( @@ -582,37 +566,38 @@ type DnsForwardingRulesetBuilder() = |> List.map (fun oe -> (oe :> IBuilder).ResourceId) |> Set.ofList - { config with - DnsResolverOutboundEndpointIds = config.DnsResolverOutboundEndpointIds |> Set.union outboundEndpointIds + { + config with + DnsResolverOutboundEndpointIds = config.DnsResolverOutboundEndpointIds |> Set.union outboundEndpointIds } [] - member _.AddRules(config: DnsForwardingRulesetConfig, rules: DnsForwardingRuleConfig list) = - { config with + member _.AddRules(config: DnsForwardingRulesetConfig, rules: DnsForwardingRuleConfig list) = { + config with Rules = config.Rules @ rules - } + } [] - member _.AddVnetLinks(config: DnsForwardingRulesetConfig, vnetLinks: ResourceId list) = - { config with + member _.AddVnetLinks(config: DnsForwardingRulesetConfig, vnetLinks: ResourceId list) = { + config with VnetLinks = config.VnetLinks @ (vnetLinks |> List.map Unmanaged) - } + } - member _.AddVnetLinks(config: DnsForwardingRulesetConfig, vnetBuilders: IBuilder list) = - { config with + member _.AddVnetLinks(config: DnsForwardingRulesetConfig, vnetBuilders: IBuilder list) = { + config with VnetLinks = config.VnetLinks @ (vnetBuilders |> List.map (fun b -> Unmanaged b.ResourceId)) - } + } interface ITaggable with - member _.Add state tags = - { state with + member _.Add state tags = { + state with Tags = state.Tags |> Map.merge tags - } + } interface IDependable with - member _.Add state newDeps = - { state with + member _.Add state newDeps = { + state with Dependencies = state.Dependencies + newDeps - } + } let dnsForwardingRuleset = DnsForwardingRulesetBuilder() diff --git a/src/Farmer/Builders/Builders.EventGrid.fs b/src/Farmer/Builders/Builders.EventGrid.fs index 929609be0..d64e4055e 100644 --- a/src/Farmer/Builders/Builders.EventGrid.fs +++ b/src/Farmer/Builders/Builders.EventGrid.fs @@ -181,76 +181,76 @@ module SystemEvents = let ClientConnectionDisconnected = toEvent "Microsoft.SignalRService.ClientConnectionDisconnected" -type EventGridConfig<'T> = - { - TopicName: ResourceName - Source: ResourceName * TopicType - Subscriptions: {| Name: ResourceName - Destination: ResourceName - Endpoint: EndpointType - SystemEvents: EventGridEvent<'T> list |} list - Tags: Map - } +type EventGridConfig<'T> = { + TopicName: ResourceName + Source: ResourceName * TopicType + Subscriptions: + {| + Name: ResourceName + Destination: ResourceName + Endpoint: EndpointType + SystemEvents: EventGridEvent<'T> list + |} list + Tags: Map +} with interface IBuilder with member this.ResourceId = systemTopics.resourceId this.TopicName - member this.BuildResources location = - [ + member this.BuildResources location = [ + { + Name = this.TopicName + Location = location + Source = fst this.Source + TopicType = snd this.Source + Tags = this.Tags + } + + for sub in this.Subscriptions do { - Name = this.TopicName - Location = location - Source = fst this.Source - TopicType = snd this.Source - Tags = this.Tags + Name = sub.Name + Topic = this.TopicName + Destination = sub.Destination + DestinationEndpoint = sub.Endpoint + Events = sub.SystemEvents } - - for sub in this.Subscriptions do - { - Name = sub.Name - Topic = this.TopicName - Destination = sub.Destination - DestinationEndpoint = sub.Endpoint - Events = sub.SystemEvents - } - ] + ] type EventGridBuilder() = - static member private ChangeTopic<'TNew>(state: EventGridConfig<_>, source, topic) : EventGridConfig<'TNew> = - { - TopicName = state.TopicName - Source = source, topic - Subscriptions = [] - Tags = Map.empty - } + static member private ChangeTopic<'TNew>(state: EventGridConfig<_>, source, topic) : EventGridConfig<'TNew> = { + TopicName = state.TopicName + Source = source, topic + Subscriptions = [] + Tags = Map.empty + } static member private AddSub(state: EventGridConfig<'T>, name, destination: ResourceName, endpoint, events) = let name = destination.Value + "-" + name - { state with - Subscriptions = - {| - Name = ResourceName name - Destination = destination - Endpoint = endpoint - SystemEvents = events - |} - :: state.Subscriptions - } - - member _.Yield _ = { - TopicName = ResourceName.Empty - Source = ResourceName.Empty, TopicType(ResourceType("", ""), "") - Subscriptions = [] - Tags = Map.empty + state with + Subscriptions = + {| + Name = ResourceName name + Destination = destination + Endpoint = endpoint + SystemEvents = events + |} + :: state.Subscriptions } + member _.Yield _ = { + TopicName = ResourceName.Empty + Source = ResourceName.Empty, TopicType(ResourceType("", ""), "") + Subscriptions = [] + Tags = Map.empty + } + [] - member _.Name(state: EventGridConfig<'T>, name) = - { state with + member _.Name(state: EventGridConfig<'T>, name) = { + state with TopicName = ResourceName name - } + } [] member _.Source(state: EventGridConfig<_>, source: StorageAccountConfig) = @@ -351,10 +351,10 @@ type EventGridBuilder() = EventGridBuilder.AddSub(state, name, topic.Name, endpoint, events) [] - member _.Tags(state: EventGridConfig<'T>, pairs) = - { state with + member _.Tags(state: EventGridConfig<'T>, pairs) = { + state with Tags = pairs |> List.fold (fun map (key, value) -> Map.add key value map) state.Tags - } + } [] member this.Tag(state: EventGridConfig<'T>, key, value) = this.Tags(state, [ (key, value) ]) diff --git a/src/Farmer/Builders/Builders.EventHub.fs b/src/Farmer/Builders/Builders.EventHub.fs index b0e854c2b..eda5bd4da 100644 --- a/src/Farmer/Builders/Builders.EventHub.fs +++ b/src/Farmer/Builders/Builders.EventHub.fs @@ -11,22 +11,21 @@ open EventHubs /// Shortcut for Manage, Send and Listen rights. let AllAuthorizationRights = [ Manage; Send; Listen ] -type EventHubConfig = - { - EventHubNamespace: ResourceRef - Name: ResourceName - Sku: EventHubSku - Capacity: int - ZoneRedundant: bool option - ThroughputSettings: InflateSetting option - MessageRetentionInDays: int option - Partitions: int - ConsumerGroups: ResourceName Set - CaptureDestination: CaptureDestination option - AuthorizationRules: Map - Dependencies: ResourceId Set - Tags: Map - } +type EventHubConfig = { + EventHubNamespace: ResourceRef + Name: ResourceName + Sku: EventHubSku + Capacity: int + ZoneRedundant: bool option + ThroughputSettings: InflateSetting option + MessageRetentionInDays: int option + Partitions: int + ConsumerGroups: ResourceName Set + CaptureDestination: CaptureDestination option + AuthorizationRules: Map + Dependencies: ResourceId Set + Tags: Map +} with member private this.CreateKeyExpression(resourceId: ResourceId) = ArmExpression @@ -51,92 +50,84 @@ type EventHubConfig = interface IBuilder with member this.ResourceId = namespaces.resourceId this.EventHubNamespaceName - member this.BuildResources location = - [ - let eventHubName = - this.Name.Map(fun hubName -> $"{this.EventHubNamespaceName.Value}/{hubName}") - - // Namespace - match this.EventHubNamespace with - | DeployableResource this _ -> - { - Name = this.EventHubNamespaceName - Location = location - Sku = - {| - Name = this.Sku - Capacity = this.Capacity - |} - ZoneRedundant = this.ZoneRedundant - AutoInflateSettings = this.ThroughputSettings - Tags = this.Tags - } - | _ -> () - - // Event hub + member this.BuildResources location = [ + let eventHubName = + this.Name.Map(fun hubName -> $"{this.EventHubNamespaceName.Value}/{hubName}") + + // Namespace + match this.EventHubNamespace with + | DeployableResource this _ -> { + Name = this.EventHubNamespaceName + Location = location + Sku = {| + Name = this.Sku + Capacity = this.Capacity + |} + ZoneRedundant = this.ZoneRedundant + AutoInflateSettings = this.ThroughputSettings + Tags = this.Tags + } + | _ -> () + + // Event hub + { + Name = eventHubName + Location = location + MessageRetentionDays = this.MessageRetentionInDays + Partitions = this.Partitions + CaptureDestination = this.CaptureDestination + Dependencies = + Set [ + namespaces.resourceId this.EventHubNamespaceName + yield! + this.CaptureDestination + |> Option.mapList (fun (StorageAccount(name, _)) -> storageAccounts.resourceId name) + yield! this.Dependencies + ] + Tags = this.Tags + } + + // Consumer groups + for consumerGroup in this.ConsumerGroups do { - Name = eventHubName + ConsumerGroupName = consumerGroup + EventHub = eventHubName Location = location - MessageRetentionDays = this.MessageRetentionInDays - Partitions = this.Partitions - CaptureDestination = this.CaptureDestination - Dependencies = - Set - [ - namespaces.resourceId this.EventHubNamespaceName - yield! - this.CaptureDestination - |> Option.mapList (fun (StorageAccount (name, _)) -> storageAccounts.resourceId name) - yield! this.Dependencies - ] - Tags = this.Tags + Dependencies = [ + namespaces.resourceId this.EventHubNamespaceName + eventHubs.resourceId (this.EventHubNamespaceName, this.Name) + ] } - // Consumer groups - for consumerGroup in this.ConsumerGroups do - { - ConsumerGroupName = consumerGroup - EventHub = eventHubName - Location = location - Dependencies = - [ - namespaces.resourceId this.EventHubNamespaceName - eventHubs.resourceId (this.EventHubNamespaceName, this.Name) - ] - } - - // Auth rules - for rule in this.AuthorizationRules do - { - Name = - rule.Key.Map(fun rule -> $"{this.EventHubNamespaceName.Value}/{this.Name.Value}/%s{rule}") - Location = location - Dependencies = - [ - namespaces.resourceId this.EventHubNamespaceName - eventHubs.resourceId (this.EventHubNamespaceName, this.Name) - ] - Rights = rule.Value - } - ] + // Auth rules + for rule in this.AuthorizationRules do + { + Name = rule.Key.Map(fun rule -> $"{this.EventHubNamespaceName.Value}/{this.Name.Value}/%s{rule}") + Location = location + Dependencies = [ + namespaces.resourceId this.EventHubNamespaceName + eventHubs.resourceId (this.EventHubNamespaceName, this.Name) + ] + Rights = rule.Value + } + ] type EventHubBuilder() = - member _.Yield _ = - { - Name = ResourceName "hub" - EventHubNamespace = derived (fun config -> namespaces.resourceId (config.Name - "ns")) - Sku = Standard - Capacity = 1 - ZoneRedundant = None - ThroughputSettings = None - MessageRetentionInDays = None - Partitions = 1 - CaptureDestination = None - ConsumerGroups = Set.empty - AuthorizationRules = Map.empty - Dependencies = Set.empty - Tags = Map.empty - } + member _.Yield _ = { + Name = ResourceName "hub" + EventHubNamespace = derived (fun config -> namespaces.resourceId (config.Name - "ns")) + Sku = Standard + Capacity = 1 + ZoneRedundant = None + ThroughputSettings = None + MessageRetentionInDays = None + Partitions = 1 + CaptureDestination = None + ConsumerGroups = Set.empty + AuthorizationRules = Map.empty + Dependencies = Set.empty + Tags = Map.empty + } /// Sets the name of the Event Hub instance. [] @@ -146,20 +137,20 @@ type EventHubBuilder() = /// Sets the name of the Event Hub namespace. [] - member _.NamespaceName(state: EventHubConfig, name) = - { state with + member _.NamespaceName(state: EventHubConfig, name) = { + state with EventHubNamespace = named namespaces name - } + } member this.NamespaceName(state: EventHubConfig, name) = this.NamespaceName(state, ResourceName name) /// Sets the name of the Event Hub namespace. [] - member _.LinkToNamespaceName(state: EventHubConfig, name) = - { state with + member _.LinkToNamespaceName(state: EventHubConfig, name) = { + state with EventHubNamespace = managed namespaces name - } + } member this.LinkToNamespaceName(state: EventHubConfig, name) = this.LinkToNamespaceName(state, ResourceName name) @@ -172,61 +163,60 @@ type EventHubBuilder() = member _.ReplicaCount(state: EventHubConfig, capacity: int) = { state with Capacity = capacity } [] - member _.ZoneRedundant(state: EventHubConfig) = - { state with ZoneRedundant = Some true } + member _.ZoneRedundant(state: EventHubConfig) = { state with ZoneRedundant = Some true } [] - member _.AutoInflate(state: EventHubConfig, maxThroughput) = - { state with + member _.AutoInflate(state: EventHubConfig, maxThroughput) = { + state with ThroughputSettings = Some(AutoInflate maxThroughput) - } + } [] - member _.MaximumThroughputUnits(state: EventHubConfig) = - { state with + member _.MaximumThroughputUnits(state: EventHubConfig) = { + state with ThroughputSettings = Some ManualInflate - } + } [] - member _.MessageRetentionDays(state: EventHubConfig, days) = - { state with + member _.MessageRetentionDays(state: EventHubConfig, days) = { + state with MessageRetentionInDays = Some days - } + } [] member _.Partitions(state: EventHubConfig, partitions) = { state with Partitions = partitions } [] - member _.AddConsumerGroup(state: EventHubConfig, name) = - { state with + member _.AddConsumerGroup(state: EventHubConfig, name) = { + state with ConsumerGroups = state.ConsumerGroups.Add(ResourceName name) - } + } [] - member _.AddAuthorizationRule(state: EventHubConfig, name, rights) = - { state with + member _.AddAuthorizationRule(state: EventHubConfig, name, rights) = { + state with AuthorizationRules = state.AuthorizationRules.Add(ResourceName name, Set rights) - } + } [] - member _.CaptureToStorage(state: EventHubConfig, storageName: ResourceName, container) = - { state with + member _.CaptureToStorage(state: EventHubConfig, storageName: ResourceName, container) = { + state with CaptureDestination = Some(StorageAccount(storageName, container)) - } + } member this.CaptureToStorage(state: EventHubConfig, storageAccount: StorageAccountConfig, container) = this.CaptureToStorage(state, storageAccount.Name.ResourceName, container) interface IDependable with - member _.Add state newDeps = - { state with + member _.Add state newDeps = { + state with Dependencies = state.Dependencies + newDeps - } + } interface ITaggable with - member _.Add state tags = - { state with + member _.Add state tags = { + state with Tags = state.Tags |> Map.merge tags - } + } let eventHub = EventHubBuilder() diff --git a/src/Farmer/Builders/Builders.ExpressRoute.fs b/src/Farmer/Builders/Builders.ExpressRoute.fs index dc93e4a81..11b64b41a 100644 --- a/src/Farmer/Builders/Builders.ExpressRoute.fs +++ b/src/Farmer/Builders/Builders.ExpressRoute.fs @@ -7,53 +7,52 @@ open System.Net open Farmer.Arm.Network /// An IP address block in CIDR notation, such as 10.100.0.0/16. -type ExpressRouteCircuitPeering = - { - PeeringType: PeeringType - AzureASN: int - PeerASN: int64 - /// A /30 IP address block to use for the primary link - PrimaryPeerAddressPrefix: IPAddressCidr - /// A /30 IP address block to use for the secondary link - SecondaryPeerAddressPrefix: IPAddressCidr - SharedKey: string option - VlanId: int - } - -type ExpressRouteCircuitPeeringConfig = - { - /// The peering type - PeeringType: PeeringType - /// Azure-side BGP Autonomous System Number (ASN) - AzureASN: int - /// Peer-side BGP Autonomous System Number (ASN) - PeerASN: int64 - /// A /30 IP address block to use for the primary link - PrimaryPeerAddressPrefix: IPAddressCidr - /// A /30 IP address block to use for the secondary link - SecondaryPeerAddressPrefix: IPAddressCidr - /// An optional shared key that can be used when creating the peering - SharedKey: string option - /// The VLAN tag. - VlanId: int - } +type ExpressRouteCircuitPeering = { + PeeringType: PeeringType + AzureASN: int + PeerASN: int64 + /// A /30 IP address block to use for the primary link + PrimaryPeerAddressPrefix: IPAddressCidr + /// A /30 IP address block to use for the secondary link + SecondaryPeerAddressPrefix: IPAddressCidr + SharedKey: string option + VlanId: int +} + +type ExpressRouteCircuitPeeringConfig = { + /// The peering type + PeeringType: PeeringType + /// Azure-side BGP Autonomous System Number (ASN) + AzureASN: int + /// Peer-side BGP Autonomous System Number (ASN) + PeerASN: int64 + /// A /30 IP address block to use for the primary link + PrimaryPeerAddressPrefix: IPAddressCidr + /// A /30 IP address block to use for the secondary link + SecondaryPeerAddressPrefix: IPAddressCidr + /// An optional shared key that can be used when creating the peering + SharedKey: string option + /// The VLAN tag. + VlanId: int +} type ExpressRouteCircuitPeeringBuilder() = - member _.Yield _ = - { - PeeringType = AzurePrivatePeering - AzureASN = 0 - PeerASN = 0L - PrimaryPeerAddressPrefix = { Address = IPAddress.None; Prefix = 0 } - SecondaryPeerAddressPrefix = { Address = IPAddress.None; Prefix = 0 } - SharedKey = None - VlanId = 0 - } + member _.Yield _ = { + PeeringType = AzurePrivatePeering + AzureASN = 0 + PeerASN = 0L + PrimaryPeerAddressPrefix = { Address = IPAddress.None; Prefix = 0 } + SecondaryPeerAddressPrefix = { Address = IPAddress.None; Prefix = 0 } + SharedKey = None + VlanId = 0 + } /// Sets the peering type. [] - member _.PeeringType(state: ExpressRouteCircuitPeeringConfig, peeringType) = - { state with PeeringType = peeringType } + member _.PeeringType(state: ExpressRouteCircuitPeeringConfig, peeringType) = { + state with + PeeringType = peeringType + } [] member _.AzureASN(state: ExpressRouteCircuitPeeringConfig, azureAsn) = { state with AzureASN = azureAsn } @@ -62,50 +61,49 @@ type ExpressRouteCircuitPeeringBuilder() = member _.PeerASN(state: ExpressRouteCircuitPeeringConfig, peerAsn) = { state with PeerASN = peerAsn } [] - member _.PrimaryPeerAddressPrefix(state: ExpressRouteCircuitPeeringConfig, primaryPrefix) = - { state with + member _.PrimaryPeerAddressPrefix(state: ExpressRouteCircuitPeeringConfig, primaryPrefix) = { + state with PrimaryPeerAddressPrefix = primaryPrefix - } + } [] - member _.SecondaryPeerAddressPrefix(state: ExpressRouteCircuitPeeringConfig, secondaryPrefix) = - { state with + member _.SecondaryPeerAddressPrefix(state: ExpressRouteCircuitPeeringConfig, secondaryPrefix) = { + state with SecondaryPeerAddressPrefix = secondaryPrefix - } + } [] - member _.SharedKey(state: ExpressRouteCircuitPeeringConfig, sharedKey) = - { state with + member _.SharedKey(state: ExpressRouteCircuitPeeringConfig, sharedKey) = { + state with SharedKey = Some sharedKey - } + } [] member _.VlanId(state: ExpressRouteCircuitPeeringConfig, vlan) = { state with VlanId = vlan } let peering = ExpressRouteCircuitPeeringBuilder() -type ExpressRouteConfig = - { - /// The name of the express route circuit - Name: ResourceName - /// Tier of the circuit (standard or premium) - Tier: Tier - /// Unlimited or metered data - Family: Family - /// The service provider name for the circuit - ServiceProviderName: string - /// A valid peering location - PeeringLocation: string - /// Bandwidth in Mbps - Bandwidth: int - /// Indicates if global reach is enabled on this circuit - GlobalReachEnabled: bool - /// Peerings on this circuit - Peerings: ExpressRouteCircuitPeeringConfig list - /// Authorizations requested on this circuit - Authorizations: ResourceName list - Tags: Map - } +type ExpressRouteConfig = { + /// The name of the express route circuit + Name: ResourceName + /// Tier of the circuit (standard or premium) + Tier: Tier + /// Unlimited or metered data + Family: Family + /// The service provider name for the circuit + ServiceProviderName: string + /// A valid peering location + PeeringLocation: string + /// Bandwidth in Mbps + Bandwidth: int + /// Indicates if global reach is enabled on this circuit + GlobalReachEnabled: bool + /// Peerings on this circuit + Peerings: ExpressRouteCircuitPeeringConfig list + /// Authorizations requested on this circuit + Authorizations: ResourceName list + Tags: Map +} with /// Returns the service key on the newly created circuit. member this.ServiceKey = @@ -133,19 +131,18 @@ type ExpressRouteConfig = PeeringLocation = this.PeeringLocation Bandwidth = this.Bandwidth GlobalReachEnabled = this.GlobalReachEnabled - Peerings = - [ - for peering in this.Peerings do - {| - PeeringType = peering.PeeringType - AzureASN = peering.AzureASN - PeerASN = peering.PeerASN - PrimaryPeerAddressPrefix = peering.PrimaryPeerAddressPrefix - SecondaryPeerAddressPrefix = peering.SecondaryPeerAddressPrefix - SharedKey = peering.SharedKey - VlanId = peering.VlanId - |} - ] + Peerings = [ + for peering in this.Peerings do + {| + PeeringType = peering.PeeringType + AzureASN = peering.AzureASN + PeerASN = peering.PeerASN + PrimaryPeerAddressPrefix = peering.PrimaryPeerAddressPrefix + SecondaryPeerAddressPrefix = peering.SecondaryPeerAddressPrefix + SharedKey = peering.SharedKey + VlanId = peering.VlanId + |} + ] Tags = this.Tags } :: (this.Authorizations @@ -157,19 +154,18 @@ type ExpressRouteConfig = :> IArmResource)) type ExpressRouteBuilder() = - member _.Yield _ = - { - Name = ResourceName.Empty - Tier = Standard - Family = MeteredData - ServiceProviderName = "" - PeeringLocation = "" - Bandwidth = 50 - GlobalReachEnabled = false - Peerings = [] - Authorizations = [] - Tags = Map.empty - } + member _.Yield _ = { + Name = ResourceName.Empty + Tier = Standard + Family = MeteredData + ServiceProviderName = "" + PeeringLocation = "" + Bandwidth = 50 + GlobalReachEnabled = false + Peerings = [] + Authorizations = [] + Tags = Map.empty + } /// Sets the name of the circuit [] @@ -185,17 +181,17 @@ type ExpressRouteBuilder() = /// Sets the service provider for of the circuit. [] - member _.ServiceProviderName(state: ExpressRouteConfig, provider) = - { state with + member _.ServiceProviderName(state: ExpressRouteConfig, provider) = { + state with ServiceProviderName = provider - } + } /// Sets the peering location for this circuit. [] - member _.PeeringLocation(state: ExpressRouteConfig, location) = - { state with + member _.PeeringLocation(state: ExpressRouteConfig, location) = { + state with PeeringLocation = location - } + } /// Sets the bandwidth of the circuit (50 Mbps to 10 Gpbs). [] @@ -203,34 +199,33 @@ type ExpressRouteBuilder() = /// Adds a peering on the circuit. [] - member _.AddPeering(state: ExpressRouteConfig, peering) = - { state with + member _.AddPeering(state: ExpressRouteConfig, peering) = { + state with Peerings = peering :: state.Peerings - } + } /// Adds peerings on the circuit. [] - member _.AddPeerings(state: ExpressRouteConfig, peerings) = - { state with + member _.AddPeerings(state: ExpressRouteConfig, peerings) = { + state with Peerings = state.Peerings @ peerings - } + } /// Adds authorization names to request. [] - member _.AddAuthorizations(state: ExpressRouteConfig, authorizations: string list) = - { state with + member _.AddAuthorizations(state: ExpressRouteConfig, authorizations: string list) = { + state with Authorizations = state.Authorizations @ (authorizations |> List.map ResourceName) - } + } /// Enables Global Reach on the circuit [] - member _.EnableGlobalReach(state: ExpressRouteConfig) = - { state with GlobalReachEnabled = true } + member _.EnableGlobalReach(state: ExpressRouteConfig) = { state with GlobalReachEnabled = true } interface ITaggable with - member _.Add state tags = - { state with + member _.Add state tags = { + state with Tags = state.Tags |> Map.merge tags - } + } let expressRoute = ExpressRouteBuilder() diff --git a/src/Farmer/Builders/Builders.Functions.fs b/src/Farmer/Builders/Builders.Functions.fs index 4cf6e6ecb..f3eeae2a7 100644 --- a/src/Farmer/Builders/Builders.Functions.fs +++ b/src/Farmer/Builders/Builders.Functions.fs @@ -42,13 +42,12 @@ type FunctionsRuntime with static member Python37 = Python, Some "3.7" static member Python36 = Python, Some "3.6" -type DockerInfo = - { - User: string - Password: SecureParameter - Url: Uri - StartupCommand: string - } +type DockerInfo = { + User: string + Password: SecureParameter + Url: Uri + StartupCommand: string +} type PublishAs = | Code @@ -67,16 +66,15 @@ type FunctionsExtensionVersion = | V3 -> "~3" | V4 -> "~4" -type FunctionsConfig = - { - CommonWebConfig: CommonWebConfig - Tags: Map - Dependencies: ResourceId Set - StorageAccount: ResourceRef - VersionedRuntime: VersionedFunctionsRuntime - PublishAs: PublishAs - ExtensionVersion: FunctionsExtensionVersion - } +type FunctionsConfig = { + CommonWebConfig: CommonWebConfig + Tags: Map + Dependencies: ResourceId Set + StorageAccount: ResourceRef + VersionedRuntime: VersionedFunctionsRuntime + PublishAs: PublishAs + ExtensionVersion: FunctionsExtensionVersion +} with member this.Name = this.CommonWebConfig.Name member this.Runtime = fst this.VersionedRuntime @@ -136,339 +134,323 @@ type FunctionsConfig = interface IBuilder with member this.ResourceId = sites.resourceId this.Name.ResourceName - member this.BuildResources location = - [ - let keyVault, secrets = - match this.CommonWebConfig.SecretStore with - | KeyVault (DeployableResource (this.CommonWebConfig) vaultName) -> - let store = - keyVault { - name vaultName.Name - - add_access_policy ( - AccessPolicy.create (this.SystemIdentity.PrincipalId, [ KeyVault.Secret.Get ]) - ) - - add_secrets - [ - for setting in this.CommonWebConfig.Settings do - match setting.Value with - | LiteralSetting _ -> () - | ParameterSetting _ -> SecretConfig.create (setting.Key) - | ExpressionSetting expr -> SecretConfig.create (setting.Key, expr) - ] - } - - Some store, [] - | KeyVault (ExternalResource vaultName) -> - let secrets = - [ - for setting in this.CommonWebConfig.Settings do - let secret = - match setting.Value with - | LiteralSetting _ -> None - | ParameterSetting _ -> SecretConfig.create setting.Key |> Some - | ExpressionSetting expr -> SecretConfig.create (setting.Key, expr) |> Some - - match secret with - | Some secret -> - { - Secret.Name = vaultName.Name / secret.SecretName - Value = secret.Value - ContentType = secret.ContentType - Enabled = secret.Enabled - ActivationDate = secret.ActivationDate - ExpirationDate = secret.ExpirationDate - Location = location - Dependencies = secret.Dependencies.Add vaultName - Tags = secret.Tags - } - :> IArmResource - | None -> () - ] - - None, secrets - | KeyVault _ - | AppService -> None, [] - - yield! secrets - - match keyVault with - | Some keyVault -> - let builder = keyVault :> IBuilder - yield! builder.BuildResources location - | None -> () - - let functionsRuntime = - match this.Runtime with - | DotNetIsolated -> "dotnet-isolated" - | DotNet -> "dotnet" - | other -> (string other).ToLower() - - let basicSettings = - [ - "FUNCTIONS_WORKER_RUNTIME", functionsRuntime - "WEBSITE_NODE_DEFAULT_VERSION", "10.14.1" - "FUNCTIONS_EXTENSION_VERSION", this.ExtensionVersion.ArmValue - "AzureWebJobsStorage", - StorageAccount.getConnectionString this.StorageAccountId |> ArmExpression.Eval - "AzureWebJobsDashboard", - StorageAccount.getConnectionString this.StorageAccountId |> ArmExpression.Eval - - yield! - this.AppInsightsKey - |> Option.mapList (fun key -> "APPINSIGHTS_INSTRUMENTATIONKEY", key |> ArmExpression.Eval) - - if this.CommonWebConfig.OperatingSystem = Windows then - "WEBSITE_CONTENTAZUREFILECONNECTIONSTRING", - StorageAccount.getConnectionString this.StorageAccountId |> ArmExpression.Eval - - "WEBSITE_CONTENTSHARE", this.Name.ResourceName.Value.ToLower() - match this.PublishAs with - | DockerContainer { - User = us - Password = pass - Url = url - } -> - yield! - [ - "DOCKER_REGISTRY_SERVER_URL", url.ToString() - "DOCKER_REGISTRY_SERVER_USERNAME", us - "DOCKER_REGISTRY_SERVER_PASSWORD", pass.ArmExpression.Eval() - ] + member this.BuildResources location = [ + let keyVault, secrets = + match this.CommonWebConfig.SecretStore with + | KeyVault(DeployableResource (this.CommonWebConfig) vaultName) -> + let store = keyVault { + name vaultName.Name + + add_access_policy ( + AccessPolicy.create (this.SystemIdentity.PrincipalId, [ KeyVault.Secret.Get ]) + ) + + add_secrets [ + for setting in this.CommonWebConfig.Settings do + match setting.Value with + | LiteralSetting _ -> () + | ParameterSetting _ -> SecretConfig.create (setting.Key) + | ExpressionSetting expr -> SecretConfig.create (setting.Key, expr) + ] + } - | _ -> () + Some store, [] + | KeyVault(ExternalResource vaultName) -> + let secrets = [ + for setting in this.CommonWebConfig.Settings do + let secret = + match setting.Value with + | LiteralSetting _ -> None + | ParameterSetting _ -> SecretConfig.create setting.Key |> Some + | ExpressionSetting expr -> SecretConfig.create (setting.Key, expr) |> Some + + match secret with + | Some secret -> + { + Secret.Name = vaultName.Name / secret.SecretName + Value = secret.Value + ContentType = secret.ContentType + Enabled = secret.Enabled + ActivationDate = secret.ActivationDate + ExpirationDate = secret.ExpirationDate + Location = location + Dependencies = secret.Dependencies.Add vaultName + Tags = secret.Tags + } + :> IArmResource + | None -> () ] - let functionsSettings = - basicSettings - |> List.map Setting.AsLiteral - |> List.append ( - (match this.CommonWebConfig.SecretStore with - | AppService -> this.CommonWebConfig.Settings - | KeyVault r -> - let name = r.resourceId (this.CommonWebConfig) - - [ - for setting in this.CommonWebConfig.Settings do - match setting.Value with - | LiteralSetting _ -> setting.Key, setting.Value - | ParameterSetting _ - | ExpressionSetting _ -> - setting.Key, - LiteralSetting - $"@Microsoft.KeyVault(SecretUri=https://{name.Name.Value}.vault.azure.net/secrets/{setting.Key})" - ] - |> Map.ofList) - |> Map.toList - ) - |> Map - - let site = - { - SiteType = Site this.Name - ServicePlan = this.ServicePlanId - Location = location - Cors = this.CommonWebConfig.Cors - Tags = this.Tags - ConnectionStrings = Some this.CommonWebConfig.ConnectionStrings - AppSettings = Some functionsSettings - Identity = this.CommonWebConfig.Identity - KeyVaultReferenceIdentity = this.CommonWebConfig.KeyVaultReferenceIdentity - Kind = - match this.CommonWebConfig.OperatingSystem with - | Windows -> "functionapp" - | Linux -> "functionapp,linux" - Dependencies = - Set - [ - yield! this.Dependencies - - match this.CommonWebConfig.AppInsights with - | Some (DependableResource this.Name.ResourceName resourceId) -> resourceId - | _ -> () - - for setting in this.CommonWebConfig.Settings do - match setting.Value with - | ExpressionSetting e -> yield! Option.toList e.Owner - | ParameterSetting _ - | LiteralSetting _ -> () - - match this.CommonWebConfig.ServicePlan with - | DependableResource this.Name.ResourceName resourceId -> resourceId - | _ -> () - - match this.StorageAccount with - | DependableResource this resourceId -> resourceId - | _ -> () - - match this.CommonWebConfig.SecretStore with - | AppService -> - for setting in this.CommonWebConfig.Settings do - match setting.Value with - | ExpressionSetting expr -> yield! Option.toList expr.Owner - | ParameterSetting _ - | LiteralSetting _ -> () - | KeyVault _ -> () - ] - HTTPSOnly = this.CommonWebConfig.HTTPSOnly - FTPState = this.CommonWebConfig.FTPState - AlwaysOn = this.CommonWebConfig.AlwaysOn - HTTP20Enabled = None - ClientAffinityEnabled = None - WebSocketsEnabled = None - LinuxFxVersion = - match this.CommonWebConfig.OperatingSystem with - | Windows -> None - | Linux -> - match this.VersionedRuntime with - | DotNet, Some version -> - match Double.TryParse(version) with - | true, versionNo when versionNo < 4.0 -> Some $"DOTNETCORE|{version}" - | _ -> Some $"DOTNET|{version}" - | DotNetIsolated, Some version -> Some $"DOTNET-ISOLATED|{version}" - | _, Some version -> Some $"{functionsRuntime.ToUpper()}|{version}" - | _, None -> None - NetFrameworkVersion = None - JavaVersion = None - JavaContainer = None - JavaContainerVersion = None - PhpVersion = None - PythonVersion = None - Metadata = [] - AutoSwapSlotName = None - ZipDeployPath = - this.CommonWebConfig.ZipDeployPath - |> Option.map (fun (path, slot) -> path, ZipDeploy.ZipDeployTarget.FunctionApp, slot) - AppCommandLine = - match this.PublishAs with - | DockerContainer { StartupCommand = sc } -> Some sc - | _ -> None - WorkerProcess = this.CommonWebConfig.WorkerProcess - HealthCheckPath = this.CommonWebConfig.HealthCheckPath - IpSecurityRestrictions = this.CommonWebConfig.IpSecurityRestrictions - LinkToSubnet = this.CommonWebConfig.IntegratedSubnet - VirtualApplications = Map [] - } + None, secrets + | KeyVault _ + | AppService -> None, [] - match this.CommonWebConfig.ServicePlan with - | DeployableResource this.Name.ResourceName resourceId -> - { - Name = resourceId.Name - Location = location - Sku = this.CommonWebConfig.Sku - WorkerSize = Serverless - WorkerCount = 0 - MaximumElasticWorkerCount = None - OperatingSystem = this.CommonWebConfig.OperatingSystem - ZoneRedundant = None - Tags = this.Tags - } - | _ -> () + yield! secrets - match this.StorageAccount with - | DeployableResource this resourceId -> - { - Name = Storage.StorageAccountName.Create(resourceId.Name).OkValue - Location = location - Sku = Storage.Sku.Standard_LRS - Dependencies = [] - NetworkAcls = None - StaticWebsite = None - EnableHierarchicalNamespace = None - MinTlsVersion = None - Tags = this.Tags - DnsZoneType = None - DisablePublicNetworkAccess = None - DisableBlobPublicAccess = None - DisableSharedKeyAccess = None - DefaultToOAuthAuthentication = None - } - | _ -> () + match keyVault with + | Some keyVault -> + let builder = keyVault :> IBuilder + yield! builder.BuildResources location + | None -> () - match this.CommonWebConfig.AppInsights with - | Some (DeployableResource this.Name.ResourceName resourceId) -> - { - Name = resourceId.Name - Location = location - DisableIpMasking = false - SamplingPercentage = 100 - Dependencies = Set.empty - InstanceKind = Classic - LinkedWebsite = - match this.CommonWebConfig.OperatingSystem with - | Windows -> Some this.Name.ResourceName - | Linux -> None - Tags = this.Tags - } - | Some _ - | None -> () - - match this.CommonWebConfig.IntegratedSubnet with - | None -> () - | Some subnetRef -> - { - Site = site - Subnet = subnetRef.ResourceId - Dependencies = subnetRef.Dependency |> Option.toList - } + let functionsRuntime = + match this.Runtime with + | DotNetIsolated -> "dotnet-isolated" + | DotNet -> "dotnet" + | other -> (string other).ToLower() + + let basicSettings = [ + "FUNCTIONS_WORKER_RUNTIME", functionsRuntime + "WEBSITE_NODE_DEFAULT_VERSION", "10.14.1" + "FUNCTIONS_EXTENSION_VERSION", this.ExtensionVersion.ArmValue + "AzureWebJobsStorage", StorageAccount.getConnectionString this.StorageAccountId |> ArmExpression.Eval + "AzureWebJobsDashboard", StorageAccount.getConnectionString this.StorageAccountId |> ArmExpression.Eval yield! - (PrivateEndpoint.create location this.ResourceId [ "sites" ] this.CommonWebConfig.PrivateEndpoints) + this.AppInsightsKey + |> Option.mapList (fun key -> "APPINSIGHTS_INSTRUMENTATIONKEY", key |> ArmExpression.Eval) + + if this.CommonWebConfig.OperatingSystem = Windows then + "WEBSITE_CONTENTAZUREFILECONNECTIONSTRING", + StorageAccount.getConnectionString this.StorageAccountId |> ArmExpression.Eval + + "WEBSITE_CONTENTSHARE", this.Name.ResourceName.Value.ToLower() + match this.PublishAs with + | DockerContainer { + User = us + Password = pass + Url = url + } -> + yield! [ + "DOCKER_REGISTRY_SERVER_URL", url.ToString() + "DOCKER_REGISTRY_SERVER_USERNAME", us + "DOCKER_REGISTRY_SERVER_PASSWORD", pass.ArmExpression.Eval() + ] + + | _ -> () + ] + + let functionsSettings = + basicSettings + |> List.map Setting.AsLiteral + |> List.append ( + (match this.CommonWebConfig.SecretStore with + | AppService -> this.CommonWebConfig.Settings + | KeyVault r -> + let name = r.resourceId (this.CommonWebConfig) + + [ + for setting in this.CommonWebConfig.Settings do + match setting.Value with + | LiteralSetting _ -> setting.Key, setting.Value + | ParameterSetting _ + | ExpressionSetting _ -> + setting.Key, + LiteralSetting + $"@Microsoft.KeyVault(SecretUri=https://{name.Name.Value}.vault.azure.net/secrets/{setting.Key})" + ] + |> Map.ofList) + |> Map.toList + ) + |> Map + + let site = { + SiteType = Site this.Name + ServicePlan = this.ServicePlanId + Location = location + Cors = this.CommonWebConfig.Cors + Tags = this.Tags + ConnectionStrings = Some this.CommonWebConfig.ConnectionStrings + AppSettings = Some functionsSettings + Identity = this.CommonWebConfig.Identity + KeyVaultReferenceIdentity = this.CommonWebConfig.KeyVaultReferenceIdentity + Kind = + match this.CommonWebConfig.OperatingSystem with + | Windows -> "functionapp" + | Linux -> "functionapp,linux" + Dependencies = + Set [ + yield! this.Dependencies + + match this.CommonWebConfig.AppInsights with + | Some(DependableResource this.Name.ResourceName resourceId) -> resourceId + | _ -> () + + for setting in this.CommonWebConfig.Settings do + match setting.Value with + | ExpressionSetting e -> yield! Option.toList e.Owner + | ParameterSetting _ + | LiteralSetting _ -> () + + match this.CommonWebConfig.ServicePlan with + | DependableResource this.Name.ResourceName resourceId -> resourceId + | _ -> () + + match this.StorageAccount with + | DependableResource this resourceId -> resourceId + | _ -> () + + match this.CommonWebConfig.SecretStore with + | AppService -> + for setting in this.CommonWebConfig.Settings do + match setting.Value with + | ExpressionSetting expr -> yield! Option.toList expr.Owner + | ParameterSetting _ + | LiteralSetting _ -> () + | KeyVault _ -> () + ] + HTTPSOnly = this.CommonWebConfig.HTTPSOnly + FTPState = this.CommonWebConfig.FTPState + AlwaysOn = this.CommonWebConfig.AlwaysOn + HTTP20Enabled = None + ClientAffinityEnabled = None + WebSocketsEnabled = None + LinuxFxVersion = + match this.CommonWebConfig.OperatingSystem with + | Windows -> None + | Linux -> + match this.VersionedRuntime with + | DotNet, Some version -> + match Double.TryParse(version) with + | true, versionNo when versionNo < 4.0 -> Some $"DOTNETCORE|{version}" + | _ -> Some $"DOTNET|{version}" + | DotNetIsolated, Some version -> Some $"DOTNET-ISOLATED|{version}" + | _, Some version -> Some $"{functionsRuntime.ToUpper()}|{version}" + | _, None -> None + NetFrameworkVersion = None + JavaVersion = None + JavaContainer = None + JavaContainerVersion = None + PhpVersion = None + PythonVersion = None + Metadata = [] + AutoSwapSlotName = None + ZipDeployPath = + this.CommonWebConfig.ZipDeployPath + |> Option.map (fun (path, slot) -> path, ZipDeploy.ZipDeployTarget.FunctionApp, slot) + AppCommandLine = + match this.PublishAs with + | DockerContainer { StartupCommand = sc } -> Some sc + | _ -> None + WorkerProcess = this.CommonWebConfig.WorkerProcess + HealthCheckPath = this.CommonWebConfig.HealthCheckPath + IpSecurityRestrictions = this.CommonWebConfig.IpSecurityRestrictions + LinkToSubnet = this.CommonWebConfig.IntegratedSubnet + VirtualApplications = Map [] + } - if Map.isEmpty this.CommonWebConfig.Slots then - site - else - { site with + match this.CommonWebConfig.ServicePlan with + | DeployableResource this.Name.ResourceName resourceId -> { + Name = resourceId.Name + Location = location + Sku = this.CommonWebConfig.Sku + WorkerSize = Serverless + WorkerCount = 0 + MaximumElasticWorkerCount = None + OperatingSystem = this.CommonWebConfig.OperatingSystem + ZoneRedundant = None + Tags = this.Tags + } + | _ -> () + + match this.StorageAccount with + | DeployableResource this resourceId -> { + Name = Storage.StorageAccountName.Create(resourceId.Name).OkValue + Location = location + Sku = Storage.Sku.Standard_LRS + Dependencies = [] + NetworkAcls = None + StaticWebsite = None + EnableHierarchicalNamespace = None + MinTlsVersion = None + Tags = this.Tags + DnsZoneType = None + DisablePublicNetworkAccess = None + DisableBlobPublicAccess = None + DisableSharedKeyAccess = None + DefaultToOAuthAuthentication = None + } + | _ -> () + + match this.CommonWebConfig.AppInsights with + | Some(DeployableResource this.Name.ResourceName resourceId) -> { + Name = resourceId.Name + Location = location + DisableIpMasking = false + SamplingPercentage = 100 + Dependencies = Set.empty + InstanceKind = Classic + LinkedWebsite = + match this.CommonWebConfig.OperatingSystem with + | Windows -> Some this.Name.ResourceName + | Linux -> None + Tags = this.Tags + } + | 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 + else + { + site with AppSettings = None ConnectionStrings = None - } + } - for (_, slot) in this.CommonWebConfig.Slots |> Map.toSeq do - slot.ToSite site - ] + for (_, slot) in this.CommonWebConfig.Slots |> Map.toSeq do + slot.ToSite site + ] type FunctionsBuilder() = - member _.Yield _ = - { - FunctionsConfig.CommonWebConfig = - { - Name = WebAppName.Empty - AlwaysOn = false - AppInsights = Some(derived (fun name -> components.resourceId (name - "ai"))) - ConnectionStrings = Map.empty - Cors = None - FTPState = None - HTTPSOnly = false - Identity = ManagedIdentity.Empty - KeyVaultReferenceIdentity = None - OperatingSystem = Windows - 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 - IpSecurityRestrictions = [] - IntegratedSubnet = None - PrivateEndpoints = Set.empty - } - StorageAccount = - derived (fun config -> - let storage = - config.Name.ResourceName.Map(sprintf "%sstorage") - |> sanitiseStorage - |> ResourceName - - storageAccounts.resourceId storage) - VersionedRuntime = FunctionsRuntime.DotNetCore31 - ExtensionVersion = V3 - Dependencies = Set.empty - PublishAs = Code - Tags = Map.empty + member _.Yield _ = { + FunctionsConfig.CommonWebConfig = { + Name = WebAppName.Empty + AlwaysOn = false + AppInsights = Some(derived (fun name -> components.resourceId (name - "ai"))) + ConnectionStrings = Map.empty + Cors = None + FTPState = None + HTTPSOnly = false + Identity = ManagedIdentity.Empty + KeyVaultReferenceIdentity = None + OperatingSystem = Windows + 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 + IpSecurityRestrictions = [] + IntegratedSubnet = None + PrivateEndpoints = Set.empty } + StorageAccount = + derived (fun config -> + let storage = + config.Name.ResourceName.Map(sprintf "%sstorage") + |> sanitiseStorage + |> ResourceName + + storageAccounts.resourceId storage) + VersionedRuntime = FunctionsRuntime.DotNetCore31 + ExtensionVersion = V3 + Dependencies = Set.empty + PublishAs = Code + Tags = Map.empty + } member _.Run(state: FunctionsConfig) = if state.Name.ResourceName = ResourceName.Empty then @@ -479,60 +461,60 @@ type FunctionsBuilder() = /// Do not create an automatic storage account; instead, link to a storage account that is created outside of this Functions instance. [] - member _.LinkToStorageAccount(state: FunctionsConfig, name) = - { state with + member _.LinkToStorageAccount(state: FunctionsConfig, name) = { + state with StorageAccount = managed storageAccounts name - } + } member this.LinkToStorageAccount(state: FunctionsConfig, name) = this.LinkToStorageAccount(state, ResourceName name) [] - member _.LinkToUnmanagedStorageAccount(state: FunctionsConfig, resourceId) = - { state with + member _.LinkToUnmanagedStorageAccount(state: FunctionsConfig, resourceId) = { + state with StorageAccount = unmanaged resourceId - } + } /// Set the name of the storage account instead of using an auto-generated one based on the function instance name. [] - member _.StorageAccountName(state: FunctionsConfig, name) = - { state with + member _.StorageAccountName(state: FunctionsConfig, name) = { + state with StorageAccount = named storageAccounts (ResourceName name) - } + } /// Sets the runtime of the Functions host. [] - member _.Runtime(state: FunctionsConfig, runtime) = - { state with + member _.Runtime(state: FunctionsConfig, runtime) = { + state with VersionedRuntime = runtime, None - } + } - member _.Runtime(state: FunctionsConfig, runtime) = - { state with + member _.Runtime(state: FunctionsConfig, runtime) = { + state with VersionedRuntime = runtime - } + } /// Sets the Publish as Code or Docker container information. [] member _.PublishAs(state: FunctionsConfig, publishAs) = { state with PublishAs = publishAs } [] - member _.ExtensionVersion(state: FunctionsConfig, version) = - { state with + member _.ExtensionVersion(state: FunctionsConfig, version) = { + state with ExtensionVersion = version - } + } interface ITaggable with - member _.Add state tags = - { state with + member _.Add state tags = { + state with Tags = state.Tags |> Map.merge tags - } + } interface IDependable with - member _.Add state newDeps = - { state with + member _.Add state newDeps = { + state with Dependencies = state.Dependencies + newDeps - } + } interface IServicePlanApp with member _.Get state = state.CommonWebConfig @@ -540,10 +522,9 @@ type FunctionsBuilder() = let functions = FunctionsBuilder() -let docker (server: Uri) (user: string) (command: string) : DockerInfo = - { - User = user - Password = SecureParameter $"{user}-password" - Url = server - StartupCommand = command - } +let docker (server: Uri) (user: string) (command: string) : DockerInfo = { + User = user + Password = SecureParameter $"{user}-password" + Url = server + StartupCommand = command +} diff --git a/src/Farmer/Builders/Builders.Gallery.fs b/src/Farmer/Builders/Builders.Gallery.fs index 5f3c3fa19..95318ee17 100644 --- a/src/Farmer/Builders/Builders.Gallery.fs +++ b/src/Farmer/Builders/Builders.Gallery.fs @@ -7,150 +7,144 @@ open Farmer.Image open Farmer.GalleryValidation open Farmer.Arm.Gallery -type GalleryConfig = - { - Name: GalleryName - Description: string option - SharingProfile: SharingProfile option - SoftDelete: FeatureFlag option - Tags: Map - Dependencies: Set - } +type GalleryConfig = { + Name: GalleryName + Description: string option + SharingProfile: SharingProfile option + SoftDelete: FeatureFlag option + Tags: Map + Dependencies: Set +} with interface IBuilder with member this.ResourceId = galleries.resourceId this.Name.ResourceName - member this.BuildResources location = - [ - { - Name = this.Name - Location = location - Description = this.Description - SharingProfile = this.SharingProfile - SoftDeletePolicy = - this.SoftDelete - |> Option.map (fun flag -> { IsSoftDeleteEnabled = flag.AsBoolean }) - Tags = this.Tags - Dependencies = this.Dependencies - } - ] + member this.BuildResources location = [ + { + Name = this.Name + Location = location + Description = this.Description + SharingProfile = this.SharingProfile + SoftDeletePolicy = + this.SoftDelete + |> Option.map (fun flag -> { IsSoftDeleteEnabled = flag.AsBoolean }) + Tags = this.Tags + Dependencies = this.Dependencies + } + ] type GalleryBuilder() = - member _.Yield _ = - { - Name = GalleryName.Empty - Description = None - SharingProfile = None - SoftDelete = None - Tags = Map.empty - Dependencies = Set.empty - } + member _.Yield _ = { + Name = GalleryName.Empty + Description = None + SharingProfile = None + SoftDelete = None + Tags = Map.empty + Dependencies = Set.empty + } [] - member _.Name(config: GalleryConfig, name: string) = - { config with + member _.Name(config: GalleryConfig, name: string) = { + config with Name = GalleryName.Create(name).OkValue - } + } [] - member _.Description(config: GalleryConfig, description) = - { config with + member _.Description(config: GalleryConfig, description) = { + config with Description = Some description - } + } [] - member _.SharingProfile(config: GalleryConfig, sharingProfile) = - { config with + member _.SharingProfile(config: GalleryConfig, sharingProfile) = { + config with SharingProfile = Some sharingProfile - } + } [] member _.SoftDelete(config: GalleryConfig, flag: FeatureFlag) = { config with SoftDelete = Some flag } interface ITaggable with - member _.Add state tags = - { state with + member _.Add state tags = { + state with Tags = state.Tags |> Map.merge tags - } + } interface IDependable with - member _.Add state resources = - { state with + member _.Add state resources = { + state with Dependencies = state.Dependencies + resources - } + } let gallery = GalleryBuilder() -type GalleryImageConfig = - { - Name: ResourceName - GalleryName: GalleryName - Architecture: Architecture option - Description: string option - Eula: string option - HyperVGeneration: HyperVGeneration option - Identifier: GalleryImageIdentifier option - OsState: OsState option - OsType: OS option - PrivacyStatementUri: Uri option - PurchasePlan: ImagePurchasePlan option - Recommended: RecommendedMachineConfiguration option - ReleaseNoteUri: Uri option - Tags: Map - Dependencies: Set - } +type GalleryImageConfig = { + Name: ResourceName + GalleryName: GalleryName + Architecture: Architecture option + Description: string option + Eula: string option + HyperVGeneration: HyperVGeneration option + Identifier: GalleryImageIdentifier option + OsState: OsState option + OsType: OS option + PrivacyStatementUri: Uri option + PurchasePlan: ImagePurchasePlan option + Recommended: RecommendedMachineConfiguration option + ReleaseNoteUri: Uri option + Tags: Map + Dependencies: Set +} with interface IBuilder with member this.ResourceId = galleryImages.resourceId (this.GalleryName.ResourceName, this.Name) - member this.BuildResources location = - [ - { - Name = this.Name - GalleryName = this.GalleryName - Location = location - Architecture = this.Architecture - Description = this.Description |> Option.toObj - Eula = this.Eula - HyperVGeneration = - this.HyperVGeneration - |> Option.defaultWith (fun _ -> raiseFarmer "Gallery image 'hyperv_generation' is required.") - Identifier = - this.Identifier - |> Option.defaultWith (fun _ -> raiseFarmer "Gallery image 'identifier' is required.") - OsState = this.OsState |> Option.defaultValue Generalized - OsType = - this.OsType - |> Option.defaultWith (fun _ -> raiseFarmer "Gallery image 'os_type' is required.") - PrivacyStatementUri = this.PrivacyStatementUri - PurchasePlan = this.PurchasePlan - Recommended = this.Recommended |> Option.defaultValue RecommendedMachineConfiguration.Default - ReleaseNoteUri = this.ReleaseNoteUri - Tags = this.Tags - Dependencies = this.Dependencies - } - ] + member this.BuildResources location = [ + { + Name = this.Name + GalleryName = this.GalleryName + Location = location + Architecture = this.Architecture + Description = this.Description |> Option.toObj + Eula = this.Eula + HyperVGeneration = + this.HyperVGeneration + |> Option.defaultWith (fun _ -> raiseFarmer "Gallery image 'hyperv_generation' is required.") + Identifier = + this.Identifier + |> Option.defaultWith (fun _ -> raiseFarmer "Gallery image 'identifier' is required.") + OsState = this.OsState |> Option.defaultValue Generalized + OsType = + this.OsType + |> Option.defaultWith (fun _ -> raiseFarmer "Gallery image 'os_type' is required.") + PrivacyStatementUri = this.PrivacyStatementUri + PurchasePlan = this.PurchasePlan + Recommended = this.Recommended |> Option.defaultValue RecommendedMachineConfiguration.Default + ReleaseNoteUri = this.ReleaseNoteUri + Tags = this.Tags + Dependencies = this.Dependencies + } + ] type GalleryImageBuilder() = - member _.Yield _ = - { - Name = ResourceName.Empty - GalleryName = GalleryName.Empty - Architecture = None - Description = None - Eula = None - HyperVGeneration = None - Identifier = None - OsState = None - OsType = None - PrivacyStatementUri = None - PurchasePlan = None - Recommended = None - ReleaseNoteUri = None - Tags = Map.empty - Dependencies = Set.empty - } + member _.Yield _ = { + Name = ResourceName.Empty + GalleryName = GalleryName.Empty + Architecture = None + Description = None + Eula = None + HyperVGeneration = None + Identifier = None + OsState = None + OsType = None + PrivacyStatementUri = None + PurchasePlan = None + Recommended = None + ReleaseNoteUri = None + Tags = Map.empty + Dependencies = Set.empty + } member _.Run(config: GalleryImageConfig) = if config.Name = ResourceName.Empty then @@ -171,48 +165,47 @@ type GalleryImageBuilder() = config [] - member _.Name(config: GalleryImageConfig, name) = - { config with Name = ResourceName name } + member _.Name(config: GalleryImageConfig, name) = { config with Name = ResourceName name } [] - member _.GalleryName(config: GalleryImageConfig, galleryName) = - { config with + member _.GalleryName(config: GalleryImageConfig, galleryName) = { + config with GalleryName = GalleryName.Create(galleryName).OkValue - } + } [] - member _.Gallery(config: GalleryImageConfig, galleryConfig: GalleryConfig) = - { config with + member _.Gallery(config: GalleryImageConfig, galleryConfig: GalleryConfig) = { + config with GalleryName = galleryConfig.Name Dependencies = config.Dependencies |> Set.add (galleryConfig :> IBuilder).ResourceId - } + } [] - member _.Architecture(config: GalleryImageConfig, architecture) = - { config with + member _.Architecture(config: GalleryImageConfig, architecture) = { + config with Architecture = Some architecture - } + } [] - member _.Description(config: GalleryImageConfig, description) = - { config with + member _.Description(config: GalleryImageConfig, description) = { + config with Description = Some description - } + } [] member _.Eula(config: GalleryImageConfig, eula) = { config with Eula = Some eula } [] - member _.HyperVGeneration(config: GalleryImageConfig, hyperVGeneration) = - { config with + member _.HyperVGeneration(config: GalleryImageConfig, hyperVGeneration) = { + config with HyperVGeneration = Some hyperVGeneration - } + } [] - member _.Identifier(config: GalleryImageConfig, identifier) = - { config with + member _.Identifier(config: GalleryImageConfig, identifier) = { + config with Identifier = Some identifier - } + } [] member _.OsState(config: GalleryImageConfig, osState) = { config with OsState = Some osState } @@ -221,25 +214,25 @@ type GalleryImageBuilder() = member _.OsType(config: GalleryImageConfig, osType) = { config with OsType = Some osType } [] - member _.PrivacyStatementUri(config: GalleryImageConfig, privacyStatementUri: Uri) = - { config with + member _.PrivacyStatementUri(config: GalleryImageConfig, privacyStatementUri: Uri) = { + config with PrivacyStatementUri = Some privacyStatementUri - } + } member this.PrivacyStatementUri(config, privacyStatementUri: string) = this.PrivacyStatementUri(config, Uri privacyStatementUri) [] - member _.PurchasePlan(config: GalleryImageConfig, purchasePlan) = - { config with + member _.PurchasePlan(config: GalleryImageConfig, purchasePlan) = { + config with PurchasePlan = Some purchasePlan - } + } [] - member _.RecommendedConfiguration(config: GalleryImageConfig, recommended) = - { config with + member _.RecommendedConfiguration(config: GalleryImageConfig, recommended) = { + config with Recommended = Some recommended - } + } [] member _.RecommendedMemory(config: GalleryImageConfig, min, max) = @@ -247,12 +240,13 @@ type GalleryImageBuilder() = config.Recommended |> Option.defaultValue RecommendedMachineConfiguration.Default - { config with - Recommended = - Some - { existing with - MemoryMin = min - MemoryMax = max + { + config with + Recommended = + Some { + existing with + MemoryMin = min + MemoryMax = max } } @@ -262,34 +256,35 @@ type GalleryImageBuilder() = config.Recommended |> Option.defaultValue RecommendedMachineConfiguration.Default - { config with - Recommended = - Some - { existing with - VCpuMin = min - VCpuMax = max + { + config with + Recommended = + Some { + existing with + VCpuMin = min + VCpuMax = max } } [] - member _.ReleaseNoteUri(config: GalleryImageConfig, releaseNoteUri) = - { config with + member _.ReleaseNoteUri(config: GalleryImageConfig, releaseNoteUri) = { + config with ReleaseNoteUri = Some releaseNoteUri - } + } member this.ReleaseNoteUri(config, releaseNoteUri: string) = this.ReleaseNoteUri(config, Uri releaseNoteUri) interface ITaggable with - member _.Add state tags = - { state with + member _.Add state tags = { + state with Tags = state.Tags |> Map.merge tags - } + } interface IDependable with - member _.Add state resources = - { state with + member _.Add state resources = { + state with Dependencies = state.Dependencies + resources - } + } let galleryImage = GalleryImageBuilder() diff --git a/src/Farmer/Builders/Builders.HostGroup.fs b/src/Farmer/Builders/Builders.HostGroup.fs index e62b254c9..841d977b2 100644 --- a/src/Farmer/Builders/Builders.HostGroup.fs +++ b/src/Farmer/Builders/Builders.HostGroup.fs @@ -6,96 +6,91 @@ open Farmer open Farmer.Arm open Farmer.DedicatedHosts -type HostGroupConfig = - { - Name: ResourceName - AvailabilityZone: string option - SupportAutomaticPlacement: FeatureFlag option - PlatformFaultDomainCount: PlatformFaultDomainCount option - DependsOn: Set - Tags: Map - } +type HostGroupConfig = { + Name: ResourceName + AvailabilityZone: string option + SupportAutomaticPlacement: FeatureFlag option + PlatformFaultDomainCount: PlatformFaultDomainCount option + DependsOn: Set + Tags: Map +} with interface IBuilder with member this.ResourceId = hostGroups.resourceId this.Name member this.BuildResources location = - let hostGroup: HostGroup = - { - Name = this.Name - Location = location - AvailabilityZone = - this.AvailabilityZone - |> Option.map (fun zone -> [ zone ]) - |> Option.defaultValue [] - SupportAutomaticPlacement = - this.SupportAutomaticPlacement |> Option.defaultValue FeatureFlag.Disabled - PlatformFaultDomainCount = - this.PlatformFaultDomainCount - |> Option.defaultValue (PlatformFaultDomainCount 1) - DependsOn = this.DependsOn - Tags = this.Tags - } + let hostGroup: HostGroup = { + Name = this.Name + Location = location + AvailabilityZone = + this.AvailabilityZone + |> Option.map (fun zone -> [ zone ]) + |> Option.defaultValue [] + SupportAutomaticPlacement = this.SupportAutomaticPlacement |> Option.defaultValue FeatureFlag.Disabled + PlatformFaultDomainCount = + this.PlatformFaultDomainCount + |> Option.defaultValue (PlatformFaultDomainCount 1) + DependsOn = this.DependsOn + Tags = this.Tags + } [ hostGroup ] type HostGroupBuilder() = - member _.Yield _ = - { - Name = ResourceName.Empty - AvailabilityZone = None - SupportAutomaticPlacement = None - PlatformFaultDomainCount = None - DependsOn = Set.empty - Tags = Map.empty - } + member _.Yield _ = { + Name = ResourceName.Empty + AvailabilityZone = None + SupportAutomaticPlacement = None + PlatformFaultDomainCount = None + DependsOn = Set.empty + Tags = Map.empty + } [] member _.Name(state: HostGroupConfig, name: string) = { state with Name = ResourceName name } [] - member _.AddAvailabilityZone(state: HostGroupConfig, az: string) = - { state with + member _.AddAvailabilityZone(state: HostGroupConfig, az: string) = { + state with AvailabilityZone = Some az - } + } [] - member _.SupportAutomaticPlacement(state: HostGroupConfig, flag: FeatureFlag) = - { state with + member _.SupportAutomaticPlacement(state: HostGroupConfig, flag: FeatureFlag) = { + state with SupportAutomaticPlacement = Some flag - } + } [] - member _.SupportAutomaticPlacement(state: HostGroupConfig, flag: bool) = - { state with + member _.SupportAutomaticPlacement(state: HostGroupConfig, flag: bool) = { + state with SupportAutomaticPlacement = Some(FeatureFlag.ofBool flag) - } + } [] - member _.PlatformFaultDomainCount(state: HostGroupConfig, domainCount: int) = - { state with + member _.PlatformFaultDomainCount(state: HostGroupConfig, domainCount: int) = { + state with PlatformFaultDomainCount = Some(PlatformFaultDomainCount.Parse domainCount) - } + } interface IDependable with - member _.Add state resIds = - { state with + member _.Add state resIds = { + state with DependsOn = state.DependsOn + resIds - } + } let hostGroup = HostGroupBuilder() -type HostConfig = - { - Name: ResourceName - AutoReplaceOnFailure: FeatureFlag option - LicenseType: HostLicenseType option - HostSku: HostSku option - PlatformFaultDomain: PlatformFaultDomainCount option - HostGroupName: LinkedResource - DependsOn: Set - Tags: Map - } +type HostConfig = { + Name: ResourceName + AutoReplaceOnFailure: FeatureFlag option + LicenseType: HostLicenseType option + HostSku: HostSku option + PlatformFaultDomain: PlatformFaultDomainCount option + HostGroupName: LinkedResource + DependsOn: Set + Tags: Map +} with interface IBuilder with member this.ResourceId = hosts.resourceId this.Name @@ -105,79 +100,75 @@ type HostConfig = | "", _ -> raiseFarmer "Hosts must have a linked host group" | _, None -> raiseFarmer "Hosts must have a sku" | _, Some sku -> - let host: Compute.Host = - { - Name = this.Name - Location = location - Sku = sku - ParentHostGroupName = this.HostGroupName.ResourceId.Name - AutoReplaceOnFailure = FeatureFlag.Enabled - LicenseType = HostLicenseType.NoLicense - DependsOn = this.DependsOn - PlatformFaultDomain = - this.PlatformFaultDomain |> Option.defaultValue (PlatformFaultDomainCount 1) - Tags = this.Tags - } + let host: Compute.Host = { + Name = this.Name + Location = location + Sku = sku + ParentHostGroupName = this.HostGroupName.ResourceId.Name + AutoReplaceOnFailure = FeatureFlag.Enabled + LicenseType = HostLicenseType.NoLicense + DependsOn = this.DependsOn + PlatformFaultDomain = this.PlatformFaultDomain |> Option.defaultValue (PlatformFaultDomainCount 1) + Tags = this.Tags + } [ host ] type HostBuilder() = - member _.Yield _ = - { - Name = ResourceName.Empty - AutoReplaceOnFailure = None - LicenseType = None - HostSku = None - DependsOn = Set.empty - PlatformFaultDomain = None - HostGroupName = Managed(ResourceId.create (hostGroups, ResourceName.Empty)) - Tags = Map.empty - } + member _.Yield _ = { + Name = ResourceName.Empty + AutoReplaceOnFailure = None + LicenseType = None + HostSku = None + DependsOn = Set.empty + PlatformFaultDomain = None + HostGroupName = Managed(ResourceId.create (hostGroups, ResourceName.Empty)) + Tags = Map.empty + } [] member _.Name(state: HostConfig, name: string) = { state with Name = ResourceName name } [] - member _.AutoReplaceOnFailure(state: HostConfig, flag: FeatureFlag) = - { state with + member _.AutoReplaceOnFailure(state: HostConfig, flag: FeatureFlag) = { + state with AutoReplaceOnFailure = Some flag - } + } [] - member _.LicenseType(state: HostConfig, licenseType: HostLicenseType) = - { state with + member _.LicenseType(state: HostConfig, licenseType: HostLicenseType) = { + state with LicenseType = Some licenseType - } + } [] member _.Sku(state: HostConfig, s: HostSku) = { state with HostSku = Some s } [] - member _.Sku(state: HostConfig, skuName: string) = - { state with + member _.Sku(state: HostConfig, skuName: string) = { + state with HostSku = Some(HostSku skuName) - } + } [] - member _.PlatformFaultDomain(state: HostConfig, faultDomains: int) = - { state with + member _.PlatformFaultDomain(state: HostConfig, faultDomains: int) = { + state with PlatformFaultDomain = Some(faultDomains |> PlatformFaultDomainCount.Parse) - } + } [] - member _.ParentHostGroup(state: HostConfig, hostGroup: LinkedResource) = - { state with HostGroupName = hostGroup } + member _.ParentHostGroup(state: HostConfig, hostGroup: LinkedResource) = { state with HostGroupName = hostGroup } [] - member _.ParentHostGroup(state: HostConfig, hostGroupName: ResourceName) = - { state with + member _.ParentHostGroup(state: HostConfig, hostGroupName: ResourceName) = { + state with HostGroupName = Unmanaged(ResourceId.create (hostGroups, hostGroupName)) - } + } interface IDependable with - member _.Add state resIds = - { state with + member _.Add state resIds = { + state with DependsOn = state.DependsOn + resIds - } + } let host = HostBuilder() diff --git a/src/Farmer/Builders/Builders.ImageTemplate.fs b/src/Farmer/Builders/Builders.ImageTemplate.fs index 603506acd..d5cae3614 100644 --- a/src/Farmer/Builders/Builders.ImageTemplate.fs +++ b/src/Farmer/Builders/Builders.ImageTemplate.fs @@ -6,139 +6,132 @@ open Farmer.Arm.ImageTemplate open Farmer.Arm.Gallery open Farmer.Identity -type ImageTemplateConfig = - { - Name: ResourceName - Identity: Identity.ManagedIdentity - BuildTimeoutInMinutes: int option - Source: ImageBuilderSource option - Customize: Customizer list - Distribute: Distibutor list - Dependencies: Set - Tags: Map - } +type ImageTemplateConfig = { + Name: ResourceName + Identity: Identity.ManagedIdentity + BuildTimeoutInMinutes: int option + Source: ImageBuilderSource option + Customize: Customizer list + Distribute: Distibutor list + Dependencies: Set + Tags: Map +} with interface IBuilder with member this.ResourceId = imageTemplates.resourceId this.Name - member this.BuildResources location = - [ - { - Name = this.Name - Location = location - Identity = this.Identity - BuildTimeoutInMinutes = this.BuildTimeoutInMinutes - Source = - match this.Source with - | Some source -> source - | None -> raiseFarmer "Image template requires a 'source'" - Customize = this.Customize - Distribute = this.Distribute - Dependencies = this.Dependencies - Tags = this.Tags - } - ] + member this.BuildResources location = [ + { + Name = this.Name + Location = location + Identity = this.Identity + BuildTimeoutInMinutes = this.BuildTimeoutInMinutes + Source = + match this.Source with + | Some source -> source + | None -> raiseFarmer "Image template requires a 'source'" + Customize = this.Customize + Distribute = this.Distribute + Dependencies = this.Dependencies + Tags = this.Tags + } + ] type ImageTemplateBuilder() = - member _.Yield _ = - { - Name = ResourceName.Empty - Identity = ManagedIdentity.Empty - BuildTimeoutInMinutes = None - Source = None - Customize = [] - Distribute = [] - Dependencies = Set.empty - Tags = Map.empty - } + member _.Yield _ = { + Name = ResourceName.Empty + Identity = ManagedIdentity.Empty + BuildTimeoutInMinutes = None + Source = None + Customize = [] + Distribute = [] + Dependencies = Set.empty + Tags = Map.empty + } [] - member _.Name(config: ImageTemplateConfig, name: string) = - { config with Name = ResourceName name } + member _.Name(config: ImageTemplateConfig, name: string) = { config with Name = ResourceName name } [] - member _.BuildTimeout(config: ImageTemplateConfig, timeoutMinutes: int) = - { config with + member _.BuildTimeout(config: ImageTemplateConfig, timeoutMinutes: int) = { + config with BuildTimeoutInMinutes = Some(timeoutMinutes / 1) - } + } member this.BuildTimeout(config, timeout: System.TimeSpan) = this.BuildTimeout(config, int timeout.TotalMinutes * 1) [] - member _.PlatformImageSource(config: ImageTemplateConfig, imageSource: PlatformImageSource) = - { config with + member _.PlatformImageSource(config: ImageTemplateConfig, imageSource: PlatformImageSource) = { + config with Source = Some(ImageBuilderSource.Platform imageSource) - } + } member this.PlatformImageSource(config: ImageTemplateConfig, image: Vm.ImageDefinition) = - let imageSource = - { - ImageIdentifier = - { - Publisher = image.Publisher.ArmValue - Offer = image.Offer.ArmValue - Sku = image.Sku.ArmValue - } - PlanInfo = None - Version = "latest" + let imageSource = { + ImageIdentifier = { + Publisher = image.Publisher.ArmValue + Offer = image.Offer.ArmValue + Sku = image.Sku.ArmValue } + PlanInfo = None + Version = "latest" + } this.PlatformImageSource(config, imageSource) [] - member _.ManagedImageSource(config: ImageTemplateConfig, imageId: ResourceId) = - { config with + member _.ManagedImageSource(config: ImageTemplateConfig, imageId: ResourceId) = { + config with Source = Some(ImageBuilderSource.Managed { ImageId = imageId }) - } + } [] - member _.SharedImageVersionSource(config: ImageTemplateConfig, imageId: ResourceId) = - { config with + member _.SharedImageVersionSource(config: ImageTemplateConfig, imageId: ResourceId) = { + config with Source = Some(ImageBuilderSource.SharedVersion { ImageId = imageId }) - } + } [] - member _.AddCustomizer(config: ImageTemplateConfig, customizers) = - { config with + member _.AddCustomizer(config: ImageTemplateConfig, customizers) = { + config with Customize = config.Customize @ customizers - } + } [] - member _.AddCustomizer(config: ImageTemplateConfig, distributors) = - { config with + member _.AddCustomizer(config: ImageTemplateConfig, distributors) = { + config with Distribute = config.Distribute @ distributors - } + } interface IIdentity with - member _.Add state updater = - { state with + member _.Add state updater = { + state with Identity = updater state.Identity - } + } interface ITaggable with - member _.Add state tags = - { state with + member _.Add state tags = { + state with Tags = state.Tags |> Map.merge tags - } + } interface IDependable with - member _.Add state resources = - { state with + member _.Add state resources = { + state with Dependencies = state.Dependencies + resources - } + } let imageTemplate = ImageTemplateBuilder() type FileCustomizerBuilder() = - member _.Yield _ = - { - Name = null - SourceUri = null - Destination = null - Sha256Checksum = None - } + member _.Yield _ = { + Name = null + SourceUri = null + Destination = null + Sha256Checksum = None + } member _.Run(customizer: FileCustomizer) = if isNull customizer.SourceUri then @@ -159,16 +152,16 @@ type FileCustomizerBuilder() = this.SourceUri(customizer, System.Uri uri) [] - member _.Destination(customizer: FileCustomizer, destination) = - { customizer with + member _.Destination(customizer: FileCustomizer, destination) = { + customizer with Destination = destination - } + } [] - member _.Checksum(customizer: FileCustomizer, checksum) = - { customizer with + member _.Checksum(customizer: FileCustomizer, checksum) = { + customizer with Sha256Checksum = Some checksum - } + } /// File customizer for downloading small files to the image, < 20MB. For larger files, use a shell or inline command /// in a shell or powershell customizer. @@ -183,21 +176,20 @@ type ShellCustomizerBuilder() = member _.Name(customizer: ShellCustomizer, name) = { customizer with Name = name } [] - member _.InlineStatements(customizer: ShellCustomizer, inlineStatements) = - { customizer with + member _.InlineStatements(customizer: ShellCustomizer, inlineStatements) = { + customizer with Inline = inlineStatements - } + } /// Shell customizer for running shell commands defined as inline strings. let shellCustomizer = ShellCustomizerBuilder() type ShellScriptCustomizerBuilder() = - member _.Yield _ = - { - Name = null - ScriptUri = null - Sha256Checksum = None - } + member _.Yield _ = { + Name = null + ScriptUri = null + Sha256Checksum = None + } member _.Run(customizer: ShellScriptCustomizer) = if isNull customizer.ScriptUri then @@ -215,23 +207,22 @@ type ShellScriptCustomizerBuilder() = this.ScriptUri(customizer, System.Uri uri) [] - member _.Checksum(customizer: ShellScriptCustomizer, checksum) = - { customizer with + member _.Checksum(customizer: ShellScriptCustomizer, checksum) = { + customizer with Sha256Checksum = Some checksum - } + } /// Shell script customizer to download a shell script to execute. let shellScriptCustomizer = ShellScriptCustomizerBuilder() type PowerShellCustomizerBuilder() = - member _.Yield _ = - { - Name = null - Inline = [] - RunAsElevated = false - RunAsSystem = false - ValidExitCodes = [] - } + member _.Yield _ = { + Name = null + Inline = [] + RunAsElevated = false + RunAsSystem = false + ValidExitCodes = [] + } member _.Run(customizer: PowerShellCustomizer) = Customizer.PowerShell customizer @@ -239,43 +230,42 @@ type PowerShellCustomizerBuilder() = member _.Name(customizer: PowerShellCustomizer, name) = { customizer with Name = name } [] - member _.InlineShell(customizer: PowerShellCustomizer, inlineStatements) = - { customizer with + member _.InlineShell(customizer: PowerShellCustomizer, inlineStatements) = { + customizer with Inline = inlineStatements - } + } [] - member _.RunAsElevated(customizer: PowerShellCustomizer, runAsElevated) = - { customizer with + member _.RunAsElevated(customizer: PowerShellCustomizer, runAsElevated) = { + customizer with RunAsElevated = runAsElevated - } + } [] - member _.RunAsSystem(customizer: PowerShellCustomizer, runAsSystem) = - { customizer with + member _.RunAsSystem(customizer: PowerShellCustomizer, runAsSystem) = { + customizer with RunAsSystem = runAsSystem - } + } [] - member _.ValidExitCodes(customizer: PowerShellCustomizer, validExitCodes) = - { customizer with + member _.ValidExitCodes(customizer: PowerShellCustomizer, validExitCodes) = { + customizer with ValidExitCodes = customizer.ValidExitCodes @ validExitCodes - } + } /// PowerShell customizer for running PowerShell commands on Windows images defined as inline strings. let powerShellCustomizer: PowerShellCustomizerBuilder = PowerShellCustomizerBuilder() type PowerShellScriptCustomizerBuilder() = - member _.Yield _ = - { - Name = null - ScriptUri = null - Sha256Checksum = None - RunAsElevated = false - RunAsSystem = false - ValidExitCodes = [] - } + member _.Yield _ = { + Name = null + ScriptUri = null + Sha256Checksum = None + RunAsElevated = false + RunAsSystem = false + ValidExitCodes = [] + } member _.Run(customizer: PowerShellScriptCustomizer) = Customizer.PowerShellScript customizer @@ -283,22 +273,22 @@ type PowerShellScriptCustomizerBuilder() = member _.Name(customizer: PowerShellScriptCustomizer, name) = { customizer with Name = name } [] - member _.RunAsElevated(customizer: PowerShellScriptCustomizer, runAsElevated) = - { customizer with + member _.RunAsElevated(customizer: PowerShellScriptCustomizer, runAsElevated) = { + customizer with RunAsElevated = runAsElevated - } + } [] - member _.RunAsSystem(customizer: PowerShellScriptCustomizer, runAsSystem) = - { customizer with + member _.RunAsSystem(customizer: PowerShellScriptCustomizer, runAsSystem) = { + customizer with RunAsSystem = runAsSystem - } + } [] - member _.ValidExitCodes(customizer: PowerShellScriptCustomizer, validExitCodes) = - { customizer with + member _.ValidExitCodes(customizer: PowerShellScriptCustomizer, validExitCodes) = { + customizer with ValidExitCodes = customizer.ValidExitCodes @ validExitCodes - } + } [] member _.ScriptUri(customizer: PowerShellScriptCustomizer, uri) = { customizer with ScriptUri = uri } @@ -307,84 +297,81 @@ type PowerShellScriptCustomizerBuilder() = this.ScriptUri(customizer, System.Uri uri) [] - member _.Checksum(customizer: PowerShellScriptCustomizer, checksum) = - { customizer with + member _.Checksum(customizer: PowerShellScriptCustomizer, checksum) = { + customizer with Sha256Checksum = Some checksum - } + } /// PowerShell script customizer for downloading a PowerShell script to run on Windows images. let powerShellScriptCustomizer = PowerShellScriptCustomizerBuilder() type WindowsRestartCustomizerBuilder() = - member _.Yield _ = - { - RestartCommand = None - RestartCheckCommand = None - RestartTimeout = None - } + member _.Yield _ = { + RestartCommand = None + RestartCheckCommand = None + RestartTimeout = None + } member _.Run(customizer: WindowsRestartCustomizer) = Customizer.WindowsRestart customizer [] - member _.RestartCommand(customizer: WindowsRestartCustomizer, restartCommand) = - { customizer with + member _.RestartCommand(customizer: WindowsRestartCustomizer, restartCommand) = { + customizer with RestartCommand = Some restartCommand - } + } [] - member _.RestartCheckCommand(customizer: WindowsRestartCustomizer, restartCheckCommand) = - { customizer with + member _.RestartCheckCommand(customizer: WindowsRestartCustomizer, restartCheckCommand) = { + customizer with RestartCheckCommand = Some restartCheckCommand - } + } [] - member _.RestartTimeout(customizer: WindowsRestartCustomizer, restartTimeout) = - { customizer with + member _.RestartTimeout(customizer: WindowsRestartCustomizer, restartTimeout) = { + customizer with RestartTimeout = Some restartTimeout - } + } /// Windows Restart Customizer to restart Windows while building the image. let windowsRestartCustomizer = WindowsRestartCustomizerBuilder() type WindowsUpdateCustomizerBuilder() = - member _.Yield _ = - { - SearchCriteria = None - Filters = [] - UpdateLimit = None - } + member _.Yield _ = { + SearchCriteria = None + Filters = [] + UpdateLimit = None + } member _.Run(customizer: WindowsUpdateCustomizer) = Customizer.WindowsUpdate customizer [] - member _.SearchCriteria(customizer: WindowsUpdateCustomizer, searchCriteria) = - { customizer with + member _.SearchCriteria(customizer: WindowsUpdateCustomizer, searchCriteria) = { + customizer with SearchCriteria = Some searchCriteria - } + } [] - member _.Filters(customizer: WindowsUpdateCustomizer, filters) = - { customizer with + member _.Filters(customizer: WindowsUpdateCustomizer, filters) = { + customizer with Filters = customizer.Filters @ filters - } + } [] - member _.Destination(customizer: WindowsUpdateCustomizer, updateLimit) = - { customizer with + member _.Destination(customizer: WindowsUpdateCustomizer, updateLimit) = { + customizer with UpdateLimit = Some updateLimit - } + } /// Windows Update Customizer to install Windows updates while building the image. let windowsUpdateCustomizer = WindowsUpdateCustomizerBuilder() type ManagedImageDistributorBuilder() = - member _.Yield _ = - { - ImageId = images.resourceId ResourceName.Empty - RunOutputName = "managed-image-run" - Location = null - ArtifactTags = Map.empty - } + member _.Yield _ = { + ImageId = images.resourceId ResourceName.Empty + RunOutputName = "managed-image-run" + Location = null + ArtifactTags = Map.empty + } member _.Run(distributor: ManagedImageDistributor) = if distributor.ImageId = images.resourceId ResourceName.Empty then @@ -399,28 +386,28 @@ type ManagedImageDistributorBuilder() = member _.ImageId(distibutor: ManagedImageDistributor, imageId) = { distibutor with ImageId = imageId } [] - member _.Image(distibutor: ManagedImageDistributor, imageName: ResourceName) = - { distibutor with + member _.Image(distibutor: ManagedImageDistributor, imageName: ResourceName) = { + distibutor with ImageId = images.resourceId imageName - } + } [] - member _.Location(distibutor: ManagedImageDistributor, location: Location) = - { distibutor with + member _.Location(distibutor: ManagedImageDistributor, location: Location) = { + distibutor with Location = location.ArmValue - } + } [] - member _.RunOutputName(distibutor: ManagedImageDistributor, runOutputName) = - { distibutor with + member _.RunOutputName(distibutor: ManagedImageDistributor, runOutputName) = { + distibutor with RunOutputName = runOutputName - } + } [] - member _.AddTags(distibutor: ManagedImageDistributor, tags) = - { distibutor with + member _.AddTags(distibutor: ManagedImageDistributor, tags) = { + distibutor with ArtifactTags = distibutor.ArtifactTags |> Map.merge tags - } + } /// Managed Image Distributor to copy the image that is built to a managed image. let managedImageDistributor = ManagedImageDistributorBuilder() @@ -430,14 +417,13 @@ type SharedImageDistributorBuilder() = ResourceType("Microsoft.Compute/galleries/images", "2020-09-30") .resourceId (ResourceName.Empty, ResourceName.Empty) - member _.Yield _ = - { - GalleryImageId = emptyGalleryImageId - RunOutputName = "shared-image-run" - ReplicationRegions = List.empty - ExcludeFromLatest = None - ArtifactTags = Map.empty - } + member _.Yield _ = { + GalleryImageId = emptyGalleryImageId + RunOutputName = "shared-image-run" + ReplicationRegions = List.empty + ExcludeFromLatest = None + ArtifactTags = Map.empty + } member _.Run(distributor: SharedImageDistributor) = if distributor.GalleryImageId = emptyGalleryImageId then @@ -449,58 +435,57 @@ type SharedImageDistributorBuilder() = Distibutor.SharedImage distributor [] - member _.Image(distibutor: SharedImageDistributor, galleryImageId: ResourceId) = - { distibutor with + member _.Image(distibutor: SharedImageDistributor, galleryImageId: ResourceId) = { + distibutor with GalleryImageId = galleryImageId - } + } [] - member _.Image(distibutor: SharedImageDistributor, locations: Location list) = - { distibutor with + member _.Image(distibutor: SharedImageDistributor, locations: Location list) = { + distibutor with ReplicationRegions = distibutor.ReplicationRegions @ locations - } + } [] - member _.Image(distibutor: SharedImageDistributor, excludeFromLatest) = - { distibutor with + member _.Image(distibutor: SharedImageDistributor, excludeFromLatest) = { + distibutor with ExcludeFromLatest = Some excludeFromLatest - } + } [] - member _.RunOutputName(distibutor: SharedImageDistributor, runOutputName) = - { distibutor with + member _.RunOutputName(distibutor: SharedImageDistributor, runOutputName) = { + distibutor with RunOutputName = runOutputName - } + } [] - member _.AddTags(distibutor: SharedImageDistributor, tags) = - { distibutor with + member _.AddTags(distibutor: SharedImageDistributor, tags) = { + distibutor with ArtifactTags = distibutor.ArtifactTags |> Map.merge tags - } + } /// Shared Image Distributor to copy the image that is built to a gallery of shared images. let sharedImageDistributor = SharedImageDistributorBuilder() type VhdDistributorBuilder() = - member _.Yield _ = - { - RunOutputName = "vhd-run" - ArtifactTags = Map.empty - } + member _.Yield _ = { + RunOutputName = "vhd-run" + ArtifactTags = Map.empty + } member _.Run(distributor: VhdDistributor) = Distibutor.VHD distributor [] - member _.RunOutputName(distibutor: VhdDistributor, runOutputName) = - { distibutor with + member _.RunOutputName(distibutor: VhdDistributor, runOutputName) = { + distibutor with RunOutputName = runOutputName - } + } [] - member _.AddTags(distibutor: VhdDistributor, tags) = - { distibutor with + member _.AddTags(distibutor: VhdDistributor, tags) = { + distibutor with ArtifactTags = distibutor.ArtifactTags |> Map.merge tags - } + } /// VHD Distributor to simply leave the virtual hard disk that is built in a generated storage account. let vhdDistributor = VhdDistributorBuilder() diff --git a/src/Farmer/Builders/Builders.IotHub.fs b/src/Farmer/Builders/Builders.IotHub.fs index 1b7b0391a..aaf7f4d69 100644 --- a/src/Farmer/Builders/Builders.IotHub.fs +++ b/src/Farmer/Builders/Builders.IotHub.fs @@ -5,16 +5,15 @@ open Farmer open Farmer.Arm open Farmer.IotHub -type IotHubConfig = - { - Name: ResourceName - Sku: Sku - Capacity: int - RetentionDays: int option - PartitionCount: int option - DeviceProvisioning: FeatureFlag - Tags: Map - } +type IotHubConfig = { + Name: ResourceName + Sku: Sku + Capacity: int + RetentionDays: int option + PartitionCount: int option + DeviceProvisioning: FeatureFlag + Tags: Map +} with member private this.BuildKey(policy: Policy) = $"listKeys('{this.Name.Value}','2019-03-22').value[{policy.Index}].primaryKey" @@ -35,50 +34,48 @@ type IotHubConfig = interface IBuilder with member this.ResourceId = this.ResourceId - member this.BuildResources location = - [ + member this.BuildResources location = [ + { + Name = this.Name + Location = location + Sku = + match this.Sku with + | F1 -> Free + | B1 -> Devices.Sku.B1 this.Capacity + | B2 -> Devices.Sku.B2 this.Capacity + | B3 -> Devices.Sku.B3 this.Capacity + | S1 -> Devices.Sku.S1 this.Capacity + | S2 -> Devices.Sku.S2 this.Capacity + | S3 -> Devices.Sku.S3 this.Capacity + RetentionDays = this.RetentionDays + PartitionCount = this.PartitionCount + DefaultTtl = None + MaxDeliveryCount = None + Feedback = None + FileNotifications = None + Tags = this.Tags + } + + if this.DeviceProvisioning = Enabled then { - Name = this.Name + Name = this.Name.Map(sprintf "%s-dps") Location = location - Sku = - match this.Sku with - | F1 -> Free - | B1 -> Devices.Sku.B1 this.Capacity - | B2 -> Devices.Sku.B2 this.Capacity - | B3 -> Devices.Sku.B3 this.Capacity - | S1 -> Devices.Sku.S1 this.Capacity - | S2 -> Devices.Sku.S2 this.Capacity - | S3 -> Devices.Sku.S3 this.Capacity - RetentionDays = this.RetentionDays - PartitionCount = this.PartitionCount - DefaultTtl = None - MaxDeliveryCount = None - Feedback = None - FileNotifications = None + IotHubKey = this.GetKey IotHubOwner + IotHubName = this.Name Tags = this.Tags } - - if this.DeviceProvisioning = Enabled then - { - Name = this.Name.Map(sprintf "%s-dps") - Location = location - IotHubKey = this.GetKey IotHubOwner - IotHubName = this.Name - Tags = this.Tags - } - ] + ] type IotHubBuilder() = - member _.Yield _ = - { - Name = ResourceName.Empty - Sku = F1 - Capacity = 1 - RetentionDays = None - PartitionCount = None - DeviceProvisioning = Disabled - Tags = Map.empty - } + member _.Yield _ = { + Name = ResourceName.Empty + Sku = F1 + Capacity = 1 + RetentionDays = None + PartitionCount = None + DeviceProvisioning = Disabled + Tags = Map.empty + } member _.Run state = match state.PartitionCount with @@ -101,27 +98,26 @@ type IotHubBuilder() = [] /// Sets the name of the SKU/Tier for the IOT Hub instance. - member _.PartitionCount(state: IotHubConfig, partitions) = - { state with + member _.PartitionCount(state: IotHubConfig, partitions) = { + state with PartitionCount = Some partitions - } + } [] /// Sets the name of the SKU/Tier for the IOT Hub instance. - member _.RetentionDays(state: IotHubConfig, days) = - { state with RetentionDays = Some days } + member _.RetentionDays(state: IotHubConfig, days) = { state with RetentionDays = Some days } [] /// Sets the name of the SKU/Tier for the IOT Hub instance. - member _.DeviceProvisioning(state: IotHubConfig) = - { state with + member _.DeviceProvisioning(state: IotHubConfig) = { + state with DeviceProvisioning = Enabled - } + } interface ITaggable with - member _.Add state tags = - { state with + member _.Add state tags = { + state with Tags = state.Tags |> Map.merge tags - } + } let iotHub = IotHubBuilder() diff --git a/src/Farmer/Builders/Builders.KeyVault.fs b/src/Farmer/Builders/Builders.KeyVault.fs index 43fb7afb7..1c18189ae 100644 --- a/src/Farmer/Builders/Builders.KeyVault.fs +++ b/src/Farmer/Builders/Builders.KeyVault.fs @@ -7,68 +7,65 @@ open Farmer.Arm.KeyVault open System open Vaults -type AccessPolicyConfig = - { - ObjectId: ArmExpression - ApplicationId: Guid option - Permissions: {| Keys: KeyVault.Key Set - Secrets: KeyVault.Secret Set - Certificates: Certificate Set - Storage: Storage Set |} - } +type AccessPolicyConfig = { + ObjectId: ArmExpression + ApplicationId: Guid option + Permissions: {| + Keys: KeyVault.Key Set + Secrets: KeyVault.Secret Set + Certificates: Certificate Set + Storage: Storage Set + |} +} type CreateMode = | Recover of NonEmptyList | Default of AccessPolicyConfig list | Unspecified of AccessPolicyConfig list -type KeyVaultConfigSettings = - { - /// Specifies whether Azure Virtual Machines are permitted to retrieve certificates stored as secrets from the key vault. - VirtualMachineAccess: FeatureFlag option - /// Specifies whether Azure Resource Manager is permitted to retrieve secrets from the key vault. - ResourceManagerAccess: FeatureFlag option - /// Specifies whether Azure Disk Encryption is permitted to retrieve secrets from the vault and unwrap keys. - AzureDiskEncryptionAccess: FeatureFlag option - /// Specifies whether Soft Deletion is enabled for the vault - SoftDelete: SoftDeletionMode option - /// Specifies whether Azure role based authorization is used for data retrieval instead of any access policies on the key vault. - RbacAuthorization: FeatureFlag option - } - -type NetworkAcl = - { - IpRules: string list - VnetRules: string list - DefaultAction: DefaultAction option - Bypass: Bypass option - } - -type SecretConfig = - { - SecretName: string - Vault: LinkedResource option - Value: SecretValue - ContentType: string option - Enabled: bool option - ActivationDate: DateTime option - ExpirationDate: DateTime option - Dependencies: ResourceId Set - Tags: Map - } - - static member internal createUnsafe key = - { - SecretName = key - Vault = None - Value = ParameterSecret(SecureParameter key) - ContentType = None - Enabled = None - ActivationDate = None - ExpirationDate = None - Dependencies = Set.empty - Tags = Map.empty - } +type KeyVaultConfigSettings = { + /// Specifies whether Azure Virtual Machines are permitted to retrieve certificates stored as secrets from the key vault. + VirtualMachineAccess: FeatureFlag option + /// Specifies whether Azure Resource Manager is permitted to retrieve secrets from the key vault. + ResourceManagerAccess: FeatureFlag option + /// Specifies whether Azure Disk Encryption is permitted to retrieve secrets from the vault and unwrap keys. + AzureDiskEncryptionAccess: FeatureFlag option + /// Specifies whether Soft Deletion is enabled for the vault + SoftDelete: SoftDeletionMode option + /// Specifies whether Azure role based authorization is used for data retrieval instead of any access policies on the key vault. + RbacAuthorization: FeatureFlag option +} + +type NetworkAcl = { + IpRules: string list + VnetRules: string list + DefaultAction: DefaultAction option + Bypass: Bypass option +} + +type SecretConfig = { + SecretName: string + Vault: LinkedResource option + Value: SecretValue + ContentType: string option + Enabled: bool option + ActivationDate: DateTime option + ExpirationDate: DateTime option + Dependencies: ResourceId Set + Tags: Map +} with + + static member internal createUnsafe key = { + SecretName = key + Vault = None + Value = ParameterSecret(SecureParameter key) + ContentType = None + Enabled = None + ActivationDate = None + ExpirationDate = None + Dependencies = Set.empty + Tags = Map.empty + } static member internal isValid key = let charRulesPassed = @@ -89,8 +86,8 @@ type SecretConfig = SecretConfig.isValid secretName SecretConfig.createUnsafe secretName - static member create(secretName, expression) = - { SecretConfig.create secretName with + static member create(secretName, expression) = { + SecretConfig.create secretName with Value = ExpressionSecret expression Dependencies = match expression.Owner with @@ -98,7 +95,7 @@ type SecretConfig = | None -> raiseFarmer $"The supplied ARM expression ('{expression.Value}') has no resource owner. You should explicitly set this using WithOwner(), supplying the Resource Name of the owner." - } + } member this.ResourceName = this.Vault |> Option.map (fun x -> x.Name / this.SecretName) member this.ResourceId = this.ResourceName |> Option.map secrets.resourceId @@ -112,40 +109,37 @@ type SecretConfig = | None -> SecretConfig.HandleNoVault() | Some id -> id - member this.BuildResources location = - [ - match this.ResourceName with - | None -> SecretConfig.HandleNoVault() - | Some name -> - { - Name = name - Value = this.Value - ContentType = this.ContentType - Enabled = this.Enabled - ActivationDate = this.ActivationDate - ExpirationDate = this.ExpirationDate - Location = location - Dependencies = - match this.Vault with - | Some (Managed id) -> this.Dependencies.Add id - | Some (Unmanaged _) -> this.Dependencies - | None -> SecretConfig.HandleNoVault() - Tags = this.Tags - } - ] - -type KeyConfig = - { - KeyName: ResourceName - Vault: LinkedResource option - Enabled: bool option - ActivationDate: DateTime option - ExpirationDate: DateTime option - KeyOps: KeyOperation list - KTY: KeyType - Dependencies: ResourceId Set - Tags: Map - } + member this.BuildResources location = [ + match this.ResourceName with + | None -> SecretConfig.HandleNoVault() + | Some name -> { + Name = name + Value = this.Value + ContentType = this.ContentType + Enabled = this.Enabled + ActivationDate = this.ActivationDate + ExpirationDate = this.ExpirationDate + Location = location + Dependencies = + match this.Vault with + | Some(Managed id) -> this.Dependencies.Add id + | Some(Unmanaged _) -> this.Dependencies + | None -> SecretConfig.HandleNoVault() + Tags = this.Tags + } + ] + +type KeyConfig = { + KeyName: ResourceName + Vault: LinkedResource option + Enabled: bool option + ActivationDate: DateTime option + ExpirationDate: DateTime option + KeyOps: KeyOperation list + KTY: KeyType + Dependencies: ResourceId Set + Tags: Map +} with member private this.vault = match this.Vault with @@ -156,44 +150,43 @@ type KeyConfig = member this.ResourceId = let resId = keys.resourceId (this.vault.Name / this.KeyName) - { resId with - Subscription = this.vault.ResourceId.Subscription - ResourceGroup = this.vault.ResourceId.ResourceGroup + { + resId with + Subscription = this.vault.ResourceId.Subscription + ResourceGroup = this.vault.ResourceId.ResourceGroup } - member this.BuildResources location = - [ - { - KeyName = this.KeyName - VaultName = this.vault.Name - KeyOps = this.KeyOps - KTY = this.KTY - Location = location - Enabled = this.Enabled - ActivationDate = this.ActivationDate - ExpirationDate = this.ExpirationDate - Dependencies = - match this.vault with - | Managed id -> this.Dependencies.Add id - | Unmanaged _ -> this.Dependencies - Tags = this.Tags - } - ] - -type KeyVaultConfig = - { - Name: ResourceName - TenantId: ArmExpression - Access: KeyVaultConfigSettings - Sku: Sku - Policies: CreateMode - NetworkAcl: NetworkAcl - Uri: Uri option - Keys: KeyConfig list - Secrets: SecretConfig list - DisablePublicNetworkAccess: FeatureFlag option - Tags: Map - } + member this.BuildResources location = [ + { + KeyName = this.KeyName + VaultName = this.vault.Name + KeyOps = this.KeyOps + KTY = this.KTY + Location = location + Enabled = this.Enabled + ActivationDate = this.ActivationDate + ExpirationDate = this.ExpirationDate + Dependencies = + match this.vault with + | Managed id -> this.Dependencies.Add id + | Unmanaged _ -> this.Dependencies + Tags = this.Tags + } + ] + +type KeyVaultConfig = { + Name: ResourceName + TenantId: ArmExpression + Access: KeyVaultConfigSettings + Sku: Sku + Policies: CreateMode + NetworkAcl: NetworkAcl + Uri: Uri option + Keys: KeyConfig list + Secrets: SecretConfig list + DisablePublicNetworkAccess: FeatureFlag option + Tags: Map +} with member this.ResourceId = vaults.resourceId this.Name @@ -205,86 +198,81 @@ type KeyVaultConfig = interface IBuilder with member this.ResourceId = this.ResourceId - member this.BuildResources location = - [ - let keyVault = - { - Name = this.Name - Location = location - TenantId = this.TenantId |> ArmExpression.Eval - Sku = this.Sku - TemplateDeployment = this.Access.ResourceManagerAccess - DiskEncryption = this.Access.AzureDiskEncryptionAccess - Deployment = this.Access.VirtualMachineAccess - RbacAuthorization = this.Access.RbacAuthorization - SoftDelete = this.Access.SoftDelete - CreateMode = - match this.Policies with - | Unspecified _ -> None - | Recover _ -> Some Arm.KeyVault.Recover - | Default _ -> Some Arm.KeyVault.Default - AccessPolicies = - let policies = - match this.Policies with - | Unspecified policies -> policies - | Recover list -> list.Value - | Default policies -> policies - - [ - for policy in policies do - {| - ObjectId = policy.ObjectId - ApplicationId = policy.ApplicationId - Permissions = - {| - Certificates = policy.Permissions.Certificates - Storage = policy.Permissions.Storage - Keys = policy.Permissions.Keys - Secrets = policy.Permissions.Secrets - |} - |} - ] - Uri = this.Uri - DefaultAction = this.NetworkAcl.DefaultAction - Bypass = this.NetworkAcl.Bypass - IpRules = this.NetworkAcl.IpRules - VnetRules = this.NetworkAcl.VnetRules - DisablePublicNetworkAccess = this.DisablePublicNetworkAccess - Tags = this.Tags - } - - keyVault - - yield! - this.Keys - |> List.map (fun s -> - { s with - Vault = Some(Managed this.ResourceId) - }) - |> List.collect (fun s -> (s :> IBuilder).BuildResources location) - - yield! - this.Secrets - |> List.map (fun s -> - { s with - Vault = Some(Managed this.ResourceId) - }) - |> List.collect (fun s -> (s :> IBuilder).BuildResources location) - ] + member this.BuildResources location = [ + let keyVault = { + Name = this.Name + Location = location + TenantId = this.TenantId |> ArmExpression.Eval + Sku = this.Sku + TemplateDeployment = this.Access.ResourceManagerAccess + DiskEncryption = this.Access.AzureDiskEncryptionAccess + Deployment = this.Access.VirtualMachineAccess + RbacAuthorization = this.Access.RbacAuthorization + SoftDelete = this.Access.SoftDelete + CreateMode = + match this.Policies with + | Unspecified _ -> None + | Recover _ -> Some Arm.KeyVault.Recover + | Default _ -> Some Arm.KeyVault.Default + AccessPolicies = + let policies = + match this.Policies with + | Unspecified policies -> policies + | Recover list -> list.Value + | Default policies -> policies + + [ + for policy in policies do + {| + ObjectId = policy.ObjectId + ApplicationId = policy.ApplicationId + Permissions = {| + Certificates = policy.Permissions.Certificates + Storage = policy.Permissions.Storage + Keys = policy.Permissions.Keys + Secrets = policy.Permissions.Secrets + |} + |} + ] + Uri = this.Uri + DefaultAction = this.NetworkAcl.DefaultAction + Bypass = this.NetworkAcl.Bypass + IpRules = this.NetworkAcl.IpRules + VnetRules = this.NetworkAcl.VnetRules + DisablePublicNetworkAccess = this.DisablePublicNetworkAccess + Tags = this.Tags + } + + keyVault + + yield! + this.Keys + |> List.map (fun s -> { + s with + Vault = Some(Managed this.ResourceId) + }) + |> List.collect (fun s -> (s :> IBuilder).BuildResources location) + + yield! + this.Secrets + |> List.map (fun s -> { + s with + Vault = Some(Managed this.ResourceId) + }) + |> List.collect (fun s -> (s :> IBuilder).BuildResources location) + ] type AccessPolicyBuilder() = - member _.Yield _ = - { - ObjectId = ArmExpression.create (string Guid.Empty) - ApplicationId = None - Permissions = - {| - Keys = Set.empty - Secrets = Set.empty - Certificates = Set.empty - Storage = Set.empty - |} - } + member _.Yield _ = { + ObjectId = ArmExpression.create (string Guid.Empty) + ApplicationId = None + Permissions = {| + Keys = Set.empty + Secrets = Set.empty + Certificates = Set.empty + Storage = Set.empty + |} + } /// Sets the Object ID of the permission set. [] @@ -302,60 +290,59 @@ type AccessPolicyBuilder() = /// Sets the Application ID of the permission set. [] - member _.ApplicationId(state: AccessPolicyConfig, applicationId) = - { state with + member _.ApplicationId(state: AccessPolicyConfig, applicationId) = { + state with ApplicationId = Some applicationId - } + } /// Sets the Key permissions of the permission set. [] - member _.SetKeyPermissions(state: AccessPolicyConfig, permissions) = - { state with - Permissions = - {| state.Permissions with + member _.SetKeyPermissions(state: AccessPolicyConfig, permissions) = { + state with + Permissions = {| + state.Permissions with Keys = set permissions - |} - } + |} + } /// Sets the Storage permissions of the permission set. [] - member _.SetStoragePermissions(state: AccessPolicyConfig, permissions) = - { state with - Permissions = - {| state.Permissions with + member _.SetStoragePermissions(state: AccessPolicyConfig, permissions) = { + state with + Permissions = {| + state.Permissions with Storage = set permissions - |} - } + |} + } /// Sets the Secret permissions of the permission set. [] - member _.SetSecretPermissions(state: AccessPolicyConfig, permissions) = - { state with - Permissions = - {| state.Permissions with + member _.SetSecretPermissions(state: AccessPolicyConfig, permissions) = { + state with + Permissions = {| + state.Permissions with Secrets = set permissions - |} - } + |} + } /// Sets the Certificate permissions of the permission set. [] - member _.SetCertificatePermissions(state: AccessPolicyConfig, permissions) = - { state with - Permissions = - {| state.Permissions with + member _.SetCertificatePermissions(state: AccessPolicyConfig, permissions) = { + state with + Permissions = {| + state.Permissions with Certificates = set permissions - |} - } + |} + } let accessPolicy = AccessPolicyBuilder() type AccessPolicy = /// Quickly creates an access policy for the supplied Principal. If no permissions are supplied, defaults to GET and LIST. - static member create(principal: PrincipalId, ?permissions) = - accessPolicy { - object_id principal - secret_permissions (permissions |> Option.defaultValue Secret.ReadSecrets) - } + static member create(principal: PrincipalId, ?permissions) = accessPolicy { + object_id principal + secret_permissions (permissions |> Option.defaultValue Secret.ReadSecrets) + } /// Quickly creates an access policy for the supplied Identity. If no permissions are supplied, defaults to GET and LIST. static member create(identity: UserAssignedIdentityConfig, ?permissions) = @@ -366,11 +353,10 @@ type AccessPolicy = AccessPolicy.create (identity.PrincipalId, ?permissions = permissions) /// Quickly creates an access policy for the supplied ObjectId. If no permissions are supplied, defaults to GET and LIST. - static member create(objectId: ObjectId, ?permissions) = - accessPolicy { - object_id objectId - secret_permissions (permissions |> Option.defaultValue Secret.ReadSecrets) - } + static member create(objectId: ObjectId, ?permissions) = accessPolicy { + object_id objectId + secret_permissions (permissions |> Option.defaultValue Secret.ReadSecrets) + } static member private findEntity(searchField, values, searcher) = values @@ -379,15 +365,17 @@ type AccessPolicy = |> sprintf "\"%s\"" |> searcher |> Result.map ( - Serialization.ofJson<{| DisplayName: string - ObjectId: Guid |} array> + Serialization.ofJson<{| + DisplayName: string + ObjectId: Guid + |} array> ) |> Result.toOption |> Option.map ( - Array.map (fun r -> - {| r with + Array.map (fun r -> {| + r with ObjectId = ObjectId r.ObjectId - |}) + |}) ) |> Option.defaultValue Array.empty @@ -404,73 +392,68 @@ type SimpleCreateMode = | Recover | Default -type KeyVaultBuilderState = - { - Name: ResourceName - Access: KeyVaultConfigSettings - Sku: Sku - TenantId: ArmExpression - NetworkAcl: NetworkAcl - CreateMode: SimpleCreateMode option - Policies: AccessPolicyConfig list - Uri: Uri option - Secrets: SecretConfig list - Keys: KeyConfig list - DisablePublicNetworkAccess: FeatureFlag option - Tags: Map - } +type KeyVaultBuilderState = { + Name: ResourceName + Access: KeyVaultConfigSettings + Sku: Sku + TenantId: ArmExpression + NetworkAcl: NetworkAcl + CreateMode: SimpleCreateMode option + Policies: AccessPolicyConfig list + Uri: Uri option + Secrets: SecretConfig list + Keys: KeyConfig list + DisablePublicNetworkAccess: FeatureFlag option + Tags: Map +} type KeyVaultBuilder() = - member _.Yield(_: unit) = - { - Name = ResourceName.Empty - TenantId = Subscription.TenantId - Access = - { - VirtualMachineAccess = None - RbacAuthorization = None - ResourceManagerAccess = Some Enabled - AzureDiskEncryptionAccess = None - SoftDelete = None - } - Sku = Standard - NetworkAcl = - { - IpRules = [] - VnetRules = [] - Bypass = None - DefaultAction = None - } - Policies = [] - CreateMode = None - Uri = None - Secrets = [] - Keys = [] - DisablePublicNetworkAccess = None - Tags = Map.empty - } + member _.Yield(_: unit) = { + Name = ResourceName.Empty + TenantId = Subscription.TenantId + Access = { + VirtualMachineAccess = None + RbacAuthorization = None + ResourceManagerAccess = Some Enabled + AzureDiskEncryptionAccess = None + SoftDelete = None + } + Sku = Standard + NetworkAcl = { + IpRules = [] + VnetRules = [] + Bypass = None + DefaultAction = None + } + Policies = [] + CreateMode = None + Uri = None + Secrets = [] + Keys = [] + DisablePublicNetworkAccess = None + Tags = Map.empty + } - member _.Run(state: KeyVaultBuilderState) : KeyVaultConfig = - { - Name = state.Name - Access = state.Access - Sku = state.Sku - NetworkAcl = state.NetworkAcl - TenantId = state.TenantId - Policies = - match state.CreateMode, state.Policies with - | None, policies -> Unspecified policies - | Some SimpleCreateMode.Default, policies -> Default policies - | Some SimpleCreateMode.Recover, [] -> - raiseFarmer - "Setting the creation mode to Recover requires at least one access policy. Use the accessPolicy builder to create a policy, and add it to the vault configuration using add_access_policy." - | Some SimpleCreateMode.Recover, policies -> Recover(NonEmptyList.create policies) - Keys = state.Keys - Secrets = state.Secrets - Uri = state.Uri - DisablePublicNetworkAccess = state.DisablePublicNetworkAccess - Tags = state.Tags - } + member _.Run(state: KeyVaultBuilderState) : KeyVaultConfig = { + Name = state.Name + Access = state.Access + Sku = state.Sku + NetworkAcl = state.NetworkAcl + TenantId = state.TenantId + Policies = + match state.CreateMode, state.Policies with + | None, policies -> Unspecified policies + | Some SimpleCreateMode.Default, policies -> Default policies + | Some SimpleCreateMode.Recover, [] -> + raiseFarmer + "Setting the creation mode to Recover requires at least one access policy. Use the accessPolicy builder to create a policy, and add it to the vault configuration using add_access_policy." + | Some SimpleCreateMode.Recover, policies -> Recover(NonEmptyList.create policies) + Keys = state.Keys + Secrets = state.Secrets + Uri = state.Uri + DisablePublicNetworkAccess = state.DisablePublicNetworkAccess + Tags = state.Tags + } /// Sets the name of the vault. [] @@ -494,103 +477,103 @@ type KeyVaultBuilder() = /// Allows VM access to the vault. [] - member _.EnableVmAccess(state: KeyVaultBuilderState) = - { state with - Access = - { state.Access with + member _.EnableVmAccess(state: KeyVaultBuilderState) = { + state with + Access = { + state.Access with VirtualMachineAccess = Some Enabled - } - } + } + } /// Disallows VM access to the vault. [] - member _.DisableVmAccess(state: KeyVaultBuilderState) = - { state with - Access = - { state.Access with + member _.DisableVmAccess(state: KeyVaultBuilderState) = { + state with + Access = { + state.Access with VirtualMachineAccess = Some Disabled - } - } + } + } /// Allows Resource Manager access to the vault so that you can deploy secrets during ARM deployments. [] - member _.EnableResourceManagerAccess(state: KeyVaultBuilderState) = - { state with - Access = - { state.Access with + member _.EnableResourceManagerAccess(state: KeyVaultBuilderState) = { + state with + Access = { + state.Access with ResourceManagerAccess = Some Enabled - } - } + } + } /// Disallows Resource Manager access to the vault. [] - member _.DisableResourceManagerAccess(state: KeyVaultBuilderState) = - { state with - Access = - { state.Access with + member _.DisableResourceManagerAccess(state: KeyVaultBuilderState) = { + state with + Access = { + state.Access with ResourceManagerAccess = Some Disabled - } - } + } + } /// Allows Azure Disk Encyption service access to the vault. [] - member _.EnableDiskEncryptionAccess(state: KeyVaultBuilderState) = - { state with - Access = - { state.Access with + member _.EnableDiskEncryptionAccess(state: KeyVaultBuilderState) = { + state with + Access = { + state.Access with AzureDiskEncryptionAccess = Some Enabled - } - } + } + } /// Disallows Azure Disk Encyption service access to the vault. [] - member _.DisableDiskEncryptionAccess(state: KeyVaultBuilderState) = - { state with - Access = - { state.Access with + member _.DisableDiskEncryptionAccess(state: KeyVaultBuilderState) = { + state with + Access = { + state.Access with AzureDiskEncryptionAccess = Some Disabled - } - } + } + } /// Enables Azure role based authentication for access to key vault data. [] - member _.EnableRbacAuthorization(state: KeyVaultBuilderState) = - { state with - Access = - { state.Access with + member _.EnableRbacAuthorization(state: KeyVaultBuilderState) = { + state with + Access = { + state.Access with RbacAuthorization = Some Enabled - } - } + } + } /// Disables Azure role based authentication for access to key vault data. [] - member _.DisableRbacAuthorization(state: KeyVaultBuilderState) = - { state with - Access = - { state.Access with + member _.DisableRbacAuthorization(state: KeyVaultBuilderState) = { + state with + Access = { + state.Access with RbacAuthorization = Some Disabled - } - } + } + } /// Enables VM access to the vault. [] - member _.EnableSoftDeletion(state: KeyVaultBuilderState) = - { state with - Access = - { state.Access with + member _.EnableSoftDeletion(state: KeyVaultBuilderState) = { + state with + Access = { + state.Access with SoftDelete = Some SoftDeletionOnly - } - } + } + } /// Disables VM access to the vault. [] - member _.EnableSoftDeletionWithPurgeProtection(state: KeyVaultBuilderState) = - { state with - Access = - { state.Access with + member _.EnableSoftDeletionWithPurgeProtection(state: KeyVaultBuilderState) = { + state with + Access = { + state.Access with SoftDelete = Some SoftDeleteWithPurgeProtection - } - } + } + } /// Sets the URI of the vault. [] @@ -598,91 +581,91 @@ type KeyVaultBuilder() = /// Sets the Creation Mode to Recovery. [] - member _.EnableRecoveryMode(state: KeyVaultBuilderState) = - { state with + member _.EnableRecoveryMode(state: KeyVaultBuilderState) = { + state with CreateMode = Some SimpleCreateMode.Recover - } + } /// Sets the Creation Mode to Default. [] - member _.DisableRecoveryMode(state: KeyVaultBuilderState) = - { state with + member _.DisableRecoveryMode(state: KeyVaultBuilderState) = { + state with CreateMode = Some SimpleCreateMode.Default - } + } /// Adds an access policy to the vault. [] - member _.AddAccessPolicy(state: KeyVaultBuilderState, accessPolicy) = - { state with + member _.AddAccessPolicy(state: KeyVaultBuilderState, accessPolicy) = { + state with Policies = accessPolicy :: state.Policies - } + } /// Adds access policies to the vault. [] - member _.AddAccessPolicies(state: KeyVaultBuilderState, accessPolicies) = - { state with + member _.AddAccessPolicies(state: KeyVaultBuilderState, accessPolicies) = { + state with Policies = List.append accessPolicies state.Policies - } + } /// Allows Azure traffic can bypass network rules. [] - member _.EnableBypass(state: KeyVaultBuilderState) = - { state with - NetworkAcl = - { state.NetworkAcl with + member _.EnableBypass(state: KeyVaultBuilderState) = { + state with + NetworkAcl = { + state.NetworkAcl with Bypass = Some AzureServices - } - } + } + } /// Disallows Azure traffic can bypass network rules. [] - member _.DisableBypass(state: KeyVaultBuilderState) = - { state with - NetworkAcl = - { state.NetworkAcl with + member _.DisableBypass(state: KeyVaultBuilderState) = { + state with + NetworkAcl = { + state.NetworkAcl with Bypass = Some NoTraffic - } - } + } + } /// Allow traffic if no rule from ipRules and virtualNetworkRules match. This is only used after the bypass property has been evaluated. [] - member _.AllowDefaultTraffic(state: KeyVaultBuilderState) = - { state with - NetworkAcl = - { state.NetworkAcl with + member _.AllowDefaultTraffic(state: KeyVaultBuilderState) = { + state with + NetworkAcl = { + state.NetworkAcl with DefaultAction = Some Allow - } - } + } + } /// Deny traffic when no rule from ipRules and virtualNetworkRules match. This is only used after the bypass property has been evaluated. [] - member _.DenyDefaultTraffic(state: KeyVaultBuilderState) = - { state with - NetworkAcl = - { state.NetworkAcl with + member _.DenyDefaultTraffic(state: KeyVaultBuilderState) = { + state with + NetworkAcl = { + state.NetworkAcl with DefaultAction = Some Deny - } - } + } + } /// Adds an IP address rule. This can be an IPv4 address range in CIDR notation, such as '124.56.78.91' (simple IP address) or '124.56.78.0/24' (all addresses that start with 124.56.78). [] - member _.AddIpRule(state: KeyVaultBuilderState, ipRule) = - { state with - NetworkAcl = - { state.NetworkAcl with + member _.AddIpRule(state: KeyVaultBuilderState, ipRule) = { + state with + NetworkAcl = { + state.NetworkAcl with IpRules = ipRule :: state.NetworkAcl.IpRules - } - } + } + } /// Adds a virtual network rule. This is the full resource id of a vnet subnet, such as '/subscriptions/subid/resourceGroups/rg1/providers/Microsoft.Network/virtualNetworks/test-vnet/subnets/subnet1'. [] - member _.AddVnetRule(state: KeyVaultBuilderState, vnetRule) = - { state with - NetworkAcl = - { state.NetworkAcl with + member _.AddVnetRule(state: KeyVaultBuilderState, vnetRule) = { + state with + NetworkAcl = { + state.NetworkAcl with VnetRules = vnetRule :: state.NetworkAcl.VnetRules - } - } + } + } /// Allows adding a key to the vault. [] @@ -694,10 +677,10 @@ type KeyVaultBuilder() = /// Allows to add a secret to the vault. [] - member _.AddSecret(state: KeyVaultBuilderState, key: SecretConfig) = - { state with + member _.AddSecret(state: KeyVaultBuilderState, key: SecretConfig) = { + state with Secrets = key :: state.Secrets - } + } member this.AddSecret(state: KeyVaultBuilderState, key: string) = this.AddSecret(state, SecretConfig.create key) @@ -722,53 +705,53 @@ type KeyVaultBuilder() = member _.DisablePublicNetworkAccess(state: KeyVaultBuilderState, ?flag: FeatureFlag) = let flag = defaultArg flag FeatureFlag.Enabled - { state with - DisablePublicNetworkAccess = Some flag + { + state with + DisablePublicNetworkAccess = Some flag } interface ITaggable with - member _.Add state tags = - { state with + member _.Add state tags = { + state with Tags = state.Tags |> Map.merge tags - } + } type KeyBuilder() = - member _.Yield _ : KeyConfig = - { - KeyName = ResourceName.Empty - Vault = None - Enabled = None - ActivationDate = None - ExpirationDate = None - KeyOps = [] - KTY = KeyType.RSA_2048 - Dependencies = Set.empty - Tags = Map.empty - } + member _.Yield _ : KeyConfig = { + KeyName = ResourceName.Empty + Vault = None + Enabled = None + ActivationDate = None + ExpirationDate = None + KeyOps = [] + KTY = KeyType.RSA_2048 + Dependencies = Set.empty + Tags = Map.empty + } [] - member _.Name(state: KeyConfig, name) = - { state with + member _.Name(state: KeyConfig, name) = { + state with KeyName = ResourceName name - } + } [] - member _.Enabled(state: KeyConfig, featureFlag: FeatureFlag) = - { state with + member _.Enabled(state: KeyConfig, featureFlag: FeatureFlag) = { + state with Enabled = Some featureFlag.AsBoolean - } + } [] - member _.ActivationDate(state: KeyConfig, activationDate) = - { state with + member _.ActivationDate(state: KeyConfig, activationDate) = { + state with ActivationDate = Some activationDate - } + } [] - member _.ExpirationDate(state: KeyConfig, expirationDate) = - { state with + member _.ExpirationDate(state: KeyConfig, expirationDate) = { + state with ExpirationDate = Some expirationDate - } + } [] member _.KeyOperations(state: KeyConfig, keyOperations: KeyOperation list) = { state with KeyOps = keyOperations } @@ -777,32 +760,32 @@ type KeyBuilder() = member _.KeyType(state: KeyConfig, keyType: KeyType) = { state with KTY = keyType } [] - member _.LinkToKeyVault(state: KeyConfig, keyVault: ResourceId) = - { state with + member _.LinkToKeyVault(state: KeyConfig, keyVault: ResourceId) = { + state with Vault = Some(Unmanaged keyVault) - } + } - member _.LinkToKeyVault(state: KeyConfig, keyVault: KeyVaultConfig) = - { state with + member _.LinkToKeyVault(state: KeyConfig, keyVault: KeyVaultConfig) = { + state with Vault = Some(Unmanaged keyVault.ResourceId) - } + } - member _.LinkToKeyVault(state: KeyConfig, keyVault: IArmResource) = - { state with + member _.LinkToKeyVault(state: KeyConfig, keyVault: IArmResource) = { + state with Vault = Some(Unmanaged keyVault.ResourceId) - } + } interface ITaggable with - member _.Add state tags = - { state with + member _.Add state tags = { + state with Tags = state.Tags |> Map.merge tags - } + } interface IDependable with - member _.Add state newDeps = - { state with + member _.Add state newDeps = { + state with Dependencies = state.Dependencies + newDeps - } + } type SecretBuilder() = @@ -813,23 +796,23 @@ type SecretBuilder() = member _.Yield(_: unit) = SecretConfig.createUnsafe "" [] - member _.Name(state: SecretConfig, name) = - { state with + member _.Name(state: SecretConfig, name) = { + state with SecretName = name Value = ParameterSecret(SecureParameter name) - } + } [] - member _.Value(state: SecretConfig, value) = - { state with + member _.Value(state: SecretConfig, value) = { + state with Value = ExpressionSecret value - } + } [] - member _.ContentType(state: SecretConfig, contentType) = - { state with + member _.ContentType(state: SecretConfig, contentType) = { + state with ContentType = Some contentType - } + } [] // Leaving in for compatibility - should use FeatureFlag member _.EnableSecret(state: SecretConfig) = { state with Enabled = Some true } @@ -838,79 +821,75 @@ type SecretBuilder() = member _.DisableSecret(state: SecretConfig) = { state with Enabled = Some false } [] - member _.Enabled(state: SecretConfig, featureFlag: FeatureFlag) = - { state with + member _.Enabled(state: SecretConfig, featureFlag: FeatureFlag) = { + state with Enabled = Some featureFlag.AsBoolean - } + } [] - member _.ActivationDate(state: SecretConfig, activationDate) = - { state with + member _.ActivationDate(state: SecretConfig, activationDate) = { + state with ActivationDate = Some activationDate - } + } [] - member _.ExpirationDate(state: SecretConfig, expirationDate) = - { state with + member _.ExpirationDate(state: SecretConfig, expirationDate) = { + state with ExpirationDate = Some expirationDate - } + } [] - member _.LinkToKeyVault(state: SecretConfig, keyVault) = - { state with + member _.LinkToKeyVault(state: SecretConfig, keyVault) = { + state with Vault = Some(Unmanaged keyVault) - } + } interface ITaggable with - member _.Add state tags = - { state with + member _.Add state tags = { + state with Tags = state.Tags |> Map.merge tags - } + } interface IDependable with - member _.Add state newDeps = - { state with + member _.Add state newDeps = { + state with Dependencies = state.Dependencies + newDeps - } + } let key = KeyBuilder() let secret = SecretBuilder() let keyVault = KeyVaultBuilder() /// Configuration for adding access policies to an existing key vault. -type KeyVaultAddPoliciesConfig = - { - KeyVault: LinkedResource option - TenantId: string option - AccessPolicies: AccessPolicyConfig list - } +type KeyVaultAddPoliciesConfig = { + KeyVault: LinkedResource option + TenantId: string option + AccessPolicies: AccessPolicyConfig list +} with interface IBuilder with member this.BuildResources _ = match this.KeyVault with | None -> raiseFarmer "Key vault policy addition must be linked to a key vault to properly assign the resourceId." - | Some kv -> - [ - { - VaultAddPolicies.KeyVault = kv - TenantId = this.TenantId - AccessPolicies = - this.AccessPolicies - |> List.map (fun policy -> - {| - ObjectId = policy.ObjectId - ApplicationId = policy.ApplicationId - Permissions = - {| - Certificates = policy.Permissions.Certificates - Storage = policy.Permissions.Storage - Keys = policy.Permissions.Keys - Secrets = policy.Permissions.Secrets - |} - |}) - } - ] + | Some kv -> [ + { + VaultAddPolicies.KeyVault = kv + TenantId = this.TenantId + AccessPolicies = + this.AccessPolicies + |> List.map (fun policy -> {| + ObjectId = policy.ObjectId + ApplicationId = policy.ApplicationId + Permissions = {| + Certificates = policy.Permissions.Certificates + Storage = policy.Permissions.Storage + Keys = policy.Permissions.Keys + Secrets = policy.Permissions.Secrets + |} + |}) + } + ] member this.ResourceId = match this.KeyVault with @@ -920,29 +899,28 @@ type KeyVaultAddPoliciesConfig = /// Builder for adding policies to an existing key vault. type KeyVaultAddPoliciesBuilder() = - member _.Yield _ = - { - KeyVault = None - TenantId = None - AccessPolicies = [] - } + member _.Yield _ = { + KeyVault = None + TenantId = None + AccessPolicies = [] + } /// The key vault where the policies should be added. [] - member _.KeyVault(state: KeyVaultAddPoliciesConfig, kv: Vault) = - { state with + member _.KeyVault(state: KeyVaultAddPoliciesConfig, kv: Vault) = { + state with KeyVault = Some(Unmanaged (kv :> IArmResource).ResourceId) - } + } - member _.KeyVault(state: KeyVaultAddPoliciesConfig, kv: KeyVaultConfig) = - { state with + member _.KeyVault(state: KeyVaultAddPoliciesConfig, kv: KeyVaultConfig) = { + state with KeyVault = Some(Unmanaged (kv :> IBuilder).ResourceId) - } + } - member _.KeyVault(state: KeyVaultAddPoliciesConfig, kv: ResourceId) = - { state with + member _.KeyVault(state: KeyVaultAddPoliciesConfig, kv: ResourceId) = { + state with KeyVault = Some(Unmanaged kv) - } + } /// Specify the tenant ID for the users or service principals being granted access. [] @@ -950,9 +928,9 @@ type KeyVaultAddPoliciesBuilder() = /// Access polices to add to the key vault. [] - member _.AddAccessPolicies(state: KeyVaultAddPoliciesConfig, accessPolicies: AccessPolicyConfig list) = - { state with + member _.AddAccessPolicies(state: KeyVaultAddPoliciesConfig, accessPolicies: AccessPolicyConfig list) = { + state with AccessPolicies = state.AccessPolicies @ accessPolicies - } + } let keyVaultAddPolicies = KeyVaultAddPoliciesBuilder() diff --git a/src/Farmer/Builders/Builders.LoadBalancer.fs b/src/Farmer/Builders/Builders.LoadBalancer.fs index 8f4fd8147..0b447d9ee 100644 --- a/src/Farmer/Builders/Builders.LoadBalancer.fs +++ b/src/Farmer/Builders/Builders.LoadBalancer.fs @@ -8,21 +8,19 @@ open Farmer.Arm.Network open Farmer.LoadBalancer open Farmer.PublicIpAddress -type FrontendIpConfig = - { - Name: ResourceName - PrivateIpAllocationMethod: PrivateIpAddress.AllocationMethod - PublicIp: LinkedResource option - Subnet: LinkedResource option - } - - static member BuildResource frontend = - {| - Name = frontend.Name - PrivateIpAllocationMethod = frontend.PrivateIpAllocationMethod - PublicIp = frontend.PublicIp |> Option.map (fun linkedRes -> linkedRes.ResourceId) - Subnet = frontend.Subnet |> Option.map (fun linkedRes -> linkedRes.ResourceId) - |} +type FrontendIpConfig = { + Name: ResourceName + PrivateIpAllocationMethod: PrivateIpAddress.AllocationMethod + PublicIp: LinkedResource option + Subnet: LinkedResource option +} with + + static member BuildResource frontend = {| + Name = frontend.Name + PrivateIpAllocationMethod = frontend.PrivateIpAllocationMethod + PublicIp = frontend.PublicIp |> Option.map (fun linkedRes -> linkedRes.ResourceId) + Subnet = frontend.Subnet |> Option.map (fun linkedRes -> linkedRes.ResourceId) + |} static member BuildIp (frontend: FrontendIpConfig) @@ -31,7 +29,7 @@ type FrontendIpConfig = (location: Location) : PublicIpAddress option = match frontend.PublicIp with - | Some (Managed resId) -> + | Some(Managed resId) -> { Name = resId.Name AllocationMethod = AllocationMethod.Static @@ -49,13 +47,12 @@ type FrontendIpConfig = type FrontendIpBuilder() = - member _.Yield _ = - { - Name = ResourceName.Empty - PrivateIpAllocationMethod = PrivateIpAddress.DynamicPrivateIp - PublicIp = None - Subnet = None - } + member _.Yield _ = { + Name = ResourceName.Empty + PrivateIpAllocationMethod = PrivateIpAddress.DynamicPrivateIp + PublicIp = None + Subnet = None + } /// Sets the name of the frontend IP configuration. [] @@ -63,48 +60,47 @@ type FrontendIpBuilder() = /// Sets the frontend's private IP allocation method. [] - member _.PrivateIpAllocationMethod(state: FrontendIpConfig, allocationMethod) = - { state with + member _.PrivateIpAllocationMethod(state: FrontendIpConfig, allocationMethod) = { + state with PrivateIpAllocationMethod = allocationMethod - } + } /// Sets the name of the frontend public IP. [] - member _.PublicIp(state: FrontendIpConfig, publicIp) = - { state with + member _.PublicIp(state: FrontendIpConfig, publicIp) = { + state with PublicIp = Some(Managed(Farmer.Arm.Network.publicIPAddresses.resourceId (ResourceName publicIp))) - } + } /// Links the frontend to an existing public IP. [] - member _.LinkToPublicIp(state: FrontendIpConfig, publicIp) = - { state with + member _.LinkToPublicIp(state: FrontendIpConfig, publicIp) = { + state with PublicIp = Some(Unmanaged publicIp) - } + } /// Links the frontend to a subnet in the same deployment. [] - member _.LinkToSubnet(state: FrontendIpConfig, subnetId) = - { state with + member _.LinkToSubnet(state: FrontendIpConfig, subnetId) = { + state with Subnet = Some(Managed subnetId) - } + } /// Links the frontend to an existing subnet. [] - member _.LinkToUnmanagedSubnet(state: FrontendIpConfig, subnetId) = - { state with + member _.LinkToUnmanagedSubnet(state: FrontendIpConfig, subnetId) = { + state with Subnet = Some(Unmanaged subnetId) - } + } let frontend = FrontendIpBuilder() -type BackendAddressPoolConfig = - { - Name: ResourceName - LoadBalancer: ResourceName - LoadBalancerBackendAddresses: System.Net.IPAddress list - VirtualNetwork: LinkedResource option - } +type BackendAddressPoolConfig = { + Name: ResourceName + LoadBalancer: ResourceName + LoadBalancerBackendAddresses: System.Net.IPAddress list + VirtualNetwork: LinkedResource option +} with interface IBuilder with member this.ResourceId = @@ -120,23 +116,21 @@ type BackendAddressPoolConfig = LoadBalancer = this.LoadBalancer LoadBalancerBackendAddresses = this.LoadBalancerBackendAddresses - |> List.mapi (fun idx addr -> - {| - Name = ResourceName $"addr{idx}" - VirtualNetwork = this.VirtualNetwork - IpAddress = addr - |}) + |> List.mapi (fun idx addr -> {| + Name = ResourceName $"addr{idx}" + VirtualNetwork = this.VirtualNetwork + IpAddress = addr + |}) } ] type BackendAddressPoolBuilder() = - member _.Yield _ = - { - Name = ResourceName.Empty - LoadBalancer = ResourceName.Empty - LoadBalancerBackendAddresses = [] - VirtualNetwork = None - } + member _.Yield _ = { + Name = ResourceName.Empty + LoadBalancer = ResourceName.Empty + LoadBalancerBackendAddresses = [] + VirtualNetwork = None + } /// Sets the name of the backend address pool. [] @@ -144,97 +138,94 @@ type BackendAddressPoolBuilder() = /// Sets the name of the load balancer for this pool. [] - member _.LoadBalancer(state: BackendAddressPoolConfig, lb) = - { state with + member _.LoadBalancer(state: BackendAddressPoolConfig, lb) = { + state with LoadBalancer = ResourceName lb - } + } /// Links to an existing vnet for addresses for this pool. [] - member _.LinkToVirtualNetwork(state: BackendAddressPoolConfig, vnet: string) = - { state with + member _.LinkToVirtualNetwork(state: BackendAddressPoolConfig, vnet: string) = { + state with VirtualNetwork = Some(Unmanaged(virtualNetworks.resourceId (ResourceName vnet))) - } + } - member _.LinkToVirtualNetwork(state: BackendAddressPoolConfig, vnet: ResourceId) = - { state with + member _.LinkToVirtualNetwork(state: BackendAddressPoolConfig, vnet: ResourceId) = { + state with VirtualNetwork = Some(Unmanaged vnet) - } + } - member _.LinkToVirtualNetwork(state: BackendAddressPoolConfig, vnetConfig: VirtualNetworkConfig) = - { state with + member _.LinkToVirtualNetwork(state: BackendAddressPoolConfig, vnetConfig: VirtualNetworkConfig) = { + state with VirtualNetwork = Some(Unmanaged(virtualNetworks.resourceId vnetConfig.Name)) - } + } /// Links to a vnet that is defined in this same deployment. [] - member _.VirtualNetwork(state: BackendAddressPoolConfig, vnet: string) = - { state with + member _.VirtualNetwork(state: BackendAddressPoolConfig, vnet: string) = { + state with VirtualNetwork = Some(Managed(virtualNetworks.resourceId (ResourceName vnet))) - } + } - member _.VirtualNetwork(state: BackendAddressPoolConfig, vnet: ResourceId) = - { state with + member _.VirtualNetwork(state: BackendAddressPoolConfig, vnet: ResourceId) = { + state with VirtualNetwork = Some(Managed vnet) - } + } - member _.VirtualNetwork(state: BackendAddressPoolConfig, vnetConfig: VirtualNetworkConfig) = - { state with + member _.VirtualNetwork(state: BackendAddressPoolConfig, vnetConfig: VirtualNetworkConfig) = { + state with VirtualNetwork = Some(Managed(virtualNetworks.resourceId vnetConfig.Name)) - } + } /// Adds IP addresses for this backend pool. [] - member _.IpAddresses(state: BackendAddressPoolConfig, backendAddresses: string list) = - { state with + member _.IpAddresses(state: BackendAddressPoolConfig, backendAddresses: string list) = { + state with LoadBalancerBackendAddresses = state.LoadBalancerBackendAddresses @ (backendAddresses |> List.map System.Net.IPAddress.Parse) - } + } - member _.IpAddresses(state: BackendAddressPoolConfig, backendAddresses: System.Net.IPAddress list) = - { state with + member _.IpAddresses(state: BackendAddressPoolConfig, backendAddresses: System.Net.IPAddress list) = { + state with LoadBalancerBackendAddresses = state.LoadBalancerBackendAddresses @ (backendAddresses) - } + } let backendAddressPool = BackendAddressPoolBuilder() -type ProbeConfig = - { - /// Name of the probe - Name: ResourceName - /// Protocol - TCP requires ACK for success, HTTP(S) require 200 OK for success - Protocol: LoadBalancerProbeProtocol option - /// Port 1-65535 - Port: uint16 option - /// Request path for HTTP(S) probes - RequestPath: string option - /// Interval between probes to the backend - IntervalInSeconds: int - /// Number of failed probes before removing from pool - NumberOfProbes: int - } - - static member BuildResource probe = - {| - Name = probe.Name - Protocol = probe.Protocol |> Option.defaultValue LoadBalancerProbeProtocol.TCP - Port = probe.Port |> Option.defaultValue 0us - RequestPath = probe.RequestPath |> Option.toObj - IntervalInSeconds = probe.IntervalInSeconds - NumberOfProbes = probe.NumberOfProbes - |} +type ProbeConfig = { + /// Name of the probe + Name: ResourceName + /// Protocol - TCP requires ACK for success, HTTP(S) require 200 OK for success + Protocol: LoadBalancerProbeProtocol option + /// Port 1-65535 + Port: uint16 option + /// Request path for HTTP(S) probes + RequestPath: string option + /// Interval between probes to the backend + IntervalInSeconds: int + /// Number of failed probes before removing from pool + NumberOfProbes: int +} with + + static member BuildResource probe = {| + Name = probe.Name + Protocol = probe.Protocol |> Option.defaultValue LoadBalancerProbeProtocol.TCP + Port = probe.Port |> Option.defaultValue 0us + RequestPath = probe.RequestPath |> Option.toObj + IntervalInSeconds = probe.IntervalInSeconds + NumberOfProbes = probe.NumberOfProbes + |} type ProbeBuilder() = - member _.Yield _ = - { - Name = ResourceName.Empty - Protocol = None - Port = None - RequestPath = None - IntervalInSeconds = 15 - NumberOfProbes = 2 - } + member _.Yield _ = { + Name = ResourceName.Empty + Protocol = None + Port = None + RequestPath = None + IntervalInSeconds = 15 + NumberOfProbes = 2 + } member _.Run(config: ProbeConfig) = match config.Port with @@ -248,8 +239,9 @@ type ProbeBuilder() = raiseFarmer "Set 'request_path' for HTTP or HTTPS probes." | _ -> () - { config with - Name = config.Name.IfEmpty $"{config.Protocol.Value}-{config.Port.Value}" + { + config with + Name = config.Name.IfEmpty $"{config.Protocol.Value}-{config.Port.Value}" } /// Sets the name of the connectivity probe. @@ -268,77 +260,74 @@ type ProbeBuilder() = /// Sets the request path for HTTP and HTTPS connection probes. [] - member _.RequestPath(state: ProbeConfig, requestPath: string) = - { state with + member _.RequestPath(state: ProbeConfig, requestPath: string) = { + state with RequestPath = Some requestPath - } + } /// Sets the interval in seconds between probes. [] - member _.Interval(state: ProbeConfig, interval: int) = - { state with + member _.Interval(state: ProbeConfig, interval: int) = { + state with IntervalInSeconds = interval - } + } - member _.Interval(state: ProbeConfig, interval: TimeSpan) = - { state with + member _.Interval(state: ProbeConfig, interval: TimeSpan) = { + state with IntervalInSeconds = int interval.TotalSeconds - } + } /// Sets the number of probes to consider this backend a failure and remove from the pool. [] - member _.NumberOfProbes(state: ProbeConfig, numberOfProbes) = - { state with + member _.NumberOfProbes(state: ProbeConfig, numberOfProbes) = { + state with NumberOfProbes = numberOfProbes - } + } let loadBalancerProbe = ProbeBuilder() -type LoadBalancingRuleConfig = - { - Name: ResourceName - FrontendIpConfiguration: ResourceName - BackendAddressPool: ResourceName - Probe: ResourceName option - FrontendPort: uint16 - BackendPort: uint16 - Protocol: TransmissionProtocol option // default "All" - IdleTimeoutMinutes: int option // default 4 minutes - LoadDistribution: Farmer.LoadBalancer.LoadDistributionPolicy - EnableTcpReset: bool option // default false - DisableOutboundSnat: bool option - } // default true - - static member BuildResource rule = - {| - Name = rule.Name - FrontendIpConfiguration = rule.FrontendIpConfiguration - BackendAddressPool = rule.BackendAddressPool - Probe = rule.Probe - FrontendPort = rule.FrontendPort - BackendPort = rule.BackendPort - Protocol = rule.Protocol - IdleTimeoutMinutes = rule.IdleTimeoutMinutes - LoadDistribution = rule.LoadDistribution - EnableTcpReset = rule.EnableTcpReset - DisableOutboundSnat = rule.DisableOutboundSnat - |} +type LoadBalancingRuleConfig = { + Name: ResourceName + FrontendIpConfiguration: ResourceName + BackendAddressPool: ResourceName + Probe: ResourceName option + FrontendPort: uint16 + BackendPort: uint16 + Protocol: TransmissionProtocol option // default "All" + IdleTimeoutMinutes: int option // default 4 minutes + LoadDistribution: Farmer.LoadBalancer.LoadDistributionPolicy + EnableTcpReset: bool option // default false + DisableOutboundSnat: bool option +} with // default true + + static member BuildResource rule = {| + Name = rule.Name + FrontendIpConfiguration = rule.FrontendIpConfiguration + BackendAddressPool = rule.BackendAddressPool + Probe = rule.Probe + FrontendPort = rule.FrontendPort + BackendPort = rule.BackendPort + Protocol = rule.Protocol + IdleTimeoutMinutes = rule.IdleTimeoutMinutes + LoadDistribution = rule.LoadDistribution + EnableTcpReset = rule.EnableTcpReset + DisableOutboundSnat = rule.DisableOutboundSnat + |} type LoadBalancingRuleBuilder() = - member _.Yield _ = - { - Name = ResourceName.Empty - FrontendIpConfiguration = ResourceName.Empty - BackendAddressPool = ResourceName.Empty - Probe = None - FrontendPort = 0us - BackendPort = 0us - Protocol = None // default "All" - IdleTimeoutMinutes = None // default 4 minutes - LoadDistribution = Farmer.LoadBalancer.LoadDistributionPolicy.Default - EnableTcpReset = None - DisableOutboundSnat = None // default true - } + member _.Yield _ = { + Name = ResourceName.Empty + FrontendIpConfiguration = ResourceName.Empty + BackendAddressPool = ResourceName.Empty + Probe = None + FrontendPort = 0us + BackendPort = 0us + Protocol = None // default "All" + IdleTimeoutMinutes = None // default 4 minutes + LoadDistribution = Farmer.LoadBalancer.LoadDistributionPolicy.Default + EnableTcpReset = None + DisableOutboundSnat = None // default true + } /// Sets the name of the load balancing rule. [] @@ -346,109 +335,109 @@ type LoadBalancingRuleBuilder() = /// Sets the name of the load balancing rule. [] - member _.FrontendIpConfig(state: LoadBalancingRuleConfig, frontendIpConfig: string) = - { state with + member _.FrontendIpConfig(state: LoadBalancingRuleConfig, frontendIpConfig: string) = { + state with FrontendIpConfiguration = ResourceName frontendIpConfig - } + } - member _.FrontendIpConfig(state: LoadBalancingRuleConfig, frontendIpConfig: FrontendIpConfig) = - { state with + member _.FrontendIpConfig(state: LoadBalancingRuleConfig, frontendIpConfig: FrontendIpConfig) = { + state with FrontendIpConfiguration = frontendIpConfig.Name - } + } /// Sets the name of the load balancing rule. [] - member _.BackendAddressPool(state: LoadBalancingRuleConfig, backendAddressPool: string) = - { state with + member _.BackendAddressPool(state: LoadBalancingRuleConfig, backendAddressPool: string) = { + state with BackendAddressPool = ResourceName backendAddressPool - } + } - member _.BackendAddressPool(state: LoadBalancingRuleConfig, backendAddressPool: BackendAddressPoolConfig) = - { state with + member _.BackendAddressPool(state: LoadBalancingRuleConfig, backendAddressPool: BackendAddressPoolConfig) = { + state with BackendAddressPool = backendAddressPool.Name - } + } /// Sets the probe to use for this load balancing rule. [] - member _.Probe(state: LoadBalancingRuleConfig, probe: string) = - { state with + member _.Probe(state: LoadBalancingRuleConfig, probe: string) = { + state with Probe = Some(ResourceName probe) - } + } member _.Probe(state: LoadBalancingRuleConfig, probe: ProbeConfig) = { state with Probe = Some probe.Name } /// Sets the frontend port for this rule. [] - member _.FrontendPort(state: LoadBalancingRuleConfig, frontendPort: uint16) = - { state with + member _.FrontendPort(state: LoadBalancingRuleConfig, frontendPort: uint16) = { + state with FrontendPort = frontendPort - } + } - member _.FrontendPort(state: LoadBalancingRuleConfig, frontendPort: int) = - { state with + member _.FrontendPort(state: LoadBalancingRuleConfig, frontendPort: int) = { + state with FrontendPort = uint16 frontendPort - } + } /// Sets the port on the backend pool for this rule. [] - member _.BackendPort(state: LoadBalancingRuleConfig, backendPort: uint16) = - { state with BackendPort = backendPort } + member _.BackendPort(state: LoadBalancingRuleConfig, backendPort: uint16) = { state with BackendPort = backendPort } - member _.BackendPort(state: LoadBalancingRuleConfig, backendPort: int) = - { state with + member _.BackendPort(state: LoadBalancingRuleConfig, backendPort: int) = { + state with BackendPort = uint16 backendPort - } + } /// Sets the load balancing protocol for this rule [] - member _.Protocol(state: LoadBalancingRuleConfig, protocol: TransmissionProtocol) = - { state with Protocol = Some protocol } + member _.Protocol(state: LoadBalancingRuleConfig, protocol: TransmissionProtocol) = { + state with + Protocol = Some protocol + } /// Sets the idle timeout in minutes for this rule, keeping it between 4 and 30 minutes. [] - member _.IdleTimeoutMinutes(state: LoadBalancingRuleConfig, idleTimeoutMin: int) = - { state with + member _.IdleTimeoutMinutes(state: LoadBalancingRuleConfig, idleTimeoutMin: int) = { + state with IdleTimeoutMinutes = if idleTimeoutMin <= 4 then 4 elif idleTimeoutMin > 30 then 30 else idleTimeoutMin |> Some - } + } /// Sets the load distribution policy for this rule [] - member _.LoadDistributionPolicy(state: LoadBalancingRuleConfig, loadDistributionPolicy: LoadDistributionPolicy) = - { state with + member _.LoadDistributionPolicy(state: LoadBalancingRuleConfig, loadDistributionPolicy: LoadDistributionPolicy) = { + state with LoadDistribution = loadDistributionPolicy - } + } /// If set, this allows the TCP connection to the load balancer to be reset by a timeout or connection termination. [] - member _.EnableTcpReset(state: LoadBalancingRuleConfig) = - { state with + member _.EnableTcpReset(state: LoadBalancingRuleConfig) = { + state with EnableTcpReset = Some true - } + } /// If set, this allows the backend pool to use this load balancer for outbound connections (disabled by default). [] - member _.EnableOutboundSnat(state: LoadBalancingRuleConfig) = - { state with + member _.EnableOutboundSnat(state: LoadBalancingRuleConfig) = { + state with DisableOutboundSnat = Some false - } + } let loadBalancingRule = LoadBalancingRuleBuilder() -type LoadBalancerConfig = - { - Name: ResourceName - Sku: LoadBalancerSku - FrontendIpConfigs: FrontendIpConfig list - BackendAddressPools: BackendAddressPoolConfig list - LoadBalancingRules: LoadBalancingRuleConfig list - Probes: ProbeConfig list - Dependencies: Set - Tags: Map - } +type LoadBalancerConfig = { + Name: ResourceName + Sku: LoadBalancerSku + FrontendIpConfigs: FrontendIpConfig list + BackendAddressPools: BackendAddressPoolConfig list + LoadBalancingRules: LoadBalancingRuleConfig list + Probes: ProbeConfig list + Dependencies: Set + Tags: Map +} with interface IBuilder with member this.ResourceId = loadBalancers.resourceId this.Name @@ -486,21 +475,19 @@ type LoadBalancerConfig = type LoadBalancerBuilder() = - member _.Yield _ = - { - Name = ResourceName.Empty - Sku = - { - Name = LoadBalancer.Sku.Basic - Tier = LoadBalancer.Tier.Regional - } - FrontendIpConfigs = [] - BackendAddressPools = [] - LoadBalancingRules = [] - Probes = [] - Dependencies = Set.empty - Tags = Map.empty - } + member _.Yield _ = { + Name = ResourceName.Empty + Sku = { + Name = LoadBalancer.Sku.Basic + Tier = LoadBalancer.Tier.Regional + } + FrontendIpConfigs = [] + BackendAddressPools = [] + LoadBalancingRules = [] + Probes = [] + Dependencies = Set.empty + Tags = Map.empty + } /// Sets the name of the load balancer. [] @@ -508,63 +495,63 @@ type LoadBalancerBuilder() = /// Sets the sku of the load balancer (default is 'basic'). [] - member _.Sku(state: LoadBalancerConfig, skuName) = - { state with + member _.Sku(state: LoadBalancerConfig, skuName) = { + state with Sku = { state.Sku with Name = skuName } - } + } /// Sets the tier of the load balancer (default is 'regional'). [] - member _.Tier(state: LoadBalancerConfig, skuTier) = - { state with + member _.Tier(state: LoadBalancerConfig, skuTier) = { + state with Sku = { state.Sku with Tier = skuTier } - } + } /// Add one or more frontend IP configs. [] - member _.AddFrontends(state: LoadBalancerConfig, frontends) = - { state with + member _.AddFrontends(state: LoadBalancerConfig, frontends) = { + state with FrontendIpConfigs = state.FrontendIpConfigs @ frontends - } + } /// Add one or more backend pools. [] - member _.AddBackendPools(state: LoadBalancerConfig, backends) = - { state with + member _.AddBackendPools(state: LoadBalancerConfig, backends) = { + state with BackendAddressPools = state.BackendAddressPools @ backends - } + } /// Add one or more load balancing rules. [] - member _.AddLoadBalancingRules(state: LoadBalancerConfig, rules) = - { state with + member _.AddLoadBalancingRules(state: LoadBalancerConfig, rules) = { + state with LoadBalancingRules = state.LoadBalancingRules @ rules - } + } /// Add one or more probes. [] - member _.AddProbes(state: LoadBalancerConfig, probes) = - { state with + member _.AddProbes(state: LoadBalancerConfig, probes) = { + state with Probes = state.Probes @ probes - } + } /// Add any additional dependencies that must be built before this - for backwards compatibility since this implements IDependable now. [] - member _.AddDependencies(state: LoadBalancerConfig, deps: ResourceId list) = - { state with + member _.AddDependencies(state: LoadBalancerConfig, deps: ResourceId list) = { + state with Dependencies = deps |> Set.ofList |> Set.union state.Dependencies - } + } interface IDependable with - member _.Add state newDeps = - { state with + member _.Add state newDeps = { + state with Dependencies = state.Dependencies + newDeps - } + } interface ITaggable with - member _.Add state tags = - { state with + member _.Add state tags = { + state with Tags = state.Tags |> Map.merge tags - } + } let loadBalancer = LoadBalancerBuilder() diff --git a/src/Farmer/Builders/Builders.LogAnalytics.fs b/src/Farmer/Builders/Builders.LogAnalytics.fs index 3c7ef7fa2..6537ba0cd 100644 --- a/src/Farmer/Builders/Builders.LogAnalytics.fs +++ b/src/Farmer/Builders/Builders.LogAnalytics.fs @@ -9,15 +9,14 @@ let private (|InBounds|OutOfBounds|) days = elif days > 730 then OutOfBounds days else InBounds days -type WorkspaceConfig = - { - Name: ResourceName - RetentionPeriod: int option - IngestionSupport: FeatureFlag option - QuerySupport: FeatureFlag option - DailyCap: int option - Tags: Map - } +type WorkspaceConfig = { + Name: ResourceName + RetentionPeriod: int option + IngestionSupport: FeatureFlag option + QuerySupport: FeatureFlag option + DailyCap: int option + Tags: Map +} with /// Gets the ARM expression path to the customer ID of this LogAnalytics instance. member this.CustomerId = LogAnalytics.getCustomerId this.Name @@ -28,36 +27,34 @@ type WorkspaceConfig = interface IBuilder with member this.ResourceId = workspaces.resourceId this.Name - member this.BuildResources location = - [ - { - Name = this.Name - Location = location - RetentionPeriod = this.RetentionPeriod - IngestionSupport = this.IngestionSupport - QuerySupport = this.QuerySupport - DailyCap = this.DailyCap - Tags = this.Tags - } - ] + member this.BuildResources location = [ + { + Name = this.Name + Location = location + RetentionPeriod = this.RetentionPeriod + IngestionSupport = this.IngestionSupport + QuerySupport = this.QuerySupport + DailyCap = this.DailyCap + Tags = this.Tags + } + ] type WorkspaceBuilder() = - member _.Yield _ = - { - Name = ResourceName.Empty - RetentionPeriod = None - DailyCap = None - IngestionSupport = None - QuerySupport = None - Tags = Map.empty - } + member _.Yield _ = { + Name = ResourceName.Empty + RetentionPeriod = None + DailyCap = None + IngestionSupport = None + QuerySupport = None + Tags = Map.empty + } member _.Run(state: WorkspaceConfig) = match state.RetentionPeriod with - | Some (OutOfBounds days) -> + | Some(OutOfBounds days) -> raiseFarmer $"The retention period must be between 30 and 730 days. It is currently {days}." | None - | Some (InBounds _) -> () + | Some(InBounds _) -> () state @@ -67,33 +64,33 @@ type WorkspaceBuilder() = /// The workspace data retention in days. Must be between 30 and 730 days. [] - member _.RetentionInDays(state: WorkspaceConfig, retentionInDays) = - { state with + member _.RetentionInDays(state: WorkspaceConfig, retentionInDays) = { + state with RetentionPeriod = Some retentionInDays - } + } /// Enables Log Analytics ingestion [] - member _.PublicNetworkAccessForIngestion(state: WorkspaceConfig) = - { state with + member _.PublicNetworkAccessForIngestion(state: WorkspaceConfig) = { + state with IngestionSupport = Some Enabled - } + } /// Enables Log Analytics querying. [] - member _.PublicNetworkAccessForQuery(state: WorkspaceConfig) = - { state with + member _.PublicNetworkAccessForQuery(state: WorkspaceConfig) = { + state with QuerySupport = Some Enabled - } + } /// Specifies the daily cap of ingested data. [] member _.DailyCap(state: WorkspaceConfig, cap) = { state with DailyCap = Some cap } interface ITaggable with - member _.Add state tags = - { state with + member _.Add state tags = { + state with Tags = state.Tags |> Map.merge tags - } + } let logAnalytics = WorkspaceBuilder() diff --git a/src/Farmer/Builders/Builders.LogicApps.fs b/src/Farmer/Builders/Builders.LogicApps.fs index e57f7ce46..531883d5d 100644 --- a/src/Farmer/Builders/Builders.LogicApps.fs +++ b/src/Farmer/Builders/Builders.LogicApps.fs @@ -10,54 +10,51 @@ type Definition = | FileDefinition of path: string | ValueDefinition of definition: string -type LogicAppConfig = - { - WorkflowName: ResourceName - Definition: Definition - Tags: Map - } +type LogicAppConfig = { + WorkflowName: ResourceName + Definition: Definition + Tags: Map +} with member this.LogicAppWorkflowName = workflows.resourceId(this.WorkflowName).Name interface IBuilder with member this.ResourceId = workflows.resourceId this.WorkflowName - member this.BuildResources location = - [ - { - Name = this.LogicAppWorkflowName - Location = location - Definition = - match this.Definition with - | FileDefinition path -> - let fileContent = File.ReadAllText(path) - JsonDocument.Parse(fileContent) - | ValueDefinition value -> JsonDocument.Parse(value) - Tags = this.Tags - } - ] + member this.BuildResources location = [ + { + Name = this.LogicAppWorkflowName + Location = location + Definition = + match this.Definition with + | FileDefinition path -> + let fileContent = File.ReadAllText(path) + JsonDocument.Parse(fileContent) + | ValueDefinition value -> JsonDocument.Parse(value) + Tags = this.Tags + } + ] type LogicAppBuilder() = - member _.Yield _ = - { - WorkflowName = ResourceName "logic-app-workflow" - Definition = ValueDefinition """{"name":"logic-app-workflow"}""" - Tags = Map.empty - } + member _.Yield _ = { + WorkflowName = ResourceName "logic-app-workflow" + Definition = ValueDefinition """{"name":"logic-app-workflow"}""" + Tags = Map.empty + } [] - member _.Name(state: LogicAppConfig, name) = - { state with + member _.Name(state: LogicAppConfig, name) = { + state with WorkflowName = ResourceName name - } + } [] member _.Definition(state: LogicAppConfig, definition: Definition) = { state with Definition = definition } interface ITaggable with - member _.Add state tags = - { state with + member _.Add state tags = { + state with Tags = state.Tags |> Map.merge tags - } + } let logicApp = LogicAppBuilder() diff --git a/src/Farmer/Builders/Builders.Maps.fs b/src/Farmer/Builders/Builders.Maps.fs index 8f94291fd..228eb2fe3 100644 --- a/src/Farmer/Builders/Builders.Maps.fs +++ b/src/Farmer/Builders/Builders.Maps.fs @@ -6,38 +6,35 @@ open Farmer.Maps open Farmer.Helpers open Farmer.Arm.Maps -type MapsConfig = - { - Name: ResourceName - Sku: Sku - Tags: Map - } +type MapsConfig = { + Name: ResourceName + Sku: Sku + Tags: Map +} with interface IBuilder with member this.ResourceId = accounts.resourceId this.Name - member this.BuildResources _ = - [ - { - Name = this.Name - Location = Location "global" - Sku = this.Sku - Tags = this.Tags - } - ] + member this.BuildResources _ = [ + { + Name = this.Name + Location = Location "global" + Sku = this.Sku + Tags = this.Tags + } + ] type MapsBuilder() = - member _.Yield _ = - { - Name = ResourceName.Empty - Sku = S0 - Tags = Map.empty - } + member _.Yield _ = { + Name = ResourceName.Empty + Sku = S0 + Tags = Map.empty + } - member _.Run(state: MapsConfig) = - { state with + member _.Run(state: MapsConfig) = { + state with Name = state.Name |> sanitiseMaps |> ResourceName - } + } /// Sets the name of the Azure Maps instance. [] @@ -50,9 +47,9 @@ type MapsBuilder() = member _.Sku(state: MapsConfig, sku) = { state with Sku = sku } interface ITaggable with - member _.Add state tags = - { state with + member _.Add state tags = { + state with Tags = state.Tags |> Map.merge tags - } + } let maps = MapsBuilder() diff --git a/src/Farmer/Builders/Builders.NatGateway.fs b/src/Farmer/Builders/Builders.NatGateway.fs index e5d00c303..a137b09ed 100644 --- a/src/Farmer/Builders/Builders.NatGateway.fs +++ b/src/Farmer/Builders/Builders.NatGateway.fs @@ -5,48 +5,44 @@ open Farmer open Farmer.Arm.Network open Farmer.PublicIpAddress -type NatGatewayConfig = - { - Name: ResourceName - IdleTimeout: int - Tags: Map - } +type NatGatewayConfig = { + Name: ResourceName + IdleTimeout: int + Tags: Map +} with interface IBuilder with member this.ResourceId = natGateways.resourceId this.Name - member this.BuildResources location = - [ - // Currently just generate with a single public IP. - { - PublicIpAddress.Name = ResourceName $"{this.Name.Value}-publicip-1" - AvailabilityZone = None - Location = location - Sku = Sku.Standard - AllocationMethod = AllocationMethod.Static - DomainNameLabel = None - Tags = this.Tags - } - { - NatGateway.Name = this.Name - Location = location - PublicIpAddresses = - [ - LinkedResource.Managed(publicIPAddresses.resourceId $"{this.Name.Value}-publicip-1") - ] - PublicIpPrefixes = [] - IdleTimeout = this.IdleTimeout - Tags = this.Tags - } - ] + member this.BuildResources location = [ + // Currently just generate with a single public IP. + { + PublicIpAddress.Name = ResourceName $"{this.Name.Value}-publicip-1" + AvailabilityZone = None + Location = location + Sku = Sku.Standard + AllocationMethod = AllocationMethod.Static + DomainNameLabel = None + Tags = this.Tags + } + { + NatGateway.Name = this.Name + Location = location + PublicIpAddresses = [ + LinkedResource.Managed(publicIPAddresses.resourceId $"{this.Name.Value}-publicip-1") + ] + PublicIpPrefixes = [] + IdleTimeout = this.IdleTimeout + Tags = this.Tags + } + ] type NatGatewayBuilder() = - member _.Yield _ = - { - Name = ResourceName.Empty - IdleTimeout = 4 - Tags = Map.empty - } + member _.Yield _ = { + Name = ResourceName.Empty + IdleTimeout = 4 + Tags = Map.empty + } [] member _.Name(state: NatGatewayConfig, name: string) = { state with Name = ResourceName name } diff --git a/src/Farmer/Builders/Builders.NetworkSecurityGroup.fs b/src/Farmer/Builders/Builders.NetworkSecurityGroup.fs index 45faf9668..f9328d477 100644 --- a/src/Farmer/Builders/Builders.NetworkSecurityGroup.fs +++ b/src/Farmer/Builders/Builders.NetworkSecurityGroup.fs @@ -8,28 +8,26 @@ open Farmer.NetworkSecurity open System.Net /// Network access policy -type SecurityRuleConfig = - { - Name: ResourceName - Description: string option - Services: NetworkService list - Sources: (NetworkProtocol * Endpoint * Port) list - Destinations: Endpoint list - Operation: Operation - Direction: TrafficDirection - } +type SecurityRuleConfig = { + Name: ResourceName + Description: string option + Services: NetworkService list + Sources: (NetworkProtocol * Endpoint * Port) list + Destinations: Endpoint list + Operation: Operation + Direction: TrafficDirection +} type SecurityRuleBuilder() = - member _.Yield _ = - { - Name = ResourceName.Empty - Description = None - Services = [] - Sources = [] - Destinations = [] - Operation = Allow - Direction = TrafficDirection.Inbound - } + member _.Yield _ = { + Name = ResourceName.Empty + Description = None + Services = [] + Sources = [] + Destinations = [] + Operation = Allow + Direction = TrafficDirection.Inbound + } /// Sets the name of the security rule [] @@ -37,93 +35,92 @@ type SecurityRuleBuilder() = /// Sets the description of the security rule [] - member _.Description(state: SecurityRuleConfig, description) = - { state with + member _.Description(state: SecurityRuleConfig, description) = { + state with Description = Some description - } + } /// Sets the service or services protected by this rule. [] member _.Services(state: SecurityRuleConfig, services) = { state with Services = services } member this.Services(state: SecurityRuleConfig, services) = - let services = - [ - for (name, port) in services do - NetworkService(name, Port(uint16 port)) - ] + let services = [ + for (name, port) in services do + NetworkService(name, Port(uint16 port)) + ] this.Services(state, services) /// Sets the source endpoint that is matched in this rule [] - member _.AddSource(state: SecurityRuleConfig, source) = - { state with + member _.AddSource(state: SecurityRuleConfig, source) = { + state with Sources = source :: state.Sources - } + } /// Sets the rule to match on any source endpoint. [] - member _.AddSourceAny(state: SecurityRuleConfig, protocol) = - { state with + member _.AddSourceAny(state: SecurityRuleConfig, protocol) = { + state with Sources = (protocol, AnyEndpoint, AnyPort) :: state.Sources - } + } /// Sets the rule to match on a tagged source endpoint, such as 'Internet'. [] - member _.AddSourceTag(state: SecurityRuleConfig, protocol, tag) = - { state with + member _.AddSourceTag(state: SecurityRuleConfig, protocol, tag) = { + state with Sources = (protocol, Tag tag, AnyPort) :: state.Sources - } + } /// Sets the rule to match on a source address. [] - member _.AddSourceAddress(state: SecurityRuleConfig, protocol, sourceAddress: string) = - { state with + member _.AddSourceAddress(state: SecurityRuleConfig, protocol, sourceAddress: string) = { + state with Sources = (protocol, Host(IPAddress.Parse sourceAddress), AnyPort) :: state.Sources - } + } /// Sets the rule to match on a source network. [] - member _.AddSourceNetwork(state: SecurityRuleConfig, protocol, sourceNetwork) = - { state with + member _.AddSourceNetwork(state: SecurityRuleConfig, protocol, sourceNetwork) = { + state with Sources = (protocol, Network(IPAddressCidr.parse sourceNetwork), AnyPort) :: state.Sources - } + } /// Sets the destination endpoint that is matched in this rule [] - member _.AddDestination(state: SecurityRuleConfig, dest) = - { state with + member _.AddDestination(state: SecurityRuleConfig, dest) = { + state with Destinations = dest :: state.Destinations - } + } /// Sets the rule to match on any destination endpoint. [] - member _.AddDestinationAny(state: SecurityRuleConfig) = - { state with + member _.AddDestinationAny(state: SecurityRuleConfig) = { + state with Destinations = AnyEndpoint :: state.Destinations - } + } /// Sets the rule to match on a tagged destination endpoint, such as 'Internet'. [] - member _.AddDestinationTag(state: SecurityRuleConfig, tag) = - { state with + member _.AddDestinationTag(state: SecurityRuleConfig, tag) = { + state with Destinations = Tag tag :: state.Destinations - } + } /// Sets the rule to match on a destination address. [] - member _.AddDestinationAddress(state: SecurityRuleConfig, destAddress: string) = - { state with + member _.AddDestinationAddress(state: SecurityRuleConfig, destAddress: string) = { + state with Destinations = Host(IPAddress.Parse destAddress) :: state.Destinations - } + } /// Sets the rule to match on a destination network. [] - member _.AddDestinationNetwork(state: SecurityRuleConfig, destNetwork) = - { state with + member _.AddDestinationNetwork(state: SecurityRuleConfig, destNetwork) = { + state with Destinations = Network(IPAddressCidr.parse destNetwork) :: state.Destinations - } + } /// Sets the rule to allow this traffic (default value). [] @@ -139,73 +136,66 @@ type SecurityRuleBuilder() = let securityRule = SecurityRuleBuilder() -let internal buildNsgRule (nsgName: ResourceName) (rule: SecurityRuleConfig) (priority: int) = - { - Name = rule.Name - Description = None - SecurityGroup = nsgName - Protocol = - let protocols = rule.Sources |> List.map (fun (protocol, _, _) -> protocol) |> Set - - if protocols.Count > 1 then - AnyProtocol - else - protocols |> Seq.head - SourcePorts = rule.Sources |> List.map (fun (_, _, sourcePort) -> sourcePort) |> Set - SourceAddresses = - rule.Sources - |> List.map (fun (_, sourceAddress, _) -> sourceAddress) - |> List.distinct - DestinationPorts = - match rule.Services with - | [] -> Set [ AnyPort ] - | services -> services |> List.map (fun (NetworkService (_, port)) -> port) |> Set - DestinationAddresses = rule.Destinations - Access = rule.Operation - Direction = rule.Direction - Priority = priority - } - -type NsgConfig = - { - Name: ResourceName - SecurityRules: SecurityRuleConfig list - Tags: Map - InitialRulePriority: int - PriorityIncrementor: int - } +let internal buildNsgRule (nsgName: ResourceName) (rule: SecurityRuleConfig) (priority: int) = { + Name = rule.Name + Description = None + SecurityGroup = nsgName + Protocol = + let protocols = rule.Sources |> List.map (fun (protocol, _, _) -> protocol) |> Set + + if protocols.Count > 1 then + AnyProtocol + else + protocols |> Seq.head + SourcePorts = rule.Sources |> List.map (fun (_, _, sourcePort) -> sourcePort) |> Set + SourceAddresses = + rule.Sources + |> List.map (fun (_, sourceAddress, _) -> sourceAddress) + |> List.distinct + DestinationPorts = + match rule.Services with + | [] -> Set [ AnyPort ] + | services -> services |> List.map (fun (NetworkService(_, port)) -> port) |> Set + DestinationAddresses = rule.Destinations + Access = rule.Operation + Direction = rule.Direction + Priority = priority +} + +type NsgConfig = { + Name: ResourceName + SecurityRules: SecurityRuleConfig list + Tags: Map + InitialRulePriority: int + PriorityIncrementor: int +} with interface IBuilder with member this.ResourceId = networkSecurityGroups.resourceId this.Name - member this.BuildResources location = - [ - { - Name = this.Name - Location = location - SecurityRules = - seq { - // Policy Rules - for priority, rule in List.indexed this.SecurityRules do - buildNsgRule - this.Name - rule - (priority * this.PriorityIncrementor + this.InitialRulePriority) - } - |> List.ofSeq - Tags = this.Tags - } - ] + member this.BuildResources location = [ + { + Name = this.Name + Location = location + SecurityRules = + seq { + // Policy Rules + for priority, rule in List.indexed this.SecurityRules do + buildNsgRule this.Name rule (priority * this.PriorityIncrementor + this.InitialRulePriority) + } + |> List.ofSeq + Tags = this.Tags + } + ] type NsgBuilder() = - member _.Yield _ = - { - Name = ResourceName.Empty - SecurityRules = [] - Tags = Map.empty - InitialRulePriority = 100 - PriorityIncrementor = 100 - } + member _.Yield _ = { + Name = ResourceName.Empty + SecurityRules = [] + Tags = Map.empty + InitialRulePriority = 100 + PriorityIncrementor = 100 + } /// Sets the name of the network security group [] @@ -213,29 +203,29 @@ type NsgBuilder() = /// Adds rules to this NSG. [] - member _.AddSecurityRules(state: NsgConfig, rules) = - { state with + member _.AddSecurityRules(state: NsgConfig, rules) = { + state with SecurityRules = state.SecurityRules @ rules - } + } /// Initial rule priority sets the priority of the first rule. [] - member _.InitialRulePriority(state: NsgConfig, initialPriority) = - { state with + member _.InitialRulePriority(state: NsgConfig, initialPriority) = { + state with InitialRulePriority = initialPriority - } + } /// First rule is priority 100. After that, this sets how much priority is increased per each rule. Default 100. [] - member _.PriorityIncrementor(state: NsgConfig, priority_incr) = - { state with + member _.PriorityIncrementor(state: NsgConfig, priority_incr) = { + state with PriorityIncrementor = priority_incr - } + } interface ITaggable with - member _.Add state tags = - { state with + member _.Add state tags = { + state with Tags = state.Tags |> Map.merge tags - } + } let nsg = NsgBuilder() diff --git a/src/Farmer/Builders/Builders.OperationsManagement.fs b/src/Farmer/Builders/Builders.OperationsManagement.fs index 9cdaa3c30..c01d2e9ba 100644 --- a/src/Farmer/Builders/Builders.OperationsManagement.fs +++ b/src/Farmer/Builders/Builders.OperationsManagement.fs @@ -6,123 +6,110 @@ open Farmer.Arm.OperationsManagement /// OMS = Operations Management Solution -type OMSProperties = - { - Workspace: IBuilder option - ContainedResources: IBuilder list - ReferencedResources: IBuilder list +type OMSProperties = { + Workspace: IBuilder option + ContainedResources: IBuilder list + ReferencedResources: IBuilder list +} with + + static member Empty = { + Workspace = None + ContainedResources = [] + ReferencedResources = [] } - static member Empty = - { - Workspace = None - ContainedResources = [] - ReferencedResources = [] - } +type OMSPlan = { + Name: string + Publisher: string + Product: string +} with -type OMSPlan = - { - Name: string - Publisher: string - Product: string + static member Empty = { + Name = "" + Publisher = "" + Product = "" } - static member Empty = - { - Name = "" - Publisher = "" - Product = "" - } - -type OMSConfig = - { - Name: ResourceName - Properties: OMSProperties - Plan: OMSPlan - Tags: Map - } +type OMSConfig = { + Name: ResourceName + Properties: OMSProperties + Plan: OMSPlan + Tags: Map +} with interface IBuilder with member this.ResourceId = oms.resourceId this.Name - member this.BuildResources location = - [ - match this.Properties.Workspace with - | Some workspace -> - { - Name = this.Name - Location = location - Plan = - {| - Name = this.Plan.Name - Product = this.Plan.Product - Publisher = this.Plan.Publisher - |} - Properties = - {| - WorkspaceResourceId = workspace.ResourceId - ContainedResources = - this.Properties.ContainedResources |> List.map (fun cr -> cr.ResourceId) - ReferencedResources = - this.Properties.ReferencedResources |> List.map (fun rr -> rr.ResourceId) - |} - Tags = this.Tags - } - | None -> () - ] + member this.BuildResources location = [ + match this.Properties.Workspace with + | Some workspace -> { + Name = this.Name + Location = location + Plan = {| + Name = this.Plan.Name + Product = this.Plan.Product + Publisher = this.Plan.Publisher + |} + Properties = {| + WorkspaceResourceId = workspace.ResourceId + ContainedResources = this.Properties.ContainedResources |> List.map (fun cr -> cr.ResourceId) + ReferencedResources = this.Properties.ReferencedResources |> List.map (fun rr -> rr.ResourceId) + |} + Tags = this.Tags + } + | None -> () + ] type OMSPropertiesBuilder() = - member _.Yield _ = - { - Workspace = None - ContainedResources = [] - ReferencedResources = [] - } + member _.Yield _ = { + Workspace = None + ContainedResources = [] + ReferencedResources = [] + } /// Sets the workspace resource id of the OMS Properties [] - member _.WorkspaceResourceId(state: OMSProperties, workspace) = - { state with + member _.WorkspaceResourceId(state: OMSProperties, workspace) = { + state with Workspace = Some workspace - } + } /// Adds a contained resource. [] - member _.AddContainedResource(state: OMSProperties, contained) = - { state with + member _.AddContainedResource(state: OMSProperties, contained) = { + state with ContainedResources = contained :: state.ContainedResources - } + } /// Adds a collection of contained resources. [] - member _.AddContainedResources(state: OMSProperties, contained) = - { state with + member _.AddContainedResources(state: OMSProperties, contained) = { + state with ContainedResources = contained @ state.ContainedResources - } + } /// Adds a referenced resource. [] - member _.AddReferencedResource(state: OMSProperties, referenced) = - { state with + member _.AddReferencedResource(state: OMSProperties, referenced) = { + state with ReferencedResources = referenced :: state.ReferencedResources - } + } /// Adds a collection of referenced resources. [] - member _.AddReferencedResources(state: OMSProperties, referenced) = - { state with + member _.AddReferencedResources(state: OMSProperties, referenced) = { + state with ReferencedResources = referenced @ state.ReferencedResources - } + } let omsProperties = OMSPropertiesBuilder() type OMSPlanBuilder() = - member _.Yield _ = - { - Name = "" - Publisher = "Microsoft" - Product = "" - } + member _.Yield _ = { + Name = "" + Publisher = "Microsoft" + Product = "" + } /// Sets the name of the OMS Plan [] @@ -139,13 +126,12 @@ type OMSPlanBuilder() = let omsPlan = OMSPlanBuilder() type OMSBuilder() = - member _.Yield _ = - { - Name = ResourceName.Empty - Plan = OMSPlan.Empty - Properties = OMSProperties.Empty - Tags = Map.empty - } + member _.Yield _ = { + Name = ResourceName.Empty + Plan = OMSPlan.Empty + Properties = OMSProperties.Empty + Tags = Map.empty + } /// Sets the name of the OMS [] @@ -160,9 +146,9 @@ type OMSBuilder() = member _.Properties(state: OMSConfig, properties: OMSProperties) = { state with Properties = properties } interface ITaggable with - member _.Add state tags = - { state with + member _.Add state tags = { + state with Tags = state.Tags |> Map.merge tags - } + } let oms = OMSBuilder() diff --git a/src/Farmer/Builders/Builders.PostgreSQL.fs b/src/Farmer/Builders/Builders.PostgreSQL.fs index d76c0fc58..d17717135 100644 --- a/src/Farmer/Builders/Builders.PostgreSQL.fs +++ b/src/Farmer/Builders/Builders.PostgreSQL.fs @@ -10,84 +10,88 @@ open Arm.DBforPostgreSQL open Servers -type PostgreSQLDbConfig = - { - Name: ResourceName - DbCollation: string option - DbCharset: string option - } - - -type PostgreSQLConfig = - { - Name: ResourceName - AdministratorCredentials: {| UserName: string - Password: SecureParameter |} - Version: Version - GeoRedundantBackup: bool - StorageAutogrow: bool - BackupRetention: int - StorageSize: int - Capacity: int - Tier: Sku - Databases: PostgreSQLDbConfig list - FirewallRules: {| Name: ResourceName - Start: IPAddress - End: IPAddress |} list - VirtualNetworkRules: {| Name: ResourceName - VirtualNetworkSubnetId: ResourceId |} list - Tags: Map - } +type PostgreSQLDbConfig = { + Name: ResourceName + DbCollation: string option + DbCharset: string option +} + + +type PostgreSQLConfig = { + Name: ResourceName + AdministratorCredentials: {| + UserName: string + Password: SecureParameter + |} + Version: Version + GeoRedundantBackup: bool + StorageAutogrow: bool + BackupRetention: int + StorageSize: int + Capacity: int + Tier: Sku + Databases: PostgreSQLDbConfig list + FirewallRules: + {| + Name: ResourceName + Start: IPAddress + End: IPAddress + |} list + VirtualNetworkRules: + {| + Name: ResourceName + VirtualNetworkSubnetId: ResourceId + |} list + Tags: Map +} with interface IBuilder with member this.ResourceId = databases.resourceId this.Name - member this.BuildResources location = - [ + member this.BuildResources location = [ + { + Name = this.Name + Location = location + Credentials = {| + Username = this.AdministratorCredentials.UserName + Password = this.AdministratorCredentials.Password + |} + Version = this.Version + StorageSize = this.StorageSize * 1024 / 1 + Capacity = this.Capacity + Tier = this.Tier + Family = PostgreSQLFamily.Gen5 + GeoRedundantBackup = FeatureFlag.ofBool this.GeoRedundantBackup + StorageAutoGrow = FeatureFlag.ofBool this.StorageAutogrow + BackupRetention = this.BackupRetention + Tags = this.Tags + } + + for database in this.Databases do + { + Name = database.Name + Server = this.Name + Collation = database.DbCollation |> Option.defaultValue "English_United States.1252" + Charset = database.DbCharset |> Option.defaultValue "UTF8" + } + + for rule in this.FirewallRules do { - Name = this.Name + Name = rule.Name + Start = rule.Start + End = rule.End + Server = this.Name Location = location - Credentials = - {| - Username = this.AdministratorCredentials.UserName - Password = this.AdministratorCredentials.Password - |} - Version = this.Version - StorageSize = this.StorageSize * 1024 / 1 - Capacity = this.Capacity - Tier = this.Tier - Family = PostgreSQLFamily.Gen5 - GeoRedundantBackup = FeatureFlag.ofBool this.GeoRedundantBackup - StorageAutoGrow = FeatureFlag.ofBool this.StorageAutogrow - BackupRetention = this.BackupRetention - Tags = this.Tags } - for database in this.Databases do - { - Name = database.Name - Server = this.Name - Collation = database.DbCollation |> Option.defaultValue "English_United States.1252" - Charset = database.DbCharset |> Option.defaultValue "UTF8" - } - - for rule in this.FirewallRules do - { - Name = rule.Name - Start = rule.Start - End = rule.End - Server = this.Name - Location = location - } - - for rule in this.VirtualNetworkRules do - { - Name = rule.Name - VirtualNetworkSubnetId = rule.VirtualNetworkSubnetId - Server = this.Name - Location = location - } - ] + for rule in this.VirtualNetworkRules do + { + Name = rule.Name + VirtualNetworkSubnetId = rule.VirtualNetworkSubnetId + Server = this.Name + Location = location + } + ] /// Generates an expression for the fully qualified domain name for reaching the postgres server. member this.FullyQualifiedDomainName = @@ -113,16 +117,15 @@ module private Helpers = [] module Validate = - let reservedUsernames = - [ - "azure_pg_admin" - "admin" - "root" - "azure_superuser" - "administrator" - "root" - "guest" - ] + let reservedUsernames = [ + "azure_pg_admin" + "admin" + "root" + "azure_superuser" + "administrator" + "root" + "guest" + ] let username (paramName: string) (candidate: string) = if String.IsNullOrWhiteSpace candidate then @@ -197,12 +200,11 @@ module Validate = raiseFarmer $"Capacity must be a power of two, was {capacity}" type PostgreSQLDbBuilder() = - member _.Yield _ : PostgreSQLDbConfig = - { - Name = ResourceName "" - DbCharset = None - DbCollation = None - } + member _.Yield _ : PostgreSQLDbConfig = { + Name = ResourceName "" + DbCharset = None + DbCollation = None + } member _.Run(state: PostgreSQLDbConfig) = if state.Name = ResourceName.Empty then @@ -222,8 +224,9 @@ type PostgreSQLDbBuilder() = if String.IsNullOrWhiteSpace collation then raiseFarmer "collation must have a value" - { state with - DbCollation = Some collation + { + state with + DbCollation = Some collation } [] @@ -236,26 +239,24 @@ type PostgreSQLDbBuilder() = let postgreSQLDb = PostgreSQLDbBuilder() type PostgreSQLBuilder() = - member _.Yield _ : PostgreSQLConfig = - { - Name = ResourceName "" - AdministratorCredentials = - {| - UserName = "" - Password = SecureParameter "" - |} - Version = VS_11 - GeoRedundantBackup = false - StorageAutogrow = true - BackupRetention = Validate.minBackupRetention - StorageSize = Validate.minStorageSize - Capacity = 2 - Tier = Basic - Databases = [] - FirewallRules = [] - VirtualNetworkRules = [] - Tags = Map.empty - } + member _.Yield _ : PostgreSQLConfig = { + Name = ResourceName "" + AdministratorCredentials = {| + UserName = "" + Password = SecureParameter "" + |} + Version = VS_11 + GeoRedundantBackup = false + StorageAutogrow = true + BackupRetention = Validate.minBackupRetention + StorageSize = Validate.minStorageSize + Capacity = 2 + Tier = Basic + Databases = [] + FirewallRules = [] + VirtualNetworkRules = [] + Tags = Map.empty + } member _.Run state : PostgreSQLConfig = state.Name.Value |> Validate.servername @@ -263,10 +264,11 @@ type PostgreSQLBuilder() = state.AdministratorCredentials.UserName |> Validate.username "AdministratorCredentials.UserName" - { state with - AdministratorCredentials = - {| state.AdministratorCredentials with - Password = SecureParameter $"password-for-{state.Name.Value}" + { + state with + AdministratorCredentials = {| + state.AdministratorCredentials with + Password = SecureParameter $"password-for-{state.Name.Value}" |} } @@ -285,20 +287,21 @@ type PostgreSQLBuilder() = member _.AdminUsername(state: PostgreSQLConfig, adminUsername: string) = Validate.username "adminUserName" adminUsername - { state with - Databases = List.rev state.Databases - AdministratorCredentials = - {| state.AdministratorCredentials with - UserName = adminUsername + { + state with + Databases = List.rev state.Databases + AdministratorCredentials = {| + state.AdministratorCredentials with + UserName = adminUsername |} } /// Sets geo-redundant backup [] - member _.SetGeoRedundantBackup(state: PostgreSQLConfig, enabled: bool) = - { state with + member _.SetGeoRedundantBackup(state: PostgreSQLConfig, enabled: bool) = { + state with GeoRedundantBackup = enabled - } + } /// Enables geo-redundant backup [] @@ -311,8 +314,7 @@ type PostgreSQLBuilder() = /// Sets storage autogrow [] - member _.SetStorageAutogrow(state: PostgreSQLConfig, enabled: bool) = - { state with StorageAutogrow = enabled } + member _.SetStorageAutogrow(state: PostgreSQLConfig, enabled: bool) = { state with StorageAutogrow = enabled } /// Enables storage autogrow [] @@ -333,8 +335,9 @@ type PostgreSQLBuilder() = member _.SetBackupRetention(state: PostgreSQLConfig, retention: int) = Validate.backupRetention retention - { state with - BackupRetention = retention + { + state with + BackupRetention = retention } /// Sets the PostgreSQl server version @@ -353,10 +356,10 @@ type PostgreSQLBuilder() = /// Adds a new database to the server, either by specifying the name of the database or providing a PostgreSQLDbConfig [] - member _.AddDatabase(state: PostgreSQLConfig, database) = - { state with + member _.AddDatabase(state: PostgreSQLConfig, database) = { + state with Databases = database :: state.Databases - } + } member this.AddDatabase(state: PostgreSQLConfig, dbName: string) = let db = postgreSQLDb { name dbName } @@ -364,8 +367,8 @@ type PostgreSQLBuilder() = /// Adds a custom firewall rule given a name, start and end IP address range. [] - member _.AddFirewallWall(state: PostgreSQLConfig, name, startRange: string, endRange: string) = - { state with + member _.AddFirewallWall(state: PostgreSQLConfig, name, startRange: string, endRange: string) = { + state with FirewallRules = {| Name = ResourceName name @@ -373,22 +376,22 @@ type PostgreSQLBuilder() = End = IPAddress.Parse endRange |} :: state.FirewallRules - } + } /// Adds a custom firewall rules given a name, start and end IP address range. [] member _.AddFirewallRules(state: PostgreSQLConfig, listOfRules: (string * string * string) list) = let newRules = listOfRules - |> List.map (fun (name, startRange, endRange) -> - {| - Name = ResourceName name - Start = IPAddress.Parse startRange - End = IPAddress.Parse endRange - |}) + |> List.map (fun (name, startRange, endRange) -> {| + Name = ResourceName name + Start = IPAddress.Parse startRange + End = IPAddress.Parse endRange + |}) - { state with - FirewallRules = newRules @ state.FirewallRules + { + state with + FirewallRules = newRules @ state.FirewallRules } /// Adds a firewall rule that enables access to other Azure services. @@ -397,36 +400,36 @@ type PostgreSQLBuilder() = this.AddFirewallWall(state, "allow-azure-services", "0.0.0.0", "0.0.0.0") interface ITaggable with - member _.Add state tags = - { state with + member _.Add state tags = { + state with Tags = state.Tags |> Map.merge tags - } + } /// Adds a custom vnet rule given a name and a virtualNetworkSubnetId. [] - member _.AddVnetRule(state: PostgreSQLConfig, name, virtualNetworkSubnetId: ResourceId) = - { state with + member _.AddVnetRule(state: PostgreSQLConfig, name, virtualNetworkSubnetId: ResourceId) = { + state with VirtualNetworkRules = {| Name = ResourceName name VirtualNetworkSubnetId = virtualNetworkSubnetId |} :: state.VirtualNetworkRules - } + } /// Adds a custom firewall rules given a name and a virtualNetworkSubnetId. [] member _.AddVnetRules(state: PostgreSQLConfig, listOfRules: (string * ResourceId) list) = let newRules = listOfRules - |> List.map (fun (name, virtualNetworkSubnetId) -> - {| - Name = ResourceName name - VirtualNetworkSubnetId = virtualNetworkSubnetId - |}) + |> List.map (fun (name, virtualNetworkSubnetId) -> {| + Name = ResourceName name + VirtualNetworkSubnetId = virtualNetworkSubnetId + |}) - { state with - VirtualNetworkRules = newRules @ state.VirtualNetworkRules + { + state with + VirtualNetworkRules = newRules @ state.VirtualNetworkRules } let postgreSQL = PostgreSQLBuilder() diff --git a/src/Farmer/Builders/Builders.PrivateEndpoint.fs b/src/Farmer/Builders/Builders.PrivateEndpoint.fs index 2ceb0b949..665999a0a 100644 --- a/src/Farmer/Builders/Builders.PrivateEndpoint.fs +++ b/src/Farmer/Builders/Builders.PrivateEndpoint.fs @@ -5,34 +5,31 @@ open Farmer open Farmer.Arm open Farmer.Arm.Network -type PrivateEndpointConfig = - { - Name: ResourceName - Subnet: SubnetReference option - Resource: LinkedResource option - CustomNetworkInterfaceName: string option - GroupIds: string list - } +type PrivateEndpointConfig = { + Name: ResourceName + Subnet: SubnetReference option + Resource: LinkedResource option + CustomNetworkInterfaceName: string option + GroupIds: string list +} with interface IBuilder with member this.ResourceId = privateEndpoints.resourceId this.Name - member this.BuildResources location = - [ - match this.Subnet, this.Resource with - | Some subnet, Some resource -> - { - PrivateEndpoint.Name = this.Name - Location = location - Subnet = subnet - Resource = resource - CustomNetworkInterfaceName = this.CustomNetworkInterfaceName - GroupIds = [] - } - | _ -> - raiseFarmer - $"Subnet and Resource must be specified. Subnet: '{this.Subnet}' Resource: '{this.Resource}'" - ] + member this.BuildResources location = [ + match this.Subnet, this.Resource with + | Some subnet, Some resource -> { + PrivateEndpoint.Name = this.Name + Location = location + Subnet = subnet + Resource = resource + CustomNetworkInterfaceName = this.CustomNetworkInterfaceName + GroupIds = [] + } + | _ -> + raiseFarmer + $"Subnet and Resource must be specified. Subnet: '{this.Subnet}' Resource: '{this.Resource}'" + ] /// If a CustomNetworkInterfaceName is set via 'custom_nic_name', this returns the private IP. member this.CustomNicEndpointIP(idx: int) : ArmExpression option = @@ -46,49 +43,48 @@ type PrivateEndpointConfig = member this.CustomNicFirstEndpointIP = this.CustomNicEndpointIP 0 type PrivateEndpointBuilder() = - member _.Yield _ = - { - Name = ResourceName.Empty - Subnet = None - Resource = None - CustomNetworkInterfaceName = None - GroupIds = [] - } + member _.Yield _ = { + Name = ResourceName.Empty + Subnet = None + Resource = None + CustomNetworkInterfaceName = None + GroupIds = [] + } [] member _.Name(state: PrivateEndpointConfig, name: string) = { state with Name = ResourceName name } [] - member _.Subnet(state: PrivateEndpointConfig, subnetReference: SubnetReference) = - { state with + member _.Subnet(state: PrivateEndpointConfig, subnetReference: SubnetReference) = { + state with Subnet = Some subnetReference - } + } [] - member _.LinkToSubnet(state: PrivateEndpointConfig, subnetId: ResourceId) = - { state with + member _.LinkToSubnet(state: PrivateEndpointConfig, subnetId: ResourceId) = { + state with Subnet = Some(SubnetReference.Direct(Managed subnetId)) - } + } [] - member _.LinkToUnmanagedSubnet(state: PrivateEndpointConfig, subnetId: ResourceId) = - { state with + member _.LinkToUnmanagedSubnet(state: PrivateEndpointConfig, subnetId: ResourceId) = { + state with Subnet = Some(SubnetReference.Direct(Unmanaged subnetId)) - } + } [] member _.Resource(state: PrivateEndpointConfig, resource: LinkedResource) = { state with Resource = Some resource } [] - member _.CustomNetworkInterfaceName(state: PrivateEndpointConfig, customNicName: string) = - { state with + member _.CustomNetworkInterfaceName(state: PrivateEndpointConfig, customNicName: string) = { + state with CustomNetworkInterfaceName = Some customNicName - } + } [] - member _.AddGroupIds(state: PrivateEndpointConfig, groupIds: string list) = - { state with + member _.AddGroupIds(state: PrivateEndpointConfig, groupIds: string list) = { + state with GroupIds = state.GroupIds @ groupIds - } + } let privateEndpoint = PrivateEndpointBuilder() diff --git a/src/Farmer/Builders/Builders.PrivateLink.fs b/src/Farmer/Builders/Builders.PrivateLink.fs index fb397d249..7f819d4ee 100644 --- a/src/Farmer/Builders/Builders.PrivateLink.fs +++ b/src/Farmer/Builders/Builders.PrivateLink.fs @@ -7,53 +7,50 @@ open System open System.Net.Sockets open Farmer.Builders -type PrivateLinkServiceIpConfig = - { - Name: ResourceName - PrivateIpAllocationMethod: AllocationMethod - PrivateIpAddressVersion: AddressFamily - Primary: bool - SubnetId: ResourceId option - } +type PrivateLinkServiceIpConfig = { + Name: ResourceName + PrivateIpAllocationMethod: AllocationMethod + PrivateIpAddressVersion: AddressFamily + Primary: bool + SubnetId: ResourceId option +} with static member internal BuildResource(ipConfig: PrivateLinkServiceIpConfig) = match ipConfig.SubnetId with | None -> raiseFarmer "Private link service IP config requires a subnet" | Some subnetId when subnetId.Type <> Farmer.Arm.Network.subnets -> raiseFarmer "Private link service IP config subnet resource ID must be a subnet resource Id" - | Some subnetId -> - {| - Name = ipConfig.Name.IfEmpty $"{subnetId.Name.Value}-{subnetId.Segments.Head.Value}" - Primary = ipConfig.Primary - PrivateIpAllocationMethod = ipConfig.PrivateIpAllocationMethod - PrivateIpAddressVersion = ipConfig.PrivateIpAddressVersion - SubnetId = subnetId - |} + | Some subnetId -> {| + Name = ipConfig.Name.IfEmpty $"{subnetId.Name.Value}-{subnetId.Segments.Head.Value}" + Primary = ipConfig.Primary + PrivateIpAllocationMethod = ipConfig.PrivateIpAllocationMethod + PrivateIpAddressVersion = ipConfig.PrivateIpAddressVersion + SubnetId = subnetId + |} type PrivateLinkServiceIpConfigBuilder() = - member _.Yield _ = - { - Name = ResourceName.Empty - PrivateIpAllocationMethod = AllocationMethod.DynamicPrivateIp - PrivateIpAddressVersion = AddressFamily.InterNetwork - Primary = false - SubnetId = None - } + member _.Yield _ = { + Name = ResourceName.Empty + PrivateIpAllocationMethod = AllocationMethod.DynamicPrivateIp + PrivateIpAddressVersion = AddressFamily.InterNetwork + Primary = false + SubnetId = None + } [] member _.Name(state: PrivateLinkServiceIpConfig, name) = { state with Name = ResourceName name } [] - member _.PrivateIpAllocation(state: PrivateLinkServiceIpConfig, allocationMethod: AllocationMethod) = - { state with + member _.PrivateIpAllocation(state: PrivateLinkServiceIpConfig, allocationMethod: AllocationMethod) = { + state with PrivateIpAllocationMethod = allocationMethod - } + } [] - member _.PrivateIpAddressVersion(state: PrivateLinkServiceIpConfig, addressFamily: AddressFamily) = - { state with + member _.PrivateIpAddressVersion(state: PrivateLinkServiceIpConfig, addressFamily: AddressFamily) = { + state with PrivateIpAddressVersion = addressFamily - } + } [] member _.Primary(state: PrivateLinkServiceIpConfig, primary: bool) = { state with Primary = primary } @@ -63,92 +60,89 @@ type PrivateLinkServiceIpConfigBuilder() = let privateLinkIpConfig = PrivateLinkServiceIpConfigBuilder() -type PrivateLinkServiceConfig = - { - Name: ResourceName - Dependencies: ResourceId Set - AutoApprovedSubscriptions: Guid list - EnableProxyProtocol: bool option - LoadBalancerFrontendIpConfigIds: ResourceId list - IpConfigs: PrivateLinkServiceIpConfig list - VisibleToSubscriptions: Guid list - Tags: Map - } +type PrivateLinkServiceConfig = { + Name: ResourceName + Dependencies: ResourceId Set + AutoApprovedSubscriptions: Guid list + EnableProxyProtocol: bool option + LoadBalancerFrontendIpConfigIds: ResourceId list + IpConfigs: PrivateLinkServiceIpConfig list + VisibleToSubscriptions: Guid list + Tags: Map +} with interface IBuilder with member this.ResourceId = privateLinkServices.resourceId (this.Name) - member this.BuildResources location = - [ - { - PrivateLinkService.Name = this.Name - Location = location - Dependencies = this.Dependencies - AutoApprovedSubscriptions = this.AutoApprovedSubscriptions - EnableProxyProtocol = this.EnableProxyProtocol |> Option.defaultValue false - LoadBalancerFrontendIpConfigIds = this.LoadBalancerFrontendIpConfigIds - IpConfigs = this.IpConfigs |> List.map PrivateLinkServiceIpConfig.BuildResource - VisibleToSubscriptions = this.VisibleToSubscriptions - Tags = this.Tags - } - ] + member this.BuildResources location = [ + { + PrivateLinkService.Name = this.Name + Location = location + Dependencies = this.Dependencies + AutoApprovedSubscriptions = this.AutoApprovedSubscriptions + EnableProxyProtocol = this.EnableProxyProtocol |> Option.defaultValue false + LoadBalancerFrontendIpConfigIds = this.LoadBalancerFrontendIpConfigIds + IpConfigs = this.IpConfigs |> List.map PrivateLinkServiceIpConfig.BuildResource + VisibleToSubscriptions = this.VisibleToSubscriptions + Tags = this.Tags + } + ] type PrivateLinkBuilder() = - member _.Yield _ = - { - Name = ResourceName.Empty - Dependencies = Set.empty - AutoApprovedSubscriptions = [] - EnableProxyProtocol = None - LoadBalancerFrontendIpConfigIds = [] - IpConfigs = [] - VisibleToSubscriptions = [] - Tags = Map.empty - } + member _.Yield _ = { + Name = ResourceName.Empty + Dependencies = Set.empty + AutoApprovedSubscriptions = [] + EnableProxyProtocol = None + LoadBalancerFrontendIpConfigIds = [] + IpConfigs = [] + VisibleToSubscriptions = [] + Tags = Map.empty + } interface IDependable with - member _.Add state newDeps = - { state with + member _.Add state newDeps = { + state with Dependencies = state.Dependencies + newDeps - } + } interface ITaggable with - member _.Add state tags = - { state with + member _.Add state tags = { + state with Tags = state.Tags |> Map.merge tags - } + } [] member _.Name(state: PrivateLinkServiceConfig, name) = { state with Name = ResourceName name } [] - member _.AddAutoApprovedSubscriptions(state: PrivateLinkServiceConfig, subscriptions) = - { state with + member _.AddAutoApprovedSubscriptions(state: PrivateLinkServiceConfig, subscriptions) = { + state with AutoApprovedSubscriptions = state.AutoApprovedSubscriptions @ subscriptions - } + } [] - member _.AddLoadBalancerFrontendIpConfigs(state: PrivateLinkServiceConfig, lbFrontendIpConfigs) = - { state with + member _.AddLoadBalancerFrontendIpConfigs(state: PrivateLinkServiceConfig, lbFrontendIpConfigs) = { + state with LoadBalancerFrontendIpConfigIds = state.LoadBalancerFrontendIpConfigIds @ lbFrontendIpConfigs - } + } [] - member _.AddIpConfigs(state: PrivateLinkServiceConfig, ipConfigs) = - { state with + member _.AddIpConfigs(state: PrivateLinkServiceConfig, ipConfigs) = { + state with IpConfigs = state.IpConfigs @ ipConfigs - } + } [] - member _.ProxyProtocol(state: PrivateLinkServiceConfig, flag: FeatureFlag) = - { state with + member _.ProxyProtocol(state: PrivateLinkServiceConfig, flag: FeatureFlag) = { + state with EnableProxyProtocol = Some flag.AsBoolean - } + } [] - member _.AddVisibleToSubscriptions(state: PrivateLinkServiceConfig, subscriptions) = - { state with + member _.AddVisibleToSubscriptions(state: PrivateLinkServiceConfig, subscriptions) = { + state with VisibleToSubscriptions = state.VisibleToSubscriptions @ subscriptions - } + } let privateLink = PrivateLinkBuilder() diff --git a/src/Farmer/Builders/Builders.Redis.fs b/src/Farmer/Builders/Builders.Redis.fs index 0ca920f8c..ee917bb04 100644 --- a/src/Farmer/Builders/Builders.Redis.fs +++ b/src/Farmer/Builders/Builders.Redis.fs @@ -11,17 +11,16 @@ let internal buildRedisKey (resourceId: ResourceId) = ArmExpression.create(expr, resourceId).WithOwner(resourceId) -type RedisConfig = - { - Name: ResourceName - Sku: Sku - Capacity: int - RedisConfiguration: Map - NonSslEnabled: bool option - ShardCount: int option - MinimumTlsVersion: TlsVersion option - Tags: Map - } +type RedisConfig = { + Name: ResourceName + Sku: Sku + Capacity: int + RedisConfiguration: Map + NonSslEnabled: bool option + ShardCount: int option + MinimumTlsVersion: TlsVersion option + Tags: Map +} with member this.Key = buildRedisKey this.ResourceId member private this.ResourceId = redis.resourceId this.Name @@ -29,39 +28,36 @@ type RedisConfig = interface IBuilder with member this.ResourceId = this.ResourceId - member this.BuildResources location = - [ - { - Name = this.Name - Location = location - Sku = - {| - Sku = this.Sku - Capacity = this.Capacity - |} - RedisConfiguration = this.RedisConfiguration - NonSslEnabled = this.NonSslEnabled - ShardCount = this.ShardCount - MinimumTlsVersion = this.MinimumTlsVersion - Tags = this.Tags - } - ] + member this.BuildResources location = [ + { + Name = this.Name + Location = location + Sku = {| + Sku = this.Sku + Capacity = this.Capacity + |} + RedisConfiguration = this.RedisConfiguration + NonSslEnabled = this.NonSslEnabled + ShardCount = this.ShardCount + MinimumTlsVersion = this.MinimumTlsVersion + Tags = this.Tags + } + ] type RedisBuilder() = - member _.Yield _ = - { - Name = ResourceName.Empty - Sku = Basic - Capacity = 1 - RedisConfiguration = Map.empty - NonSslEnabled = None - ShardCount = None - MinimumTlsVersion = None - Tags = Map.empty - } + member _.Yield _ = { + Name = ResourceName.Empty + Sku = Basic + Capacity = 1 + RedisConfiguration = Map.empty + NonSslEnabled = None + ShardCount = None + MinimumTlsVersion = None + Tags = Map.empty + } - member _.Run(state: RedisConfig) = - { state with + member _.Run(state: RedisConfig) = { + state with Capacity = match state with | { Sku = (Basic | Standard) } when state.Capacity > 6 -> 6 @@ -77,7 +73,7 @@ type RedisBuilder() = } when shards > 10 -> Some 10 | { Sku = Premium; ShardCount = shards } -> shards | _ -> None - } + } /// Sets the name of the Redis instance. [] @@ -95,10 +91,10 @@ type RedisBuilder() = /// Adds a custom setting to the Redis configuration [] - member _.AddSetting(state: RedisConfig, key, value) = - { state with + member _.AddSetting(state: RedisConfig, key, value) = { + state with RedisConfiguration = state.RedisConfiguration.Add(key, value) - } + } member this.AddSetting(state: RedisConfig, key, value: int) = this.AddSetting(state, key, string value) @@ -111,25 +107,24 @@ type RedisBuilder() = /// Specifies whether the non-ssl Redis server port (6379) is enabled. [] - member _.EnableNonSsl(state: RedisConfig) = - { state with NonSslEnabled = Some true } + member _.EnableNonSsl(state: RedisConfig) = { state with NonSslEnabled = Some true } [] - member _.ShardCount(state: RedisConfig, shardCount) = - { state with + member _.ShardCount(state: RedisConfig, shardCount) = { + state with ShardCount = Some shardCount - } + } [] - member _.MinimumTlsVersion(state: RedisConfig, tlsVersion) = - { state with + member _.MinimumTlsVersion(state: RedisConfig, tlsVersion) = { + state with MinimumTlsVersion = Some tlsVersion - } + } interface ITaggable with - member _.Add state tags = - { state with + member _.Add state tags = { + state with Tags = state.Tags |> Map.merge tags - } + } let redis = RedisBuilder() diff --git a/src/Farmer/Builders/Builders.ResourceGroup.fs b/src/Farmer/Builders/Builders.ResourceGroup.fs index 25e113460..2b3ade735 100644 --- a/src/Farmer/Builders/Builders.ResourceGroup.fs +++ b/src/Farmer/Builders/Builders.ResourceGroup.fs @@ -22,27 +22,26 @@ module AutoGeneratable = let Manual value = FixedValue value let AutoGenerate<'t> () = - AutoGeneratable<'t>.AutoGeneratedValue (ref None) + AutoGeneratable<'t>.AutoGeneratedValue(ref None) let deploymentIndex = let mutable index = 1000 fun () -> System.Threading.Interlocked.Increment(&index) -type ResourceGroupConfig = - { - TargetResourceGroup: string Option - DeploymentName: AutoGeneratable - Dependencies: ResourceId Set - Parameters: string Set - Outputs: Map - Location: Location - Resources: IArmResource list - ParameterValues: ParameterValue list - SubscriptionId: System.Guid option - Mode: DeploymentMode - Tags: Map - } +type ResourceGroupConfig = { + TargetResourceGroup: string Option + DeploymentName: AutoGeneratable + Dependencies: ResourceId Set + Parameters: string Set + Outputs: Map + Location: Location + Resources: IArmResource list + ParameterValues: ParameterValue list + SubscriptionId: System.Guid option + Mode: DeploymentMode + Tags: Map +} with member private this.GenerateDeploymentName() = match this.TargetResourceGroup with @@ -53,11 +52,11 @@ type ResourceGroupConfig = | Some rg -> $"{rg}-deployment-{deploymentIndex ()}" | None -> $"deployment-{deploymentIndex ()}" - member this.ResourceId = - { resourceGroupDeployment.resourceId (this.DeploymentName.GetValue this.GenerateDeploymentName) with + 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 @@ -93,23 +92,21 @@ type ResourceGroupConfig = member this.Template = this.ContentDeployment |> Option.map (fun x -> x.Template) - |> Option.defaultValue - { - Parameters = List.empty - Outputs = List.empty - Resources = List.empty - } + |> Option.defaultValue { + Parameters = List.empty + Outputs = List.empty + Resources = List.empty + } interface IDeploymentSource with member this.Deployment = - let rec getPostDeployTasks (resources: IArmResource list) = - [ - for resource in resources do - match resource with - | :? IPostDeploy as pd -> pd - | :? ResourceGroupDeployment as rgp -> yield! getPostDeployTasks rgp.Resources - | _ -> () - ] + let rec getPostDeployTasks (resources: IArmResource list) = [ + for resource in resources do + match resource with + | :? IPostDeploy as pd -> pd + | :? ResourceGroupDeployment as rgp -> yield! getPostDeployTasks rgp.Resources + | _ -> () + ] { Location = this.Location @@ -126,35 +123,33 @@ type ResourceGroupConfig = interface IBuilder with member this.ResourceId = this.ResourceId - member this.BuildResources loc = - [ - match this.ContentDeployment with - | Some x -> x - | None -> () - ] + member this.BuildResources loc = [ + match this.ContentDeployment with + | Some x -> x + | None -> () + ] type DeploymentBuilder() = - member _.Yield _ = - { - TargetResourceGroup = None - DeploymentName = AutoGeneratable.AutoGenerate() - Dependencies = Set.empty - Parameters = Set.empty - Outputs = Map.empty - Resources = List.empty - ParameterValues = List.empty - SubscriptionId = None - Location = Location.WestEurope - Mode = Incremental - Tags = Map.empty - } + member _.Yield _ = { + TargetResourceGroup = None + DeploymentName = AutoGeneratable.AutoGenerate() + Dependencies = Set.empty + Parameters = Set.empty + Outputs = Map.empty + Resources = List.empty + ParameterValues = List.empty + SubscriptionId = None + Location = Location.WestEurope + Mode = Incremental + Tags = Map.empty + } /// Creates an output value that will be returned by the ARM template. [] - member _.Output(state, outputName, outputValue) : ResourceGroupConfig = - { state with + member _.Output(state, outputName, outputValue) : ResourceGroupConfig = { + state with Outputs = state.Outputs.Add(outputName, outputValue) - } + } member this.Output(state: ResourceGroupConfig, outputName: string, (ResourceName outputValue)) = this.Output(state, outputName, outputValue) @@ -173,10 +168,10 @@ type DeploymentBuilder() = | None -> state [] - member __.Outputs(state, outputs) : ResourceGroupConfig = - { state with + member __.Outputs(state, outputs) : ResourceGroupConfig = { + state with Outputs = Map.merge outputs state.Outputs - } + } member this.Outputs(state: ResourceGroupConfig, outputs) = this.Outputs(state, outputs |> List.map (fun (k: string, ResourceName r) -> k, r)) @@ -188,12 +183,12 @@ type DeploymentBuilder() = [] member _.Location(state, location) : ResourceGroupConfig = { state with Location = location } - static member private AddResources(state: ResourceGroupConfig, resources: IArmResource list) = - { state with + static member private AddResources(state: ResourceGroupConfig, resources: IArmResource list) = { + state with Resources = state.Resources @ resources |> List.distinctBy (fun r -> r.ResourceId, r.GetType().Name) - } + } /// Adds a builder's ARM resources to the ARM template. [] @@ -231,51 +226,51 @@ type DeploymentBuilder() = DeploymentBuilder.AddResources(state, input) interface ITaggable with - member _.Add state tags = - { state with + member _.Add state tags = { + state with Tags = state.Tags |> Map.merge tags - } + } interface IDependable with - member _.Add state newDeps = - { state with + member _.Add state newDeps = { + state with Dependencies = state.Dependencies + newDeps - } + } type ResourceGroupBuilder() = inherit DeploymentBuilder() /// Creates an output value that will be returned by the ARM template. [] - member _.SetName(state: ResourceGroupConfig, name) = - { state with + member _.SetName(state: ResourceGroupConfig, name) = { + state with TargetResourceGroup = Some name - } + } /// Creates an output value that will be returned by the ARM template. [] - member _.SetDeploymentName(state: ResourceGroupConfig, name) = - { state with + member _.SetDeploymentName(state: ResourceGroupConfig, name) = { + state with DeploymentName = FixedValue name - } + } /// Sets the subscription ID for a nested deployment [] - member this.SubscriptionId(state: ResourceGroupConfig, subscriptionId: System.Guid) = - { state with + member this.SubscriptionId(state: ResourceGroupConfig, subscriptionId: System.Guid) = { + state with SubscriptionId = Some subscriptionId - } + } - member this.SubscriptionId(state: ResourceGroupConfig, subscriptionId: string) = - { state with + member this.SubscriptionId(state: ResourceGroupConfig, subscriptionId: string) = { + state with SubscriptionId = Some(System.Guid subscriptionId) - } + } [] - member this.AddParameterValues(state: ResourceGroupConfig, parameters: (string * string) list) = - { state with + member this.AddParameterValues(state: ResourceGroupConfig, parameters: (string * string) list) = { + state with ParameterValues = state.ParameterValues @ (parameters |> List.map ParameterValue) - } + } [] member this.AddKeyVaultSecretReferences @@ -283,17 +278,17 @@ type ResourceGroupBuilder() = state: ResourceGroupConfig, parameters: (string * ResourceId * string) list ) = - { state with - ParameterValues = state.ParameterValues @ (parameters |> List.map KeyVaultReference) + { + state with + ParameterValues = state.ParameterValues @ (parameters |> List.map KeyVaultReference) } let resourceGroup = ResourceGroupBuilder() /// Creates a resource group in a subscription level deployment. -let createResourceGroup (name: string) (location: Location) : ResourceGroup = - { - Name = ResourceName name - Location = location - Dependencies = Set.empty - Tags = Map.empty - } +let createResourceGroup (name: string) (location: Location) : ResourceGroup = { + Name = ResourceName name + Location = location + Dependencies = Set.empty + Tags = Map.empty +} diff --git a/src/Farmer/Builders/Builders.RouteTable.fs b/src/Farmer/Builders/Builders.RouteTable.fs index 4a91e613e..3a04616a4 100644 --- a/src/Farmer/Builders/Builders.RouteTable.fs +++ b/src/Farmer/Builders/Builders.RouteTable.fs @@ -5,21 +5,19 @@ open Farmer open Farmer.Arm open Farmer.Route -type RouteConfig = - { - Name: ResourceName - AddressPrefix: IPAddressCidr option - NextHopType: Route.HopType - HasBgpOverride: FeatureFlag option - } - -type RouteTableConfig = - { - Name: ResourceName - DisableBGPRoutePropagation: FeatureFlag option - Routes: RouteConfig list - Tags: Map - } +type RouteConfig = { + Name: ResourceName + AddressPrefix: IPAddressCidr option + NextHopType: Route.HopType + HasBgpOverride: FeatureFlag option +} + +type RouteTableConfig = { + Name: ResourceName + DisableBGPRoutePropagation: FeatureFlag option + Routes: RouteConfig list + Tags: Map +} with interface IBuilder with member this.ResourceId = routeTables.resourceId this.Name @@ -30,60 +28,55 @@ type RouteTableConfig = |> List.map (fun r -> match r.AddressPrefix with | None -> raiseFarmer ("address prefix is required") - | Some addressPrefix -> - { - Name = r.Name - AddressPrefix = addressPrefix - NextHopType = r.NextHopType - HasBgpOverride = r.HasBgpOverride |> Option.defaultValue FeatureFlag.Disabled - }) - - let routeTable: Network.RouteTable = - { - RouteTable.Name = this.Name - Location = location - DisableBGPRoutePropagation = - this.DisableBGPRoutePropagation |> Option.defaultValue FeatureFlag.Disabled - Routes = routes - Tags = this.Tags - } + | Some addressPrefix -> { + Name = r.Name + AddressPrefix = addressPrefix + NextHopType = r.NextHopType + HasBgpOverride = r.HasBgpOverride |> Option.defaultValue FeatureFlag.Disabled + }) + + let routeTable: Network.RouteTable = { + RouteTable.Name = this.Name + Location = location + DisableBGPRoutePropagation = this.DisableBGPRoutePropagation |> Option.defaultValue FeatureFlag.Disabled + Routes = routes + Tags = this.Tags + } [ routeTable ] type RouteTableBuilder() = - member _.Yield _ = - { - Name = ResourceName.Empty - Routes = [] - DisableBGPRoutePropagation = None - Tags = Map.empty - } + member _.Yield _ = { + Name = ResourceName.Empty + Routes = [] + DisableBGPRoutePropagation = None + Tags = Map.empty + } [] member _.Name(state: RouteTableConfig, name: string) = { state with Name = ResourceName name } [] - member _.DisableBGPRoutePropagation(state: RouteTableConfig, flag: bool) = - { state with + member _.DisableBGPRoutePropagation(state: RouteTableConfig, flag: bool) = { + state with DisableBGPRoutePropagation = Some(FeatureFlag.ofBool flag) - } + } [] - member _.AddRoute(state: RouteTableConfig, routeConfigs: RouteConfig list) = - { state with + member _.AddRoute(state: RouteTableConfig, routeConfigs: RouteConfig list) = { + state with Routes = routeConfigs @ state.Routes - } + } let routeTable = RouteTableBuilder() type RouteBuilder() = - member _.Yield _ = - { - Name = ResourceName.Empty - AddressPrefix = None - NextHopType = Route.HopType.Nothing - HasBgpOverride = None - } + member _.Yield _ = { + Name = ResourceName.Empty + AddressPrefix = None + NextHopType = Route.HopType.Nothing + HasBgpOverride = None + } [] member _.Name(state: RouteConfig, name: string) = { state with Name = ResourceName name } @@ -91,29 +84,29 @@ type RouteBuilder() = [] member _.AddressPrefix(state: RouteConfig, ip: IPAddressCidr) = { state with AddressPrefix = Some ip } - member _.AddressPrefix(state: RouteConfig, ip: string) = - { state with + member _.AddressPrefix(state: RouteConfig, ip: string) = { + state with AddressPrefix = Some(IPAddressCidr.parse ip) - } + } [] member _.NextHopType(state: RouteConfig, ht: Route.HopType) = { state with NextHopType = ht } [] - member _.NextHopIpAddress(state: RouteConfig, ip: System.Net.IPAddress) = - { state with + member _.NextHopIpAddress(state: RouteConfig, ip: System.Net.IPAddress) = { + state with NextHopType = VirtualAppliance(Some ip) - } + } - member _.NextHopIpAddress(state: RouteConfig, ip: string) = - { state with + member _.NextHopIpAddress(state: RouteConfig, ip: string) = { + state with NextHopType = VirtualAppliance(Some(System.Net.IPAddress.Parse ip)) - } + } [] - member _.HasBgpOverride(state: RouteConfig, flag: bool) = - { state with + member _.HasBgpOverride(state: RouteConfig, flag: bool) = { + state with HasBgpOverride = Some(FeatureFlag.ofBool flag) - } + } let route = RouteBuilder() diff --git a/src/Farmer/Builders/Builders.Search.fs b/src/Farmer/Builders/Builders.Search.fs index d3360044b..857343113 100644 --- a/src/Farmer/Builders/Builders.Search.fs +++ b/src/Farmer/Builders/Builders.Search.fs @@ -6,14 +6,13 @@ open Farmer.Search open Farmer.Helpers open Farmer.Arm.Search -type SearchConfig = - { - Name: ResourceName - Sku: Sku - Replicas: int - Partitions: int - Tags: Map - } +type SearchConfig = { + Name: ResourceName + Sku: Sku + Replicas: int + Partitions: int + Tags: Map +} with /// Gets an ARM expression for the admin key of the search instance. member this.AdminKey = @@ -34,32 +33,30 @@ type SearchConfig = interface IBuilder with member this.ResourceId = this.ResourceId - member this.BuildResources location = - [ - { - Name = this.Name - Location = location - Sku = this.Sku - ReplicaCount = this.Replicas - PartitionCount = this.Partitions - Tags = this.Tags - } - ] + member this.BuildResources location = [ + { + Name = this.Name + Location = location + Sku = this.Sku + ReplicaCount = this.Replicas + PartitionCount = this.Partitions + Tags = this.Tags + } + ] type SearchBuilder() = - member _.Yield _ = - { - Name = ResourceName.Empty - Sku = Standard - Replicas = 1 - Partitions = 1 - Tags = Map.empty - } + member _.Yield _ = { + Name = ResourceName.Empty + Sku = Standard + Replicas = 1 + Partitions = 1 + Tags = Map.empty + } - member _.Run(state: SearchConfig) = - { state with + member _.Run(state: SearchConfig) = { + state with Name = state.Name |> sanitiseSearch |> ResourceName - } + } /// Sets the name of the Azure Search instance. [] @@ -80,9 +77,9 @@ type SearchBuilder() = member _.PartitionCount(state: SearchConfig, partitions: int) = { state with Partitions = partitions } interface ITaggable with - member _.Add state tags = - { state with + member _.Add state tags = { + state with Tags = state.Tags |> Map.merge tags - } + } let search = SearchBuilder() diff --git a/src/Farmer/Builders/Builders.ServiceBus.fs b/src/Farmer/Builders/Builders.ServiceBus.fs index 4432c43ec..b18f6d58a 100644 --- a/src/Farmer/Builders/Builders.ServiceBus.fs +++ b/src/Farmer/Builders/Builders.ServiceBus.fs @@ -14,66 +14,63 @@ module CommonMembers = open CommonMembers -type ServiceBusQueueConfig = - { - Name: ResourceName - Namespace: LinkedResource - LockDuration: TimeSpan option - DuplicateDetection: TimeSpan option - DefaultMessageTimeToLive: TimeSpan option - ForwardTo: ResourceName option - Session: bool option - DeadLetteringOnMessageExpiration: bool option - MaxDeliveryCount: int option - MaxSizeInMegabytes: int option - EnablePartitioning: bool option - AuthorizationRules: Map - } +type ServiceBusQueueConfig = { + Name: ResourceName + Namespace: LinkedResource + LockDuration: TimeSpan option + DuplicateDetection: TimeSpan option + DefaultMessageTimeToLive: TimeSpan option + ForwardTo: ResourceName option + Session: bool option + DeadLetteringOnMessageExpiration: bool option + MaxDeliveryCount: int option + MaxSizeInMegabytes: int option + EnablePartitioning: bool option + AuthorizationRules: Map +} with interface IBuilder with member this.ResourceId = queues.resourceId (this.Namespace.Name / this.Name) - member this.BuildResources location = - [ + member this.BuildResources location = [ + { + Name = this.Name + Namespace = this.Namespace + LockDuration = this.LockDuration |> Option.map IsoDateTime.OfTimeSpan + DuplicateDetectionHistoryTimeWindow = this.DuplicateDetection |> Option.map IsoDateTime.OfTimeSpan + Session = this.Session + DeadLetteringOnMessageExpiration = this.DeadLetteringOnMessageExpiration + DefaultMessageTimeToLive = this.DefaultMessageTimeToLive |> Option.map IsoDateTime.OfTimeSpan + ForwardTo = this.ForwardTo + MaxDeliveryCount = this.MaxDeliveryCount + MaxSizeInMegabytes = this.MaxSizeInMegabytes + EnablePartitioning = this.EnablePartitioning + } + for rule in this.AuthorizationRules do { - Name = this.Name - Namespace = this.Namespace - LockDuration = this.LockDuration |> Option.map IsoDateTime.OfTimeSpan - DuplicateDetectionHistoryTimeWindow = this.DuplicateDetection |> Option.map IsoDateTime.OfTimeSpan - Session = this.Session - DeadLetteringOnMessageExpiration = this.DeadLetteringOnMessageExpiration - DefaultMessageTimeToLive = this.DefaultMessageTimeToLive |> Option.map IsoDateTime.OfTimeSpan - ForwardTo = this.ForwardTo - MaxDeliveryCount = this.MaxDeliveryCount - MaxSizeInMegabytes = this.MaxSizeInMegabytes - EnablePartitioning = this.EnablePartitioning + QueueAuthorizationRule.Name = + rule.Key.Map(fun name -> $"{this.Namespace.Name.Value}/{this.Name.Value}/%s{name}") + Location = location + Dependencies = [ namespaces.resourceId this.Name; queues.resourceId (this.Name, this.Name) ] + Rights = rule.Value } - for rule in this.AuthorizationRules do - { - QueueAuthorizationRule.Name = - rule.Key.Map(fun name -> $"{this.Namespace.Name.Value}/{this.Name.Value}/%s{name}") - Location = location - Dependencies = [ namespaces.resourceId this.Name; queues.resourceId (this.Name, this.Name) ] - Rights = rule.Value - } - ] + ] type ServiceBusQueueBuilder() = - member _.Yield _ = - { - Name = ResourceName.Empty - Namespace = Managed(namespaces.resourceId ResourceName.Empty) - LockDuration = None - DuplicateDetection = None - Session = None - DeadLetteringOnMessageExpiration = None - DefaultMessageTimeToLive = None - ForwardTo = None - MaxDeliveryCount = None - MaxSizeInMegabytes = None - EnablePartitioning = None - AuthorizationRules = Map.empty - } + member _.Yield _ = { + Name = ResourceName.Empty + Namespace = Managed(namespaces.resourceId ResourceName.Empty) + LockDuration = None + DuplicateDetection = None + Session = None + DeadLetteringOnMessageExpiration = None + DefaultMessageTimeToLive = None + ForwardTo = None + MaxDeliveryCount = None + MaxSizeInMegabytes = None + EnablePartitioning = None + AuthorizationRules = Map.empty + } /// The name of the queue. [] @@ -81,61 +78,61 @@ type ServiceBusQueueBuilder() = /// The length of time that a lock can be held on a message. [] - member _.LockDurationMinutes(state: ServiceBusQueueConfig, duration) = - { state with + member _.LockDurationMinutes(state: ServiceBusQueueConfig, duration) = { + state with LockDuration = Some(TimeSpan.FromMinutes(float duration)) - } + } /// Whether to enable duplicate detection, and if so, how long to check for.ServiceBusQueueConfig [] - member _.DuplicateDetectionTs(state: ServiceBusQueueConfig, maxTimeWindow) = - { state with + member _.DuplicateDetectionTs(state: ServiceBusQueueConfig, maxTimeWindow) = { + state with DuplicateDetection = maxTimeWindow - } + } - member _.DuplicateDetectionTs(state: ServiceBusQueueConfig, maxTimeWindow) = - { state with + member _.DuplicateDetectionTs(state: ServiceBusQueueConfig, maxTimeWindow) = { + state with DuplicateDetection = Some maxTimeWindow - } + } /// Whether to enable duplicate detection, and if so, how long to check for.ServiceBusQueueConfig [] - member _.DuplicateDetection(state: ServiceBusQueueConfig, maxTimeWindow) = - { state with + member _.DuplicateDetection(state: ServiceBusQueueConfig, maxTimeWindow) = { + state with DuplicateDetection = Some(TimeSpan.FromMinutes(float maxTimeWindow)) - } + } /// The maximum size for the queue in megabytes. [] - member _.MaxTopicSize(state: ServiceBusQueueConfig, maxTopicSize: int) = - { state with + member _.MaxTopicSize(state: ServiceBusQueueConfig, maxTopicSize: int) = { + state with MaxSizeInMegabytes = Some maxTopicSize - } + } /// The default time-to-live for messages in a timespan string (e.g. '00:05:00'). If not specified, the maximum TTL will be set for the SKU. [] - member _.MessageTtl(state: ServiceBusQueueConfig, ttl) = - { state with + member _.MessageTtl(state: ServiceBusQueueConfig, ttl) = { + state with DefaultMessageTimeToLive = Some(TimeSpan.Parse ttl) - } + } /// The default time-to-live for messages in days. If not specified, the maximum TTL will be set for the SKU. - member _.MessageTtl(state: ServiceBusQueueConfig, ttl: int) = - { state with + member _.MessageTtl(state: ServiceBusQueueConfig, ttl: int) = { + state with DefaultMessageTimeToLive = ttl / 1 |> float |> TimeSpan.FromDays |> Some - } + } //// The default time-to-live for messages defined in a .NET TimeSpan. - member _.MessageTtl(state: ServiceBusQueueConfig, timespan: TimeSpan) = - { state with + member _.MessageTtl(state: ServiceBusQueueConfig, timespan: TimeSpan) = { + state with DefaultMessageTimeToLive = Some timespan - } + } /// Enables session support. [] - member _.MaxDeliveryCount(state: ServiceBusQueueConfig, count) = - { state with + member _.MaxDeliveryCount(state: ServiceBusQueueConfig, count) = { + state with MaxDeliveryCount = Some count - } + } /// Whether to enable duplicate detection, and if so, how long to check for.ServiceBusQueueConfig [] @@ -143,91 +140,88 @@ type ServiceBusQueueBuilder() = /// Enables dead lettering of messages that expire. [] - member _.DeadLetteringOnMessageExpiration(state: ServiceBusQueueConfig) = - { state with + member _.DeadLetteringOnMessageExpiration(state: ServiceBusQueueConfig) = { + state with DeadLetteringOnMessageExpiration = Some true - } + } /// Enables partition support on the queue. [] - member _.EnablePartition(state: ServiceBusQueueConfig) = - { state with + member _.EnablePartition(state: ServiceBusQueueConfig) = { + state with EnablePartitioning = Some true - } + } /// Add authorization rule on the queue. [] - member _.AddAuthorizationRule(state: ServiceBusQueueConfig, name, rights) = - { state with + member _.AddAuthorizationRule(state: ServiceBusQueueConfig, name, rights) = { + state with AuthorizationRules = state.AuthorizationRules.Add(ResourceName name, Set rights) - } + } /// Instead of creating or modifying a namespace, configure this subscription to point to another unmanaged namespace instance. [] - member this.LinkToUnmanagedNamespace(state: ServiceBusQueueConfig, namespaceName: ResourceName) = - { state with + member this.LinkToUnmanagedNamespace(state: ServiceBusQueueConfig, namespaceName: ResourceName) = { + state with Namespace = Unmanaged(namespaces.resourceId namespaceName) - } + } - member this.LinkToUnmanagedNamespace(state: ServiceBusQueueConfig, namespaceName) = - { state with + member this.LinkToUnmanagedNamespace(state: ServiceBusQueueConfig, namespaceName) = { + state with Namespace = Unmanaged(namespaces.resourceId (ResourceName namespaceName)) - } + } interface IForwardTo with member this.SetForwardTo state resource = { state with ForwardTo = resource } -type ServiceBusSubscriptionConfig = - { - Name: ResourceName - Topic: LinkedResource - LockDuration: TimeSpan option - DuplicateDetection: TimeSpan option - DefaultMessageTimeToLive: TimeSpan option - ForwardTo: ResourceName option - MaxDeliveryCount: int option - Session: bool option - DeadLetteringOnMessageExpiration: bool option - Rules: Rule list - DependsOn: Set - } +type ServiceBusSubscriptionConfig = { + Name: ResourceName + Topic: LinkedResource + LockDuration: TimeSpan option + DuplicateDetection: TimeSpan option + DefaultMessageTimeToLive: TimeSpan option + ForwardTo: ResourceName option + MaxDeliveryCount: int option + Session: bool option + DeadLetteringOnMessageExpiration: bool option + Rules: Rule list + DependsOn: Set +} with interface IBuilder with member this.ResourceId = subscriptions.resourceId (this.Topic.Name / this.Topic.ResourceId.Segments.[0] / this.Name) - member this.BuildResources location = - [ - { - Name = this.Name - Topic = this.Topic - LockDuration = this.LockDuration |> Option.map IsoDateTime.OfTimeSpan - DuplicateDetectionHistoryTimeWindow = this.DuplicateDetection |> Option.map IsoDateTime.OfTimeSpan - DefaultMessageTimeToLive = this.DefaultMessageTimeToLive |> Option.map IsoDateTime.OfTimeSpan - ForwardTo = this.ForwardTo - MaxDeliveryCount = this.MaxDeliveryCount - Session = this.Session - DeadLetteringOnMessageExpiration = this.DeadLetteringOnMessageExpiration - Rules = this.Rules - DependsOn = this.DependsOn - } - ] + member this.BuildResources location = [ + { + Name = this.Name + Topic = this.Topic + LockDuration = this.LockDuration |> Option.map IsoDateTime.OfTimeSpan + DuplicateDetectionHistoryTimeWindow = this.DuplicateDetection |> Option.map IsoDateTime.OfTimeSpan + DefaultMessageTimeToLive = this.DefaultMessageTimeToLive |> Option.map IsoDateTime.OfTimeSpan + ForwardTo = this.ForwardTo + MaxDeliveryCount = this.MaxDeliveryCount + Session = this.Session + DeadLetteringOnMessageExpiration = this.DeadLetteringOnMessageExpiration + Rules = this.Rules + DependsOn = this.DependsOn + } + ] type ServiceBusSubscriptionBuilder() = - member _.Yield _ = - { - Name = ResourceName.Empty - Topic = Managed(namespaces.resourceId ResourceName.Empty) - LockDuration = None - DuplicateDetection = None - DefaultMessageTimeToLive = None - ForwardTo = None - MaxDeliveryCount = None - Session = None - DeadLetteringOnMessageExpiration = None - Rules = List.empty - DependsOn = Set.empty - } + member _.Yield _ = { + Name = ResourceName.Empty + Topic = Managed(namespaces.resourceId ResourceName.Empty) + LockDuration = None + DuplicateDetection = None + DefaultMessageTimeToLive = None + ForwardTo = None + MaxDeliveryCount = None + Session = None + DeadLetteringOnMessageExpiration = None + Rules = List.empty + DependsOn = Set.empty + } /// The name of the queue. [] @@ -235,42 +229,42 @@ type ServiceBusSubscriptionBuilder() = /// The length of time that a lock can be held on a message. [] - member _.LockDurationMinutes(state: ServiceBusSubscriptionConfig, duration) = - { state with + member _.LockDurationMinutes(state: ServiceBusSubscriptionConfig, duration) = { + state with LockDuration = Some(TimeSpan.FromMinutes(float duration)) - } + } /// Whether to enable duplicate detection, and if so, how long to check for.ServiceBusQueueConfig [] - member _.DuplicateDetection(state: ServiceBusSubscriptionConfig, maxTimeWindow) = - { state with + member _.DuplicateDetection(state: ServiceBusSubscriptionConfig, maxTimeWindow) = { + state with DuplicateDetection = Some(TimeSpan.FromMinutes(float maxTimeWindow)) - } + } /// The default time-to-live for messages in a timespan string (e.g. '00:05:00'). If not specified, the maximum TTL will be set for the SKU. [] - member _.MessageTtl(state: ServiceBusSubscriptionConfig, ttl) = - { state with + member _.MessageTtl(state: ServiceBusSubscriptionConfig, ttl) = { + state with DefaultMessageTimeToLive = Some(TimeSpan.Parse ttl) - } + } /// The default time-to-live for messages in days. If not specified, the maximum TTL will be set for the SKU. - member _.MessageTtl(state: ServiceBusSubscriptionConfig, ttl: int) = - { state with + member _.MessageTtl(state: ServiceBusSubscriptionConfig, ttl: int) = { + state with DefaultMessageTimeToLive = ttl / 1 |> float |> TimeSpan.FromDays |> Some - } + } //// The default time-to-live for messages defined in a .NET TimeSpan. - member _.MessageTtl(state: ServiceBusSubscriptionConfig, timespan: TimeSpan) = - { state with + member _.MessageTtl(state: ServiceBusSubscriptionConfig, timespan: TimeSpan) = { + state with DefaultMessageTimeToLive = Some timespan - } + } /// Enables session support. [] - member _.MaxDeliveryCount(state: ServiceBusSubscriptionConfig, count) = - { state with + member _.MaxDeliveryCount(state: ServiceBusSubscriptionConfig, count) = { + state with MaxDeliveryCount = Some count - } + } /// Whether to enable duplicate detection, and if so, how long to check for.ServiceBusQueueConfig [] @@ -278,18 +272,18 @@ type ServiceBusSubscriptionBuilder() = /// Enables dead lettering of messages that expire. [] - member _.DeadLetteringOnMessageExpiration(state: ServiceBusSubscriptionConfig) = - { state with + member _.DeadLetteringOnMessageExpiration(state: ServiceBusSubscriptionConfig) = { + state with DeadLetteringOnMessageExpiration = Some true - } + } /// Automatically forward to a queue or topic /// Adds filtering rules for a subscription [] - member _.AddFilters(state: ServiceBusSubscriptionConfig, filters) = - { state with + member _.AddFilters(state: ServiceBusSubscriptionConfig, filters) = { + state with Rules = state.Rules @ filters - } + } /// Adds a sql filtering rule for a subscription [] @@ -303,82 +297,80 @@ type ServiceBusSubscriptionBuilder() = /// Instead of creating or modifying a namespace, configure this subscription to point to another unmanaged namespace instance. [] - member this.LinkToUnmanagedTopic(state: ServiceBusSubscriptionConfig, topicName: ResourceName) = - { state with + member this.LinkToUnmanagedTopic(state: ServiceBusSubscriptionConfig, topicName: ResourceName) = { + state with Topic = Unmanaged(topics.resourceId topicName) - } + } - member this.LinkToUnmanagedTopic(state: ServiceBusSubscriptionConfig, topicName) = - { state with + member this.LinkToUnmanagedTopic(state: ServiceBusSubscriptionConfig, topicName) = { + state with Topic = Unmanaged(topics.resourceId (ResourceName topicName)) - } + } interface IForwardTo with member this.SetForwardTo state resource = { state with ForwardTo = resource } interface IDependable with - member _.Add state resIds = - { state with + member _.Add state resIds = { + state with DependsOn = state.DependsOn + resIds - } + } -type ServiceBusTopicConfig = - { - Name: ResourceName - Namespace: LinkedResource - DuplicateDetection: TimeSpan option - DefaultMessageTimeToLive: TimeSpan option - EnablePartitioning: bool option - MaxSizeInMegabytes: int option - Subscriptions: Map - } +type ServiceBusTopicConfig = { + Name: ResourceName + Namespace: LinkedResource + DuplicateDetection: TimeSpan option + DefaultMessageTimeToLive: TimeSpan option + EnablePartitioning: bool option + MaxSizeInMegabytes: int option + Subscriptions: Map +} with member this.ResourceId = topics.resourceId (this.Namespace.Name, this.Name) interface IBuilder with member this.ResourceId = this.ResourceId - member this.BuildResources location = - [ - { - Name = this.Name - Dependencies = - [ - match this.Namespace with - | Managed resId -> resId // Only generate dependency if this is managed by Farmer (same template) - | _ -> () - ] - |> Set.ofList - Namespace = + member this.BuildResources location = [ + { + Name = this.Name + Dependencies = + [ match this.Namespace with - | Managed resId - | Unmanaged resId -> resId - DuplicateDetectionHistoryTimeWindow = this.DuplicateDetection |> Option.map IsoDateTime.OfTimeSpan - DefaultMessageTimeToLive = this.DefaultMessageTimeToLive |> Option.map IsoDateTime.OfTimeSpan - EnablePartitioning = this.EnablePartitioning - MaxSizeInMegabytes = this.MaxSizeInMegabytes - } - for subscription in this.Subscriptions do - let subscription = - { subscription.Value with + | Managed resId -> resId // Only generate dependency if this is managed by Farmer (same template) + | _ -> () + ] + |> Set.ofList + Namespace = + match this.Namespace with + | Managed resId + | Unmanaged resId -> resId + DuplicateDetectionHistoryTimeWindow = this.DuplicateDetection |> Option.map IsoDateTime.OfTimeSpan + DefaultMessageTimeToLive = this.DefaultMessageTimeToLive |> Option.map IsoDateTime.OfTimeSpan + EnablePartitioning = this.EnablePartitioning + MaxSizeInMegabytes = this.MaxSizeInMegabytes + } + for subscription in this.Subscriptions do + let subscription = + { + subscription.Value with Topic = Managed this.ResourceId - } - :> IBuilder + } + :> IBuilder - yield! subscription.BuildResources location - ] + yield! subscription.BuildResources location + ] type ServiceBusTopicBuilder() = - member _.Yield _ = - { - Name = ResourceName.Empty - Namespace = Managed(namespaces.resourceId ResourceName.Empty) - DuplicateDetection = None - DefaultMessageTimeToLive = None - EnablePartitioning = None - MaxSizeInMegabytes = None - Subscriptions = Map.empty - } + member _.Yield _ = { + Name = ResourceName.Empty + Namespace = Managed(namespaces.resourceId ResourceName.Empty) + DuplicateDetection = None + DefaultMessageTimeToLive = None + EnablePartitioning = None + MaxSizeInMegabytes = None + Subscriptions = Map.empty + } /// The name of the queue. [] @@ -386,90 +378,89 @@ type ServiceBusTopicBuilder() = /// Whether to enable duplicate detection, and if so, how long to check for.ServiceBusQueueConfig [] - member _.DuplicateDetectionTs(state: ServiceBusTopicConfig, maxTimeWindow) = - { state with + member _.DuplicateDetectionTs(state: ServiceBusTopicConfig, maxTimeWindow) = { + state with DuplicateDetection = maxTimeWindow - } + } - member _.DuplicateDetectionTs(state: ServiceBusTopicConfig, maxTimeWindow) = - { state with + member _.DuplicateDetectionTs(state: ServiceBusTopicConfig, maxTimeWindow) = { + state with DuplicateDetection = Some maxTimeWindow - } + } /// Whether to enable duplicate detection, and if so, how long to check for.ServiceBusQueueConfig [] - member _.DuplicateDetection(state: ServiceBusTopicConfig, maxTimeWindow) = - { state with + member _.DuplicateDetection(state: ServiceBusTopicConfig, maxTimeWindow) = { + state with DuplicateDetection = Some(TimeSpan.FromMinutes(float maxTimeWindow)) - } + } /// The maximum size for the topic in megabytes. [] - member _.MaxTopicSize(state: ServiceBusTopicConfig, maxTopicSize: int) = - { state with + member _.MaxTopicSize(state: ServiceBusTopicConfig, maxTopicSize: int) = { + state with MaxSizeInMegabytes = Some maxTopicSize - } + } /// The default time-to-live for messages in a timespan string (e.g. '00:05:00'). If not specified, the maximum TTL will be set for the SKU. [] - member _.MessageTtl(state: ServiceBusTopicConfig, ttl) = - { state with + member _.MessageTtl(state: ServiceBusTopicConfig, ttl) = { + state with DefaultMessageTimeToLive = Some(TimeSpan.Parse ttl) - } + } /// The default time-to-live for messages in days. If not specified, the maximum TTL will be set for the SKU. - member _.MessageTtl(state: ServiceBusTopicConfig, ttl: int) = - { state with + member _.MessageTtl(state: ServiceBusTopicConfig, ttl: int) = { + state with DefaultMessageTimeToLive = ttl / 1 |> float |> TimeSpan.FromDays |> Some - } + } //// The default time-to-live for messages defined in a .NET TimeSpan. - member _.MessageTtl(state: ServiceBusTopicConfig, timespan: TimeSpan) = - { state with + member _.MessageTtl(state: ServiceBusTopicConfig, timespan: TimeSpan) = { + state with DefaultMessageTimeToLive = Some timespan - } + } /// Enables partition support on the topic. [] - member _.EnablePartition(state: ServiceBusTopicConfig) = - { state with + member _.EnablePartition(state: ServiceBusTopicConfig) = { + state with EnablePartitioning = Some true - } + } [] - member _.AddSubscriptions(state: ServiceBusTopicConfig, subscriptions) = - { state with + member _.AddSubscriptions(state: ServiceBusTopicConfig, subscriptions) = { + state with Subscriptions = (state.Subscriptions, subscriptions) ||> List.fold (fun state (subscription: ServiceBusSubscriptionConfig) -> state.Add(subscription.Name, subscription)) - } + } /// Instead of creating or modifying a namespace, configure this topic to point to another unmanaged namespace instance. [] - member this.LinkToUnmanagedNamespace(state: ServiceBusTopicConfig, namespaceName: ResourceName) = - { state with + member this.LinkToUnmanagedNamespace(state: ServiceBusTopicConfig, namespaceName: ResourceName) = { + state with Namespace = Unmanaged(namespaces.resourceId namespaceName) - } + } - member this.LinkToUnmanagedNamespace(state: ServiceBusTopicConfig, namespaceName) = - { state with + member this.LinkToUnmanagedNamespace(state: ServiceBusTopicConfig, namespaceName) = { + state with Namespace = Unmanaged(namespaces.resourceId (ResourceName namespaceName)) - } - -type ServiceBusConfig = - { - Name: ResourceName - Sku: Sku - Dependencies: ResourceId Set - Queues: Map - Topics: Map - AuthorizationRules: Map - ZoneRedundant: FeatureFlag option - DisablePublicNetworkAccess: FeatureFlag option - MinTlsVersion: TlsVersion option - Tags: Map } +type ServiceBusConfig = { + Name: ResourceName + Sku: Sku + Dependencies: ResourceId Set + Queues: Map + Topics: Map + AuthorizationRules: Map + ZoneRedundant: FeatureFlag option + DisablePublicNetworkAccess: FeatureFlag option + MinTlsVersion: TlsVersion option + Tags: Map +} with + member private this.GetKeyPath property = let expr = $"listkeys(resourceId('Microsoft.ServiceBus/namespaces/authorizationRules', '{this.Name.Value}', 'RootManageSharedAccessKey'), '2017-04-01').{property}" @@ -484,67 +475,67 @@ type ServiceBusConfig = interface IBuilder with member this.ResourceId = namespaces.resourceId this.Name - member this.BuildResources location = - [ - { - Name = this.Name - Location = location - Sku = this.Sku - Dependencies = this.Dependencies - DisablePublicNetworkAccess = this.DisablePublicNetworkAccess - ZoneRedundant = this.ZoneRedundant - MinTlsVersion = this.MinTlsVersion - Tags = this.Tags - } + member this.BuildResources location = [ + { + Name = this.Name + Location = location + Sku = this.Sku + Dependencies = this.Dependencies + DisablePublicNetworkAccess = this.DisablePublicNetworkAccess + ZoneRedundant = this.ZoneRedundant + MinTlsVersion = this.MinTlsVersion + Tags = this.Tags + } - for queue in this.Queues do - let queue = - { queue.Value with + for queue in this.Queues do + let queue = + { + queue.Value with Namespace = Managed(namespaces.resourceId this.Name) - } - :> IBuilder + } + :> IBuilder - yield! queue.BuildResources location + yield! queue.BuildResources location - for topic in this.Topics do - let topic = - { topic.Value with + for topic in this.Topics do + let topic = + { + topic.Value with Namespace = Managed(namespaces.resourceId this.Name) - } - :> IBuilder + } + :> IBuilder - yield! topic.BuildResources location + yield! topic.BuildResources location - for rule in this.AuthorizationRules do - { - Name = rule.Key.Map(fun rule -> $"{this.Name.Value}/%s{rule}") - Location = location - Dependencies = [ namespaces.resourceId this.Name ] - Rights = rule.Value - } - ] + for rule in this.AuthorizationRules do + { + Name = rule.Key.Map(fun rule -> $"{this.Name.Value}/%s{rule}") + Location = location + Dependencies = [ namespaces.resourceId this.Name ] + Rights = rule.Value + } + ] type ServiceBusBuilder() = interface IDependable with - member _.Add state newDeps = - { state with + member _.Add state newDeps = { + state with Dependencies = state.Dependencies + newDeps - } - - member _.Yield _ = - { - Name = ResourceName.Empty - Sku = Basic - Queues = Map.empty - Topics = Map.empty - Dependencies = Set.empty - AuthorizationRules = Map.empty - DisablePublicNetworkAccess = None - ZoneRedundant = None - MinTlsVersion = None - Tags = Map.empty } + member _.Yield _ = { + Name = ResourceName.Empty + Sku = Basic + Queues = Map.empty + Topics = Map.empty + Dependencies = Set.empty + AuthorizationRules = Map.empty + DisablePublicNetworkAccess = None + ZoneRedundant = None + MinTlsVersion = None + Tags = Map.empty + } + member _.Run(state: ServiceBusConfig) = match state.ZoneRedundant, state.Sku with | Some FeatureFlag.Enabled, Standard -> @@ -569,10 +560,10 @@ type ServiceBusBuilder() = /// The name of the namespace that holds the queue. [] - member _.NamespaceName(state: ServiceBusConfig, name) = - { state with + member _.NamespaceName(state: ServiceBusConfig, name) = { + state with Name = ServiceBusValidation.ServiceBusName.Create(name).OkValue.ResourceName - } + } /// The SKU of the namespace. [] @@ -589,51 +580,53 @@ type ServiceBusBuilder() = member _.DisablePublicNetworkAccess(state: ServiceBusConfig, ?flag: FeatureFlag) = let flag = defaultArg flag FeatureFlag.Enabled - { state with - DisablePublicNetworkAccess = Some flag + { + state with + DisablePublicNetworkAccess = Some flag } /// Set minimum TLS version [] - member _.SetMinTlsVersion(state: ServiceBusConfig, minTlsVersion: TlsVersion) = - { state with + member _.SetMinTlsVersion(state: ServiceBusConfig, minTlsVersion: TlsVersion) = { + state with MinTlsVersion = Some minTlsVersion - } + } [] - member _.AddQueues(state: ServiceBusConfig, queues) = - { state with + member _.AddQueues(state: ServiceBusConfig, queues) = { + state with Queues = (state.Queues, queues) ||> List.fold (fun queueState (queue: ServiceBusQueueConfig) -> queueState.Add(queue.Name, queue)) - } + } [] - member _.AddTopics(state: ServiceBusConfig, topics) = - { state with + member _.AddTopics(state: ServiceBusConfig, topics) = { + state with Topics = (state.Topics, topics) ||> List.fold (fun topics (topic: ServiceBusTopicConfig) -> topics.Add( topic.Name, - { topic with - Namespace = Managed(namespaces.resourceId state.Name) + { + topic with + Namespace = Managed(namespaces.resourceId state.Name) } )) - } + } /// Add authorization rule on the namespace. [] - member _.AddAuthorizationRule(state: ServiceBusConfig, name, rights) = - { state with + member _.AddAuthorizationRule(state: ServiceBusConfig, name, rights) = { + state with AuthorizationRules = state.AuthorizationRules.Add(ResourceName name, Set rights) - } + } interface ITaggable with - member _.Add state tags = - { state with + member _.Add state tags = { + state with Tags = state.Tags |> Map.merge tags - } + } let serviceBus = ServiceBusBuilder() let topic = ServiceBusTopicBuilder() diff --git a/src/Farmer/Builders/Builders.ServicePlan.fs b/src/Farmer/Builders/Builders.ServicePlan.fs index d9ddf3d9a..b318beae5 100644 --- a/src/Farmer/Builders/Builders.ServicePlan.fs +++ b/src/Farmer/Builders/Builders.ServicePlan.fs @@ -5,48 +5,45 @@ open Farmer open Farmer.WebApp open Arm.Web -type ServicePlanConfig = - { - Name: ResourceName - Sku: Sku - WorkerSize: WorkerSize - WorkerCount: int - MaximumElasticWorkerCount: int option - OperatingSystem: OS - ZoneRedundant: FeatureFlag option - Tags: Map - } +type ServicePlanConfig = { + Name: ResourceName + Sku: Sku + WorkerSize: WorkerSize + WorkerCount: int + MaximumElasticWorkerCount: int option + OperatingSystem: OS + ZoneRedundant: FeatureFlag option + Tags: Map +} with interface IBuilder with member this.ResourceId = serverFarms.resourceId this.Name - member this.BuildResources location = - [ - { - Name = this.Name - Location = location - Sku = this.Sku - WorkerSize = this.WorkerSize - OperatingSystem = this.OperatingSystem - WorkerCount = this.WorkerCount - MaximumElasticWorkerCount = this.MaximumElasticWorkerCount - ZoneRedundant = this.ZoneRedundant - Tags = this.Tags - } - ] + member this.BuildResources location = [ + { + Name = this.Name + Location = location + Sku = this.Sku + WorkerSize = this.WorkerSize + OperatingSystem = this.OperatingSystem + WorkerCount = this.WorkerCount + MaximumElasticWorkerCount = this.MaximumElasticWorkerCount + ZoneRedundant = this.ZoneRedundant + Tags = this.Tags + } + ] type ServicePlanBuilder() = - member _.Yield _ : ServicePlanConfig = - { - Name = ResourceName.Empty - Sku = Free - WorkerSize = Small - WorkerCount = 1 - MaximumElasticWorkerCount = None - OperatingSystem = Windows - ZoneRedundant = None - Tags = Map.empty - } + member _.Yield _ : ServicePlanConfig = { + Name = ResourceName.Empty + Sku = Free + WorkerSize = Small + WorkerCount = 1 + MaximumElasticWorkerCount = None + OperatingSystem = Windows + ZoneRedundant = None + Tags = Map.empty + } [] /// Sets the name of the Server Farm. @@ -62,15 +59,14 @@ type ServicePlanBuilder() = /// Sets the number of instances on the service plan. [] - member _.NumberOfWorkers(state: ServicePlanConfig, workerCount) = - { state with WorkerCount = workerCount } + member _.NumberOfWorkers(state: ServicePlanConfig, workerCount) = { state with WorkerCount = workerCount } /// Sets the maximum number of elastic workers [] - member _.MaximumElasticWorkerCount(state: ServicePlanConfig, maxElasticWorkerCount) = - { state with + member _.MaximumElasticWorkerCount(state: ServicePlanConfig, maxElasticWorkerCount) = { + state with MaximumElasticWorkerCount = Some maxElasticWorkerCount - } + } [] /// Sets the operating system @@ -78,20 +74,19 @@ type ServicePlanBuilder() = [] /// Configures this server farm to host serverless functions, not web apps. - member _.Serverless(state: ServicePlanConfig) = - { state with + member _.Serverless(state: ServicePlanConfig) = { + state with Sku = Dynamic WorkerSize = Serverless - } + } [] - member _.ZoneRedundant(state: ServicePlanConfig, flag: FeatureFlag) = - { state with ZoneRedundant = Some flag } + member _.ZoneRedundant(state: ServicePlanConfig, flag: FeatureFlag) = { state with ZoneRedundant = Some flag } interface ITaggable with - member _.Add state tags = - { state with + member _.Add state tags = { + state with Tags = state.Tags |> Map.merge tags - } + } let servicePlan = ServicePlanBuilder() diff --git a/src/Farmer/Builders/Builders.SignalR.fs b/src/Farmer/Builders/Builders.SignalR.fs index 0d3391701..59b6f083a 100644 --- a/src/Farmer/Builders/Builders.SignalR.fs +++ b/src/Farmer/Builders/Builders.SignalR.fs @@ -7,33 +7,31 @@ open Farmer.Builders open Farmer.Helpers open Farmer.SignalR -type SignalRConfig = - { - Name: ResourceName - Sku: Sku - Capacity: int option - AllowedOrigins: string list - ServiceMode: ServiceMode - Tags: Map - } +type SignalRConfig = { + Name: ResourceName + Sku: Sku + Capacity: int option + AllowedOrigins: string list + ServiceMode: ServiceMode + Tags: Map +} with member this.ResourceId = signalR.resourceId this.Name interface IBuilder with member this.ResourceId = this.ResourceId - member this.BuildResources location = - [ - { - Name = this.Name - Location = location - Sku = this.Sku - Capacity = this.Capacity - AllowedOrigins = this.AllowedOrigins - ServiceMode = this.ServiceMode - Tags = this.Tags - } - ] + member this.BuildResources location = [ + { + Name = this.Name + Location = location + Sku = this.Sku + Capacity = this.Capacity + AllowedOrigins = this.AllowedOrigins + ServiceMode = this.ServiceMode + Tags = this.Tags + } + ] member private this.GetKeyExpr field = let expr = @@ -45,20 +43,19 @@ type SignalRConfig = member this.ConnectionString = this.GetKeyExpr "primaryConnectionString" type SignalRBuilder() = - member _.Yield _ = - { - Name = ResourceName.Empty - Sku = Free - Capacity = None - AllowedOrigins = [] - ServiceMode = Default - Tags = Map.empty - } + member _.Yield _ = { + Name = ResourceName.Empty + Sku = Free + Capacity = None + AllowedOrigins = [] + ServiceMode = Default + Tags = Map.empty + } - member _.Run(state: SignalRConfig) = - { state with + member _.Run(state: SignalRConfig) = { + state with Name = state.Name |> sanitiseSignalR |> ResourceName - } + } /// Sets the name of the Azure SignalR instance. [] @@ -76,20 +73,19 @@ type SignalRBuilder() = /// Sets the allowed origins of the Azure SignalR instance. [] - member _.AllowedOrigins(state: SignalRConfig, allowedOrigins) = - { state with + member _.AllowedOrigins(state: SignalRConfig, allowedOrigins) = { + state with AllowedOrigins = allowedOrigins - } + } /// Sets the service mode of the Azure SignalR instance. [] - member _.ServiceMode(state: SignalRConfig, serviceMode) = - { state with ServiceMode = serviceMode } + member _.ServiceMode(state: SignalRConfig, serviceMode) = { state with ServiceMode = serviceMode } interface ITaggable with - member _.Add state tags = - { state with + member _.Add state tags = { + state with Tags = state.Tags |> Map.merge tags - } + } let signalR = SignalRBuilder() diff --git a/src/Farmer/Builders/Builders.Sql.fs b/src/Farmer/Builders/Builders.Sql.fs index ef0de5830..21037f2de 100644 --- a/src/Farmer/Builders/Builders.Sql.fs +++ b/src/Farmer/Builders/Builders.Sql.fs @@ -8,44 +8,48 @@ open System.Net open Servers open Databases -type SqlAzureDbConfig = - { - Name: ResourceName - Sku: DbPurchaseModel option - MaxSize: int option - Collation: string - Encryption: FeatureFlag - } - -type SqlAzureConfig = - { - Name: SqlAccountName - AdministratorCredentials: {| UserName: string - Password: SecureParameter |} - MinTlsVersion: TlsVersion option - FirewallRules: {| Name: ResourceName - Start: IPAddress - End: IPAddress |} list - ElasticPoolSettings: {| Name: ResourceName option - Sku: PoolSku - PerDbLimits: {| Min: int; Max: int |} option - Capacity: int option |} - Databases: SqlAzureDbConfig list - GeoReplicaServer: GeoReplicationSettings option - Tags: Map - } +type SqlAzureDbConfig = { + Name: ResourceName + Sku: DbPurchaseModel option + MaxSize: int option + Collation: string + Encryption: FeatureFlag +} + +type SqlAzureConfig = { + Name: SqlAccountName + AdministratorCredentials: {| + UserName: string + Password: SecureParameter + |} + MinTlsVersion: TlsVersion option + FirewallRules: + {| + Name: ResourceName + Start: IPAddress + End: IPAddress + |} list + ElasticPoolSettings: {| + Name: ResourceName option + Sku: PoolSku + PerDbLimits: {| Min: int; Max: int |} option + Capacity: int option + |} + Databases: SqlAzureDbConfig list + GeoReplicaServer: GeoReplicationSettings option + Tags: Map +} with /// Gets a basic .NET connection string using the administrator username / password. member this.ConnectionString(database: SqlAzureDbConfig) = let expr = - ArmExpression.concat - [ - ArmExpression.literal - $"Server=tcp:{this.Name.ResourceName.Value}.database.windows.net,1433;Initial Catalog={database.Name.Value};Persist Security Info=False;User ID={this.AdministratorCredentials.UserName};Password=" - this.AdministratorCredentials.Password.ArmExpression - ArmExpression.literal - ";MultipleActiveResultSets=False;Encrypt=True;TrustServerCertificate=False;Connection Timeout=30;" - ] + ArmExpression.concat [ + ArmExpression.literal + $"Server=tcp:{this.Name.ResourceName.Value}.database.windows.net,1433;Initial Catalog={database.Name.Value};Persist Security Info=False;User ID={this.AdministratorCredentials.UserName};Password=" + this.AdministratorCredentials.Password.ArmExpression + ArmExpression.literal + ";MultipleActiveResultSets=False;Encrypt=True;TrustServerCertificate=False;Connection Timeout=30;" + ] expr.WithOwner(databases.resourceId (this.Name.ResourceName, database.Name)) @@ -64,150 +68,140 @@ type SqlAzureConfig = interface IBuilder with member this.ResourceId = servers.resourceId this.Name.ResourceName - member this.BuildResources location = - [ - let elasticPoolName = - this.ElasticPoolSettings.Name - |> Option.defaultValue (this.Name.ResourceName - "pool") + member this.BuildResources location = [ + let elasticPoolName = + this.ElasticPoolSettings.Name + |> Option.defaultValue (this.Name.ResourceName - "pool") + + { + ServerName = this.Name + Location = location + Credentials = {| + Username = this.AdministratorCredentials.UserName + Password = this.AdministratorCredentials.Password + |} + MinTlsVersion = this.MinTlsVersion + Tags = this.Tags + } + for database in this.Databases do { - ServerName = this.Name + Name = database.Name + Server = this.Name Location = location - Credentials = - {| - Username = this.AdministratorCredentials.UserName - Password = this.AdministratorCredentials.Password - |} - MinTlsVersion = this.MinTlsVersion - Tags = this.Tags + MaxSizeBytes = + match database.Sku, database.MaxSize with + | Some _, Some maxSize -> Some(Mb.toBytes maxSize) + | _ -> None + Sku = + match database.Sku with + | Some dbSku -> Standalone dbSku + | None -> Pool elasticPoolName + Collation = database.Collation } - for database in this.Databases do - { - Name = database.Name - Server = this.Name - Location = location - MaxSizeBytes = - match database.Sku, database.MaxSize with - | Some _, Some maxSize -> Some(Mb.toBytes maxSize) - | _ -> None - Sku = - match database.Sku with - | Some dbSku -> Standalone dbSku - | None -> Pool elasticPoolName - Collation = database.Collation - } + match database.Encryption with + | Enabled -> { + Server = this.Name + Database = database.Name + } + | Disabled -> () - match database.Encryption with - | Enabled -> - { - Server = this.Name - Database = database.Name - } - | Disabled -> () + for rule in this.FirewallRules do + { + Name = rule.Name + Start = rule.Start + End = rule.End + Location = location + Server = this.Name + } - for rule in this.FirewallRules do - { - Name = rule.Name - Start = rule.Start - End = rule.End - Location = location - Server = this.Name - } + if this.Databases |> List.exists (fun db -> db.Sku.IsNone) then + { + Name = elasticPoolName + Server = this.Name + Location = location + Sku = this.ElasticPoolSettings.Sku + MaxSizeBytes = this.ElasticPoolSettings.Capacity |> Option.map Mb.toBytes + MinMax = this.ElasticPoolSettings.PerDbLimits |> Option.map (fun l -> l.Min, l.Max) + } + + match this.GeoReplicaServer with + | Some replica -> + if replica.Location.ArmValue = location.ArmValue then + raiseFarmer + $"Geo-replica cannot be deployed to the same location than the main database {this.Name}: {location.ArmValue}" + else + let replicaServerName = + match (this.Name.ResourceName.Value + replica.NameSuffix) |> SqlAccountName.Create with + | Ok x -> x + | Error e -> raiseFarmer e - if this.Databases |> List.exists (fun db -> db.Sku.IsNone) then { - Name = elasticPoolName - Server = this.Name - Location = location - Sku = this.ElasticPoolSettings.Sku - MaxSizeBytes = this.ElasticPoolSettings.Capacity |> Option.map Mb.toBytes - MinMax = this.ElasticPoolSettings.PerDbLimits |> Option.map (fun l -> l.Min, l.Max) + ServerName = replicaServerName + Location = replica.Location + Credentials = {| + Username = this.AdministratorCredentials.UserName + Password = this.AdministratorCredentials.Password + |} + MinTlsVersion = this.MinTlsVersion + Tags = this.Tags } - match this.GeoReplicaServer with - | Some replica -> - if replica.Location.ArmValue = location.ArmValue then - raiseFarmer - $"Geo-replica cannot be deployed to the same location than the main database {this.Name}: {location.ArmValue}" - else - let replicaServerName = - match (this.Name.ResourceName.Value + replica.NameSuffix) |> SqlAccountName.Create with - | Ok x -> x - | Error e -> raiseFarmer e - - { - ServerName = replicaServerName - Location = replica.Location - Credentials = - {| - Username = this.AdministratorCredentials.UserName - Password = this.AdministratorCredentials.Password - |} - MinTlsVersion = this.MinTlsVersion - Tags = this.Tags - } - - for database in this.Databases do - let geoSku = - match replica.DbSku, database.Sku with - | Some relicaSku, _ -> relicaSku.Name, relicaSku.Edition - | None, Some dbSku -> dbSku.Name, dbSku.Edition - | None, None -> this.ElasticPoolSettings.Sku.Name, this.ElasticPoolSettings.Sku.Edition - - let primaryDatabaseFullId = - ArmExpression - .create( - $"concat('/subscriptions/', subscription().subscriptionId, '/resourceGroups/', resourceGroup().name, '/providers/Microsoft.Sql/servers/', '{this.Name.ResourceName.Value}', '/databases/','{database.Name.Value}')" - ) - .Eval() + for database in this.Databases do + let geoSku = + match replica.DbSku, database.Sku with + | Some relicaSku, _ -> relicaSku.Name, relicaSku.Edition + | None, Some dbSku -> dbSku.Name, dbSku.Edition + | None, None -> this.ElasticPoolSettings.Sku.Name, this.ElasticPoolSettings.Sku.Edition + + let primaryDatabaseFullId = + ArmExpression + .create( + $"concat('/subscriptions/', subscription().subscriptionId, '/resourceGroups/', resourceGroup().name, '/providers/Microsoft.Sql/servers/', '{this.Name.ResourceName.Value}', '/databases/','{database.Name.Value}')" + ) + .Eval() - {| - apiVersion = "2021-02-01-preview" - location = replica.Location.ArmValue - dependsOn = - [ - Farmer - .ResourceId - .create(Farmer.Arm.Sql.servers, replicaServerName.ResourceName) - .Eval() - primaryDatabaseFullId - ] - name = - $"{replicaServerName.ResourceName.Value}/{database.Name.Value + replica.NameSuffix}" - ``type`` = Farmer.Arm.Sql.databases.Type - sku = - {| - name = fst geoSku - tier = snd geoSku - |} - properties = - {| - createMode = "OnlineSecondary" - secondaryType = "Geo" - sourceDatabaseId = primaryDatabaseFullId - zoneRedundant = false - licenseType = "" - readScale = "Disabled" - highAvailabilityReplicaCount = 0 - minCapacity = "" - autoPauseDelay = "" - requestedBackupStorageRedundancy = "" - |} + {| + apiVersion = "2021-02-01-preview" + location = replica.Location.ArmValue + dependsOn = [ + Farmer.ResourceId + .create(Farmer.Arm.Sql.servers, replicaServerName.ResourceName) + .Eval() + primaryDatabaseFullId + ] + name = $"{replicaServerName.ResourceName.Value}/{database.Name.Value + replica.NameSuffix}" + ``type`` = Farmer.Arm.Sql.databases.Type + sku = {| + name = fst geoSku + tier = snd geoSku |} - |> Farmer.Resource.ofObj - | None -> () - ] + properties = {| + createMode = "OnlineSecondary" + secondaryType = "Geo" + sourceDatabaseId = primaryDatabaseFullId + zoneRedundant = false + licenseType = "" + readScale = "Disabled" + highAvailabilityReplicaCount = 0 + minCapacity = "" + autoPauseDelay = "" + requestedBackupStorageRedundancy = "" + |} + |} + |> Farmer.Resource.ofObj + | None -> () + ] type SqlDbBuilder() = - member _.Yield _ = - { - Name = ResourceName "" - Collation = "SQL_Latin1_General_CP1_CI_AS" - Sku = None - MaxSize = None - Encryption = Disabled - } + member _.Yield _ = { + Name = ResourceName "" + Collation = "SQL_Latin1_General_CP1_CI_AS" + Sku = None + MaxSize = None + Encryption = Disabled + } /// Sets the name of the database. [] @@ -219,20 +213,20 @@ type SqlDbBuilder() = [] member _.DbSku(state: SqlAzureDbConfig, sku: DtuSku) = { state with Sku = Some(DTU sku) } - member _.DbSku(state: SqlAzureDbConfig, sku: MSeries) = - { state with + member _.DbSku(state: SqlAzureDbConfig, sku: MSeries) = { + state with Sku = Some(VCore(MemoryIntensive sku, LicenseRequired)) - } + } - member _.DbSku(state: SqlAzureDbConfig, sku: FSeries) = - { state with + member _.DbSku(state: SqlAzureDbConfig, sku: FSeries) = { + state with Sku = Some(VCore(CpuIntensive sku, LicenseRequired)) - } + } - member _.DbSku(state: SqlAzureDbConfig, sku: VCoreSku) = - { state with + member _.DbSku(state: SqlAzureDbConfig, sku: VCoreSku) = { + state with Sku = Some(VCore(sku, LicenseRequired)) - } + } /// Sets the collation of the database. [] @@ -240,16 +234,16 @@ type SqlDbBuilder() = /// States that you already have a SQL license and qualify for Azure Hybrid Benefit discount. [] - member _.ZoneRedundant(state: SqlAzureDbConfig) = - { state with + member _.ZoneRedundant(state: SqlAzureDbConfig) = { + state with Sku = match state.Sku with - | Some (VCore (v, _)) -> Some(VCore(v, AzureHybridBenefit)) - | Some (DTU _) + | Some(VCore(v, _)) -> Some(VCore(v, AzureHybridBenefit)) + | Some(DTU _) | None -> raiseFarmer "You can only set licensing on VCore databases. Ensure that you have already set the SKU to a VCore model." - } + } /// Sets the maximum size of the database, if this database is not part of an elastic pool. [] @@ -269,41 +263,40 @@ type SqlDbBuilder() = type SqlServerBuilder() = let makeIp (text: string) = IPAddress.Parse text - member _.Yield _ = - { - Name = SqlAccountName.Empty - AdministratorCredentials = - {| - UserName = "" - Password = SecureParameter "" - |} - ElasticPoolSettings = - {| - Name = None - Sku = PoolSku.Basic50 - PerDbLimits = None - Capacity = None - |} - Databases = [] - FirewallRules = [] - MinTlsVersion = None - GeoReplicaServer = None - Tags = Map.empty - } + member _.Yield _ = { + Name = SqlAccountName.Empty + AdministratorCredentials = {| + UserName = "" + Password = SecureParameter "" + |} + ElasticPoolSettings = {| + Name = None + Sku = PoolSku.Basic50 + PerDbLimits = None + Capacity = None + |} + Databases = [] + FirewallRules = [] + MinTlsVersion = None + GeoReplicaServer = None + Tags = Map.empty + } member _.Run state : SqlAzureConfig = if state.Name.ResourceName = ResourceName.Empty then raiseFarmer "No SQL Server account name has been set." - { state with - AdministratorCredentials = - if System.String.IsNullOrWhiteSpace state.AdministratorCredentials.UserName then - raiseFarmer - $"You must specify the admin_username for SQL Server instance {state.Name.ResourceName.Value}" + { + state with + AdministratorCredentials = + if System.String.IsNullOrWhiteSpace state.AdministratorCredentials.UserName then + raiseFarmer + $"You must specify the admin_username for SQL Server instance {state.Name.ResourceName.Value}" - {| state.AdministratorCredentials with - Password = SecureParameter state.PasswordParameter - |} + {| + state.AdministratorCredentials with + Password = SecureParameter state.PasswordParameter + |} } /// Sets the name of the SQL server. @@ -315,57 +308,57 @@ type SqlServerBuilder() = /// Sets the name of the elastic pool. If not set, the name will be generated based off the server name. [] - member _.Name(state: SqlAzureConfig, name) = - { state with - ElasticPoolSettings = - {| state.ElasticPoolSettings with + member _.Name(state: SqlAzureConfig, name) = { + state with + ElasticPoolSettings = {| + state.ElasticPoolSettings with Name = Some name - |} - } + |} + } member this.Name(state, name) = this.Name(state, ResourceName name) /// Sets the sku of the server, to be shared on all databases that do not have an explicit sku set. [] - member _.Sku(state: SqlAzureConfig, sku) = - { state with - ElasticPoolSettings = - {| state.ElasticPoolSettings with + member _.Sku(state: SqlAzureConfig, sku) = { + state with + ElasticPoolSettings = {| + state.ElasticPoolSettings with Sku = sku - |} - } + |} + } /// The per-database min and max DTUs to allocate. [] - member _.PerDbLimits(state: SqlAzureConfig, min, max) = - { state with - ElasticPoolSettings = - {| state.ElasticPoolSettings with + member _.PerDbLimits(state: SqlAzureConfig, min, max) = { + state with + ElasticPoolSettings = {| + state.ElasticPoolSettings with PerDbLimits = Some {| Min = min; Max = max |} - |} - } + |} + } /// The per-database min and max DTUs to allocate. [] - member _.PoolCapacity(state: SqlAzureConfig, capacity) = - { state with - ElasticPoolSettings = - {| state.ElasticPoolSettings with + member _.PoolCapacity(state: SqlAzureConfig, capacity) = { + state with + ElasticPoolSettings = {| + state.ElasticPoolSettings with Capacity = Some capacity - |} - } + |} + } /// The per-database min and max DTUs to allocate. [] - member _.AddDatabases(state: SqlAzureConfig, databases) = - { state with + member _.AddDatabases(state: SqlAzureConfig, databases) = { + state with Databases = state.Databases @ databases - } + } /// Adds a firewall rule that enables access to a specific IP Address range. [] - member _.AddFirewallRule(state: SqlAzureConfig, name, startRange, endRange) = - { state with + member _.AddFirewallRule(state: SqlAzureConfig, name, startRange, endRange) = { + state with FirewallRules = {| Name = ResourceName name @@ -373,22 +366,22 @@ type SqlServerBuilder() = End = makeIp endRange |} :: state.FirewallRules - } + } /// Adds a firewall rules that enables access to a specific IP Address range. [] member _.AddFirewallRules(state: SqlAzureConfig, listOfRules: (string * string * string) list) = let newRules = listOfRules - |> List.map (fun (name, startRange, endRange) -> - {| - Name = ResourceName name - Start = makeIp startRange - End = makeIp endRange - |}) + |> List.map (fun (name, startRange, endRange) -> {| + Name = ResourceName name + Start = makeIp startRange + End = makeIp endRange + |}) - { state with - FirewallRules = newRules @ state.FirewallRules + { + state with + FirewallRules = newRules @ state.FirewallRules } /// Adds a firewall rule that enables access to other Azure services. @@ -398,33 +391,33 @@ type SqlServerBuilder() = /// Sets the admin username of the server (note: the password is supplied as a securestring parameter to the generated ARM template). [] - member _.AdminUsername(state: SqlAzureConfig, username) = - { state with - AdministratorCredentials = - {| state.AdministratorCredentials with + member _.AdminUsername(state: SqlAzureConfig, username) = { + state with + AdministratorCredentials = {| + state.AdministratorCredentials with UserName = username - |} - } + |} + } /// Set minimum TLS version [] - member _.SetMinTlsVersion(state: SqlAzureConfig, minTlsVersion) = - { state with + member _.SetMinTlsVersion(state: SqlAzureConfig, minTlsVersion) = { + state with MinTlsVersion = Some minTlsVersion - } + } /// Geo-replicate all the databases in this server to another location, having NameSuffix after original server and database names. [] - member _.SetGeoReplication(state: SqlAzureConfig, replicaSettings) = - { state with + member _.SetGeoReplication(state: SqlAzureConfig, replicaSettings) = { + state with GeoReplicaServer = Some replicaSettings - } + } interface ITaggable with - member _.Add state tags = - { state with + member _.Add state tags = { + state with Tags = state.Tags |> Map.merge tags - } + } let sqlServer = SqlServerBuilder() let sqlDb = SqlDbBuilder() diff --git a/src/Farmer/Builders/Builders.StaticWebApp.fs b/src/Farmer/Builders/Builders.StaticWebApp.fs index 979196a57..9a19442cf 100644 --- a/src/Farmer/Builders/Builders.StaticWebApp.fs +++ b/src/Farmer/Builders/Builders.StaticWebApp.fs @@ -6,95 +6,92 @@ open Farmer.Arm.Web open Farmer.Arm.Web.StaticSites open System -type StaticWebAppConfig = - { - Name: ResourceName - Repository: Uri option - Branch: string - RepositoryToken: SecureParameter - AppLocation: string - ApiLocation: string option - AppArtifactLocation: string option - AppSettings: Map - } +type StaticWebAppConfig = { + Name: ResourceName + Repository: Uri option + Branch: string + RepositoryToken: SecureParameter + AppLocation: string + ApiLocation: string option + AppArtifactLocation: string option + AppSettings: Map +} with interface IBuilder with member this.ResourceId = staticSites.resourceId this.Name - member this.BuildResources location = - [ - match this with - | { Repository = Some uri } -> + member this.BuildResources location = [ + match this with + | { Repository = Some uri } -> + { + Name = this.Name + Location = location + Repository = uri + Branch = this.Branch + RepositoryToken = this.RepositoryToken + AppLocation = this.AppLocation + ApiLocation = this.ApiLocation + AppArtifactLocation = this.AppArtifactLocation + } + + if not this.AppSettings.IsEmpty then { - Name = this.Name - Location = location - Repository = uri - Branch = this.Branch - RepositoryToken = this.RepositoryToken - AppLocation = this.AppLocation - ApiLocation = this.ApiLocation - AppArtifactLocation = this.AppArtifactLocation + Config.StaticSite = this.Name + Properties = this.AppSettings } - - if not this.AppSettings.IsEmpty then - { - Config.StaticSite = this.Name - Properties = this.AppSettings - } - | _ -> raiseFarmer "You must set the repository URI." - ] + | _ -> raiseFarmer "You must set the repository URI." + ] member this.RepositoryParameter = $"repositorytoken-for-{this.Name.Value}" type StaticWebAppBuilder() = - member _.Yield _ = - { - Name = ResourceName.Empty - Repository = None - Branch = "master" - RepositoryToken = SecureParameter "" - AppLocation = "" - ApiLocation = None - AppArtifactLocation = None - AppSettings = Map.empty - } - - member _.Run(state: StaticWebAppConfig) = - { state with + member _.Yield _ = { + Name = ResourceName.Empty + Repository = None + Branch = "master" + RepositoryToken = SecureParameter "" + AppLocation = "" + ApiLocation = None + AppArtifactLocation = None + AppSettings = Map.empty + } + + member _.Run(state: StaticWebAppConfig) = { + state with RepositoryToken = SecureParameter state.RepositoryParameter - } + } [] member _.Name(state: StaticWebAppConfig, name) = { state with Name = ResourceName name } [] - member _.Repository(state: StaticWebAppConfig, uri) = - { state with + member _.Repository(state: StaticWebAppConfig, uri) = { + state with Repository = Some(Uri uri) - } + } [] member _.Branch(state: StaticWebAppConfig, branch) = { state with Branch = branch } [] - member _.ApiLocation(state: StaticWebAppConfig, location) = - { state with + member _.ApiLocation(state: StaticWebAppConfig, location) = { + state with ApiLocation = Some location - } + } [] member _.AppLocation(state: StaticWebAppConfig, location) = { state with AppLocation = location } [] - member _.ArtifactLocation(state: StaticWebAppConfig, location) = - { state with + member _.ArtifactLocation(state: StaticWebAppConfig, location) = { + state with AppArtifactLocation = Some location - } + } [] - member _.AppSettings(state: StaticWebAppConfig, settings) = - { state with + member _.AppSettings(state: StaticWebAppConfig, settings) = { + state with AppSettings = Map settings - } + } let staticWebApp = StaticWebAppBuilder() diff --git a/src/Farmer/Builders/Builders.Storage.fs b/src/Farmer/Builders/Builders.Storage.fs index 109a32a05..c8b37924e 100644 --- a/src/Farmer/Builders/Builders.Storage.fs +++ b/src/Farmer/Builders/Builders.Storage.fs @@ -24,61 +24,62 @@ type StorageAccount = StorageAccount.getConnectionString(resourceId).WithOwner(resourceId) -type StoragePolicy = - { - CoolBlobAfter: int option - ArchiveBlobAfter: int option - DeleteBlobAfter: int option - DeleteSnapshotAfter: int option - Filters: string list - } - -type StorageAccountConfig = - { - /// The name of the storage account. - Name: StorageAccountName - /// The sku of the storage account. - Sku: Sku - /// Whether to enable Data Lake Storage Gen2. - EnableDataLake: bool option - /// Containers for the storage account. - Containers: (StorageResourceName * StorageContainerAccess) list - /// File shares - FileShares: (StorageResourceName * int option) list - /// Queues - Queues: StorageResourceName Set - /// Network Access Control Lists - NetworkAcls: NetworkRuleSet option - /// Tables - Tables: StorageResourceName Set - /// Rules - Rules: Map - RoleAssignments: Roles.RoleAssignment Set - /// Static Website Settings - StaticWebsite: {| IndexPage: string - ContentPath: string - ErrorPage: string option |} option - /// The CORS rules for a storage service - CorsRules: List - /// The Policies for a storage service - Policies: List - /// Versioning enable information for a storage service - IsVersioningEnabled: List - /// Minimum TLS version - MinTlsVersion: TlsVersion option - /// Tags to apply to the storage account - Tags: Map - /// DNS endpoint type - DnsZoneType: string option - /// Disable Public Network Acccess - DisablePublicNetworkAccess: FeatureFlag option - /// Disable blob public access - DisableBlobPublicAccess: FeatureFlag option - /// Disable Shared Key Access - DisableSharedKeyAccess: FeatureFlag option - /// Default to Azure Active Directory authorization in the Azure portal - DefaultToOAuthAuthentication: FeatureFlag option - } +type StoragePolicy = { + CoolBlobAfter: int option + ArchiveBlobAfter: int option + DeleteBlobAfter: int option + DeleteSnapshotAfter: int option + Filters: string list +} + +type StorageAccountConfig = { + /// The name of the storage account. + Name: StorageAccountName + /// The sku of the storage account. + Sku: Sku + /// Whether to enable Data Lake Storage Gen2. + EnableDataLake: bool option + /// Containers for the storage account. + Containers: (StorageResourceName * StorageContainerAccess) list + /// File shares + FileShares: (StorageResourceName * int option) list + /// Queues + Queues: StorageResourceName Set + /// Network Access Control Lists + NetworkAcls: NetworkRuleSet option + /// Tables + Tables: StorageResourceName Set + /// Rules + Rules: Map + RoleAssignments: Roles.RoleAssignment Set + /// Static Website Settings + StaticWebsite: + {| + IndexPage: string + ContentPath: string + ErrorPage: string option + |} option + /// The CORS rules for a storage service + CorsRules: List + /// The Policies for a storage service + Policies: List + /// Versioning enable information for a storage service + IsVersioningEnabled: List + /// Minimum TLS version + MinTlsVersion: TlsVersion option + /// Tags to apply to the storage account + Tags: Map + /// DNS endpoint type + DnsZoneType: string option + /// Disable Public Network Acccess + DisablePublicNetworkAccess: FeatureFlag option + /// Disable blob public access + DisableBlobPublicAccess: FeatureFlag option + /// Disable Shared Key Access + DisableSharedKeyAccess: FeatureFlag option + /// Default to Azure Active Directory authorization in the Azure portal + DefaultToOAuthAuthentication: FeatureFlag option +} with /// Gets the ARM expression path to the key of this storage account. member this.Key = StorageAccount.getConnectionString (this.Name) @@ -98,150 +99,145 @@ type StorageAccountConfig = interface IBuilder with member this.ResourceId = this.ResourceId - member this.BuildResources location = - [ + member this.BuildResources location = [ + { + Name = this.Name + Location = location + Sku = this.Sku + EnableHierarchicalNamespace = this.EnableDataLake + Dependencies = + this.RoleAssignments + |> Seq.choose (fun roleAssignment -> roleAssignment.Principal.ArmExpression.Owner) + |> Seq.append ( + match this.NetworkAcls with + | Some acl -> + acl.VirtualNetworkRules + |> Seq.map (fun r -> r.VirtualNetwork) + |> Seq.distinct + |> Seq.map Arm.Network.virtualNetworks.resourceId + | None -> Seq.empty + ) + |> Seq.toList + NetworkAcls = this.NetworkAcls + StaticWebsite = this.StaticWebsite + MinTlsVersion = this.MinTlsVersion + DnsZoneType = this.DnsZoneType + DisablePublicNetworkAccess = this.DisablePublicNetworkAccess + DisableBlobPublicAccess = this.DisableBlobPublicAccess + DisableSharedKeyAccess = this.DisableSharedKeyAccess + DefaultToOAuthAuthentication = this.DefaultToOAuthAuthentication + Tags = this.Tags + } + for name, access in this.Containers do { - Name = this.Name - Location = location - Sku = this.Sku - EnableHierarchicalNamespace = this.EnableDataLake - Dependencies = - this.RoleAssignments - |> Seq.choose (fun roleAssignment -> roleAssignment.Principal.ArmExpression.Owner) - |> Seq.append ( - match this.NetworkAcls with - | Some acl -> - acl.VirtualNetworkRules - |> Seq.map (fun r -> r.VirtualNetwork) - |> Seq.distinct - |> Seq.map Arm.Network.virtualNetworks.resourceId - | None -> Seq.empty - ) - |> Seq.toList - NetworkAcls = this.NetworkAcls - StaticWebsite = this.StaticWebsite - MinTlsVersion = this.MinTlsVersion - DnsZoneType = this.DnsZoneType - DisablePublicNetworkAccess = this.DisablePublicNetworkAccess - DisableBlobPublicAccess = this.DisableBlobPublicAccess - DisableSharedKeyAccess = this.DisableSharedKeyAccess - DefaultToOAuthAuthentication = this.DefaultToOAuthAuthentication - Tags = this.Tags + Name = name + StorageAccount = this.Name.ResourceName + Accessibility = access } - for name, access in this.Containers do - { - Name = name - StorageAccount = this.Name.ResourceName - Accessibility = access - } - for (name, shareQuota) in this.FileShares do - { - Name = name - ShareQuota = shareQuota - StorageAccount = this.Name.ResourceName - } - for queue in this.Queues do - { - Queues.Queue.Name = queue - Queues.Queue.StorageAccount = this.Name.ResourceName - } - for table in this.Tables do - { - Tables.Table.Name = table - Tables.Table.StorageAccount = this.Name.ResourceName - } - match this.Rules |> Map.toList with - | [] -> () - | rules -> - { - ManagementPolicies.ManagementPolicy.StorageAccount = this.Name.ResourceName - ManagementPolicies.ManagementPolicy.Rules = - [ - for name, rule in rules do - {| rule with Name = name |} - ] - } - for roleAssignment in this.RoleAssignments do - let uniqueName = - $"{this.Name.ResourceName.Value}{roleAssignment.Principal.ArmExpression.Value}{roleAssignment.Role.Id}" - |> DeterministicGuid.create - |> string - |> ResourceName + for (name, shareQuota) in this.FileShares do + { + Name = name + ShareQuota = shareQuota + StorageAccount = this.Name.ResourceName + } + for queue in this.Queues do + { + Queues.Queue.Name = queue + Queues.Queue.StorageAccount = this.Name.ResourceName + } + for table in this.Tables do + { + Tables.Table.Name = table + Tables.Table.StorageAccount = this.Name.ResourceName + } + match this.Rules |> Map.toList with + | [] -> () + | rules -> { + ManagementPolicies.ManagementPolicy.StorageAccount = this.Name.ResourceName + ManagementPolicies.ManagementPolicy.Rules = [ + for name, rule in rules do + {| rule with Name = name |} + ] + } + for roleAssignment in this.RoleAssignments do + let uniqueName = + $"{this.Name.ResourceName.Value}{roleAssignment.Principal.ArmExpression.Value}{roleAssignment.Role.Id}" + |> DeterministicGuid.create + |> string + |> ResourceName - { - Name = uniqueName - RoleDefinitionId = roleAssignment.Role - PrincipalId = roleAssignment.Principal - PrincipalType = PrincipalType.ServicePrincipal - Scope = ResourceGroup - Dependencies = - Set - [ - ResourceId.create (storageAccounts, this.Name.ResourceName) - yield! roleAssignment.Owner |> Option.toList - ] - } + { + Name = uniqueName + RoleDefinitionId = roleAssignment.Role + PrincipalId = roleAssignment.Principal + PrincipalType = PrincipalType.ServicePrincipal + Scope = ResourceGroup + Dependencies = + Set [ + ResourceId.create (storageAccounts, this.Name.ResourceName) + yield! roleAssignment.Owner |> Option.toList + ] + } - let storageResourceName = StorageResourceName.Create(this.Name.ResourceName).OkValue + let storageResourceName = StorageResourceName.Create(this.Name.ResourceName).OkValue - let rules = this.CorsRules |> List.groupBy fst - let versioning = this.IsVersioningEnabled |> List.groupBy fst - let policies = this.Policies |> List.groupBy fst + let rules = this.CorsRules |> List.groupBy fst + let versioning = this.IsVersioningEnabled |> List.groupBy fst + let policies = this.Policies |> List.groupBy fst - let allSvcs = - rules - |> List.map fst - |> (@) (versioning |> List.map fst) - |> (@) (policies |> List.map fst) - |> List.distinct + let allSvcs = + rules + |> List.map fst + |> (@) (versioning |> List.map fst) + |> (@) (policies |> List.map fst) + |> List.distinct - for svc in allSvcs do - { - ResourceType = - match svc with - | StorageService.Blobs -> blobServices - | StorageService.Queues -> queueServices - | StorageService.Tables -> tableServices - | StorageService.Files -> fileServices - StorageAccount = storageResourceName - CorsRules = this.CorsRules |> List.filter (fst >> (=) svc) |> List.map (fun (_, s) -> s) - Policies = - this.Policies - |> List.filter (fst >> (=) svc) - |> List.map (fun (_, s) -> s) - |> List.collect id - IsVersioningEnabled = - this.IsVersioningEnabled - |> List.filter (fst >> (=) svc) - |> List.forall (fun (_, ive) -> ive = true) - } - ] + for svc in allSvcs do + { + ResourceType = + match svc with + | StorageService.Blobs -> blobServices + | StorageService.Queues -> queueServices + | StorageService.Tables -> tableServices + | StorageService.Files -> fileServices + StorageAccount = storageResourceName + CorsRules = this.CorsRules |> List.filter (fst >> (=) svc) |> List.map (fun (_, s) -> s) + Policies = + this.Policies + |> List.filter (fst >> (=) svc) + |> List.map (fun (_, s) -> s) + |> List.collect id + IsVersioningEnabled = + this.IsVersioningEnabled + |> List.filter (fst >> (=) svc) + |> List.forall (fun (_, ive) -> ive = true) + } + ] type StorageAccountBuilder() = - member _.Yield _ = - { - Name = StorageAccountName.Empty - Sku = Sku.Standard_LRS - EnableDataLake = None - Containers = [] - FileShares = [] - Rules = Map.empty - Queues = Set.empty - NetworkAcls = None - Tables = Set.empty - RoleAssignments = Set.empty - StaticWebsite = None - CorsRules = [] - Policies = [] - IsVersioningEnabled = [] - MinTlsVersion = None - Tags = Map.empty - DnsZoneType = None - DisablePublicNetworkAccess = None - DisableBlobPublicAccess = None - DisableSharedKeyAccess = None - DefaultToOAuthAuthentication = None - } + member _.Yield _ = { + Name = StorageAccountName.Empty + Sku = Sku.Standard_LRS + EnableDataLake = None + Containers = [] + FileShares = [] + Rules = Map.empty + Queues = Set.empty + NetworkAcls = None + Tables = Set.empty + RoleAssignments = Set.empty + StaticWebsite = None + CorsRules = [] + Policies = [] + IsVersioningEnabled = [] + MinTlsVersion = None + Tags = Map.empty + DnsZoneType = None + DisablePublicNetworkAccess = None + DisableBlobPublicAccess = None + DisableSharedKeyAccess = None + DefaultToOAuthAuthentication = None + } member _.Run state = if state.Name.ResourceName = ResourceName.Empty then @@ -249,22 +245,22 @@ type StorageAccountBuilder() = state - static member private AddContainer(state, access, name: string) = - { state with + static member private AddContainer(state, access, name: string) = { + state with Containers = state.Containers @ [ ((StorageResourceName.Create name).OkValue, access) ] - } + } - static member private AddFileShare(state: StorageAccountConfig, name: string, quota) = - { state with + static member private AddFileShare(state: StorageAccountConfig, name: string, quota) = { + state with FileShares = state.FileShares @ [ (StorageResourceName.Create(name).OkValue, quota) ] - } + } /// Sets the name of the storage account. [] - member _.Name(state: StorageAccountConfig, name: ResourceName) = - { state with + member _.Name(state: StorageAccountConfig, name: ResourceName) = { + state with Name = StorageAccountName.Create(name).OkValue - } + } member this.Name(state: StorageAccountConfig, name) = this.Name(state, ResourceName name) @@ -299,10 +295,10 @@ type StorageAccountBuilder() = /// Adds a single queue to the storage account. [] - member _.AddQueue(state: StorageAccountConfig, name: string) = - { state with + member _.AddQueue(state: StorageAccountConfig, name: string) = { + state with Queues = state.Queues.Add(StorageResourceName.Create(name).OkValue) - } + } /// Adds a set of queues to the storage account. [] @@ -311,10 +307,10 @@ type StorageAccountBuilder() = /// Adds a single table to the storage account. [] - member _.AddTable(state: StorageAccountConfig, name: string) = - { state with + member _.AddTable(state: StorageAccountConfig, name: string) = { + state with Tables = state.Tables.Add(StorageResourceName.Create(name).OkValue) - } + } /// Adds a set of tables to the storage account. [] @@ -323,73 +319,72 @@ type StorageAccountBuilder() = /// Enable static website support, using the supplied local content path to the storage account's $web folder as a post-deployment task, and setting the index page as supplied. [] - member _.StaticWebsite(state: StorageAccountConfig, contentPath, indexPage) = - { state with + member _.StaticWebsite(state: StorageAccountConfig, contentPath, indexPage) = { + state with StaticWebsite = - Some - {| - IndexPage = indexPage - ErrorPage = None - ContentPath = contentPath - |} - } + Some {| + IndexPage = indexPage + ErrorPage = None + ContentPath = contentPath + |} + } /// Sets the error page for the static website. [] - member _.StaticWebsiteErrorPage(state: StorageAccountConfig, errorPage) = - { state with + member _.StaticWebsiteErrorPage(state: StorageAccountConfig, errorPage) = { + state with StaticWebsite = state.StaticWebsite - |> Option.map (fun staticWebsite -> - {| staticWebsite with + |> Option.map (fun staticWebsite -> {| + staticWebsite with ErrorPage = Some errorPage - |}) - } + |}) + } /// Enables support for hierarchical namespace, also known as Data Lake Storage Gen2. [] - member _.UseHns(state: StorageAccountConfig, value) = - { state with + member _.UseHns(state: StorageAccountConfig, value) = { + state with EnableDataLake = Some value - } + } /// Adds tags to the storage account /// Adds a lifecycle rule [] member _.AddLifecycleRule(state: StorageAccountConfig, ruleName, actions, filters) = - let rule = - { - Filters = filters - CoolBlobAfter = - actions - |> List.tryPick (function - | CoolAfter days -> Some days - | _ -> None) - ArchiveBlobAfter = - actions - |> List.tryPick (function - | ArchiveAfter days -> Some days - | _ -> None) - DeleteBlobAfter = - actions - |> List.tryPick (function - | DeleteAfter days -> Some days - | _ -> None) - DeleteSnapshotAfter = - actions - |> List.tryPick (function - | DeleteSnapshotAfter days -> Some days - | _ -> None) - } + let rule = { + Filters = filters + CoolBlobAfter = + actions + |> List.tryPick (function + | CoolAfter days -> Some days + | _ -> None) + ArchiveBlobAfter = + actions + |> List.tryPick (function + | ArchiveAfter days -> Some days + | _ -> None) + DeleteBlobAfter = + actions + |> List.tryPick (function + | DeleteAfter days -> Some days + | _ -> None) + DeleteSnapshotAfter = + actions + |> List.tryPick (function + | DeleteSnapshotAfter days -> Some days + | _ -> None) + } - { state with - Rules = state.Rules.Add(ResourceName ruleName, rule) + { + state with + Rules = state.Rules.Add(ResourceName ruleName, rule) } - static member private GrantAccess(state: StorageAccountConfig, assignment) = - { state with + static member private GrantAccess(state: StorageAccountConfig, assignment) = { + state with RoleAssignments = state.RoleAssignments.Add assignment - } + } [] member _.GrantAccess(state: StorageAccountConfig, principalId: PrincipalId, role) = @@ -423,37 +418,36 @@ type StorageAccountBuilder() = ) [] - member _.SetDefaultAccessTier(state: StorageAccountConfig, tier) = - { state with + member _.SetDefaultAccessTier(state: StorageAccountConfig, tier) = { + state with Sku = match state.Sku with - | Blobs (replication, _) -> Blobs(replication, Some tier) - | GeneralPurpose (V2 (replication, _)) -> GeneralPurpose(V2(replication, Some tier)) + | Blobs(replication, _) -> Blobs(replication, Some tier) + | GeneralPurpose(V2(replication, _)) -> GeneralPurpose(V2(replication, Some tier)) | other -> raiseFarmer $"You can only set the default access tier for Blobs or General Purpose V2 storage accounts. This account is %A{other}." - } + } /// Specify network access control lists for this storage account. [] - member _.SetNetworkAcls(state: StorageAccountConfig, networkAcls) = - { state with + member _.SetNetworkAcls(state: StorageAccountConfig, networkAcls) = { + state with NetworkAcls = Some networkAcls - } + } /// Restrict access to this storage account to a subnet on a virtual network. [] member _.RestrictToSubnet(state: StorageAccountConfig, vnet: string, subnet: string) = - let allowVnet = - { - Subnet = ResourceName subnet - VirtualNetwork = ResourceName vnet - Action = RuleAction.Allow - } + let allowVnet = { + Subnet = ResourceName subnet + VirtualNetwork = ResourceName vnet + Action = RuleAction.Allow + } match state.NetworkAcls with - | None -> - { state with + | None -> { + state with NetworkAcls = { Bypass = set [ NetworkRuleSetBypass.AzureServices ] @@ -462,28 +456,28 @@ type StorageAccountBuilder() = DefaultAction = RuleAction.Deny } |> Some - } - | Some existingAcl -> - { state with + } + | Some existingAcl -> { + state with NetworkAcls = - { existingAcl with - VirtualNetworkRules = allowVnet :: existingAcl.VirtualNetworkRules + { + existingAcl with + VirtualNetworkRules = allowVnet :: existingAcl.VirtualNetworkRules } |> Some - } + } /// Restrict access to this storage account to a IP address network prefix. [] member _.RestrictToPrefix(state: StorageAccountConfig, cidr: string) = - let allowIp = - { - Value = IpRulePrefix(IPAddressCidr.parse cidr) - Action = RuleAction.Allow - } + let allowIp = { + Value = IpRulePrefix(IPAddressCidr.parse cidr) + Action = RuleAction.Allow + } match state.NetworkAcls with - | None -> - { state with + | None -> { + state with NetworkAcls = { Bypass = set [ NetworkRuleSetBypass.AzureServices ] @@ -492,28 +486,28 @@ type StorageAccountBuilder() = DefaultAction = RuleAction.Deny } |> Some - } - | Some existingAcl -> - { state with + } + | Some existingAcl -> { + state with NetworkAcls = - { existingAcl with - IpRules = allowIp :: existingAcl.IpRules + { + existingAcl with + IpRules = allowIp :: existingAcl.IpRules } |> Some - } + } /// Restrict access to this storage account to an IP address. [] member this.RestrictToIp(state: StorageAccountConfig, ip: string) = - let allowIp = - { - Value = IpRuleAddress(System.Net.IPAddress.Parse ip) - Action = RuleAction.Allow - } + let allowIp = { + Value = IpRuleAddress(System.Net.IPAddress.Parse ip) + Action = RuleAction.Allow + } match state.NetworkAcls with - | None -> - { state with + | None -> { + state with NetworkAcls = { Bypass = set [ NetworkRuleSetBypass.AzureServices ] @@ -522,22 +516,23 @@ type StorageAccountBuilder() = DefaultAction = RuleAction.Deny } |> Some - } - | Some existingAcl -> - { state with + } + | Some existingAcl -> { + state with NetworkAcls = - { existingAcl with - IpRules = allowIp :: existingAcl.IpRules + { + existingAcl with + IpRules = allowIp :: existingAcl.IpRules } |> Some - } + } /// Restrict access to this storage account to the private endpoints and azure services. [] member _.RestrictToAzureServices(state: StorageAccountConfig, bypass: NetworkRuleSetBypass list) = match state.NetworkAcls with - | None -> - { state with + | None -> { + state with DisablePublicNetworkAccess = Some FeatureFlag.Disabled NetworkAcls = { @@ -547,56 +542,57 @@ type StorageAccountBuilder() = DefaultAction = RuleAction.Deny } |> Some - } - | Some existingAcl -> - { state with + } + | Some existingAcl -> { + state with DisablePublicNetworkAccess = Some FeatureFlag.Disabled NetworkAcls = - { existingAcl with - Bypass = Set.union (set bypass) existingAcl.Bypass + { + existingAcl with + Bypass = Set.union (set bypass) existingAcl.Bypass } |> Some - } + } /// Adds a set of CORS rules to the storage account. [] - member _.AddCorsRules(state: StorageAccountConfig, rules) = - { state with + member _.AddCorsRules(state: StorageAccountConfig, rules) = { + state with CorsRules = state.CorsRules @ rules - } + } /// Adds a set of policies to the storage account. [] - member _.AddPolicies(state: StorageAccountConfig, policies) = - { state with + member _.AddPolicies(state: StorageAccountConfig, policies) = { + state with Policies = state.Policies @ policies - } + } /// Adds a versioning enabled rule to the storage account. [] - member _.EnableVersioning(state: StorageAccountConfig, enableVersioning) = - { state with + member _.EnableVersioning(state: StorageAccountConfig, enableVersioning) = { + state with IsVersioningEnabled = state.IsVersioningEnabled @ enableVersioning - } + } /// Set minimum TLS version [] - member _.SetMinTlsVersion(state: StorageAccountConfig, minTlsVersion) = - { state with + member _.SetMinTlsVersion(state: StorageAccountConfig, minTlsVersion) = { + state with MinTlsVersion = Some minTlsVersion - } + } /// Set DNS Endpoint type [] - member _.SetDnsEndpointType(state: StorageAccountConfig) = - { state with + member _.SetDnsEndpointType(state: StorageAccountConfig) = { + state with DnsZoneType = Some "AzureDnsZone" - } + } /// Disable public network access, all access must be through a private endpoint. [] - member _.DisablePublicNetworkAccess(state: StorageAccountConfig) = - { state with + member _.DisablePublicNetworkAccess(state: StorageAccountConfig) = { + state with DisablePublicNetworkAccess = Some FeatureFlag.Enabled NetworkAcls = { @@ -606,15 +602,16 @@ type StorageAccountBuilder() = DefaultAction = RuleAction.Deny } |> Some - } + } /// Disable blob public access [] member _.DisableBlobPublicAccess(state: StorageAccountConfig, ?flag: FeatureFlag) = let flag = defaultArg flag FeatureFlag.Enabled - { state with - DisableBlobPublicAccess = Some flag + { + state with + DisableBlobPublicAccess = Some flag } /// Disable shared key access @@ -622,8 +619,9 @@ type StorageAccountBuilder() = member _.DisableSharedKeyAccess(state: StorageAccountConfig, ?flag: FeatureFlag) = let flag = defaultArg flag FeatureFlag.Enabled - { state with - DisableSharedKeyAccess = Some flag + { + state with + DisableSharedKeyAccess = Some flag } /// Default to Azure Active Directory authorization in the Azure portal @@ -631,15 +629,16 @@ type StorageAccountBuilder() = member _.DefaultToOAuthAuthentication(state: StorageAccountConfig, ?flag: FeatureFlag) = let flag = defaultArg flag FeatureFlag.Enabled - { state with - DefaultToOAuthAuthentication = Some flag + { + state with + DefaultToOAuthAuthentication = Some flag } interface ITaggable with - member _.Add state tags = - { state with + member _.Add state tags = { + state with Tags = state.Tags |> Map.merge tags - } + } /// Allow adding storage accounts directly to CDNs type EndpointBuilder with diff --git a/src/Farmer/Builders/Builders.TrafficManager.fs b/src/Farmer/Builders/Builders.TrafficManager.fs index 8aa6865c2..4b77f092c 100644 --- a/src/Farmer/Builders/Builders.TrafficManager.fs +++ b/src/Farmer/Builders/Builders.TrafficManager.fs @@ -8,28 +8,26 @@ open System open System.Net open Farmer.Arm -type EndpointConfig = - { - Name: ResourceName - Status: FeatureFlag - Target: EndpointTarget - Weight: int option - Priority: int option - Dependencies: Set - } - -type TrafficManagerConfig = - { - Name: ResourceName - DnsTtl: int - Status: FeatureFlag - RoutingMethod: RoutingMethod - MonitorConfig: MonitorConfig - EndpointConfigs: EndpointConfig list - TrafficViewEnrollmentStatus: FeatureFlag - Dependencies: Set - Tags: Map - } +type EndpointConfig = { + Name: ResourceName + Status: FeatureFlag + Target: EndpointTarget + Weight: int option + Priority: int option + Dependencies: Set +} + +type TrafficManagerConfig = { + Name: ResourceName + DnsTtl: int + Status: FeatureFlag + RoutingMethod: RoutingMethod + MonitorConfig: MonitorConfig + EndpointConfigs: EndpointConfig list + TrafficViewEnrollmentStatus: FeatureFlag + Dependencies: Set + Tags: Map +} with interface IBuilder with member this.ResourceId = profiles.resourceId this.Name @@ -63,22 +61,22 @@ type TrafficManagerConfig = Priority = e.Priority Location = match e.Target with - | External (_, l) -> Some l + | External(_, l) -> Some l | _ -> None - }: Endpoint) + } + : Endpoint) } ] type EndpointBuilder() = - member _.Yield _ = - { - Name = ResourceName.Empty - Status = Enabled - Target = EndpointTarget.Website ResourceName.Empty - Weight = None - Priority = None - Dependencies = Set.empty - } + member _.Yield _ = { + Name = ResourceName.Empty + Status = Enabled + Target = EndpointTarget.Website ResourceName.Empty + Weight = None + Priority = None + Dependencies = Set.empty + } /// Sets the name of the Endpoint [] @@ -104,51 +102,49 @@ type EndpointBuilder() = /// Sets the target of the Endpoint to a web app [] - member _.TargetWebApp(state: EndpointConfig, name) = - { state with + member _.TargetWebApp(state: EndpointConfig, name) = { + state with Target = Website name Dependencies = state.Dependencies |> Set.add (sites.resourceId (name)) - } + } - member _.TargetWebApp(state: EndpointConfig, (webApp: WebAppConfig)) = - { state with + member _.TargetWebApp(state: EndpointConfig, (webApp: WebAppConfig)) = { + state with Target = Website webApp.Name.ResourceName Dependencies = state.Dependencies |> Set.add webApp.ResourceId - } + } /// Sets the target of the Endpoint to an external domain/IP and location [] - member _.TargetExternal(state: EndpointConfig, domain, location) = - { state with + member _.TargetExternal(state: EndpointConfig, domain, location) = { + state with Target = External(domain, location) - } + } - member _.TargetExternal(state: EndpointConfig, ipAddress: IPAddress, location) = - { state with + member _.TargetExternal(state: EndpointConfig, ipAddress: IPAddress, location) = { + state with Target = External(string ipAddress, location) - } + } type TrafficManagerBuilder() = - member _.Yield _ = - { - Name = ResourceName.Empty - DnsTtl = 30 - Status = Enabled - RoutingMethod = RoutingMethod.Performance - TrafficViewEnrollmentStatus = Disabled - EndpointConfigs = [] - MonitorConfig = - { - Protocol = MonitorProtocol.Http - Port = 80 - Path = "/" - IntervalInSeconds = 30 - ToleratedNumberOfFailures = 3 - TimeoutInSeconds = 10 - } - Dependencies = Set.empty - Tags = Map.empty + member _.Yield _ = { + Name = ResourceName.Empty + DnsTtl = 30 + Status = Enabled + RoutingMethod = RoutingMethod.Performance + TrafficViewEnrollmentStatus = Disabled + EndpointConfigs = [] + MonitorConfig = { + Protocol = MonitorProtocol.Http + Port = 80 + Path = "/" + IntervalInSeconds = 30 + ToleratedNumberOfFailures = 3 + TimeoutInSeconds = 10 } + Dependencies = Set.empty + Tags = Map.empty + } member _.Run(state: TrafficManagerConfig) = state @@ -160,10 +156,10 @@ type TrafficManagerBuilder() = /// Adds Endpoints to the Traffic Manager profile [] - member _.AddEndpoints(state: TrafficManagerConfig, endpoints: EndpointConfig list) = - { state with + member _.AddEndpoints(state: TrafficManagerConfig, endpoints: EndpointConfig list) = { + state with EndpointConfigs = state.EndpointConfigs @ endpoints - } + } member this.AddEndpoints(state: TrafficManagerConfig, endpoint: EndpointConfig) = this.AddEndpoints(state, [ endpoint ]) @@ -172,10 +168,10 @@ type TrafficManagerBuilder() = [] member _.DnsTtl(state: TrafficManagerConfig, ttl: int) = { state with DnsTtl = ttl } - member this.DnsTtl(state: TrafficManagerConfig, ttl: TimeSpan) = - { state with + member this.DnsTtl(state: TrafficManagerConfig, ttl: TimeSpan) = { + state with DnsTtl = (int ttl.TotalSeconds) * 1 - } + } /// Disables the Traffic Manager profile [] @@ -187,96 +183,96 @@ type TrafficManagerBuilder() = /// Sets the routing method of the Traffic Manager profile (default Performance) [] - member _.RoutingMethod(state: TrafficManagerConfig, routingMethod) = - { state with + member _.RoutingMethod(state: TrafficManagerConfig, routingMethod) = { + state with RoutingMethod = routingMethod - } + } /// Enables the Traffic View of the Traffic Manager profile [] - member _.EnableTrafficView(state: TrafficManagerConfig) = - { state with + member _.EnableTrafficView(state: TrafficManagerConfig) = { + state with TrafficViewEnrollmentStatus = Enabled - } + } /// Disables the Traffic View of the Traffic Manager profile [] - member _.DisableTrafficView(state: TrafficManagerConfig) = - { state with + member _.DisableTrafficView(state: TrafficManagerConfig) = { + state with TrafficViewEnrollmentStatus = Disabled - } + } /// Sets the monitoring protocol of the Traffic Manager profile (default Http) [] - member _.MonitorProtocol(state: TrafficManagerConfig, protocol) = - { state with - MonitorConfig = - { state.MonitorConfig with + member _.MonitorProtocol(state: TrafficManagerConfig, protocol) = { + state with + MonitorConfig = { + state.MonitorConfig with Protocol = protocol - } - } + } + } /// Sets the monitoring port of the Traffic Manager profile (default 80) [] - member _.MonitorPort(state: TrafficManagerConfig, port) = - { state with + member _.MonitorPort(state: TrafficManagerConfig, port) = { + state with MonitorConfig = { state.MonitorConfig with Port = port } - } + } /// Sets the monitoring path of the Traffic Manager profile (default /) [] - member _.MonitorPath(state: TrafficManagerConfig, path) = - { state with + member _.MonitorPath(state: TrafficManagerConfig, path) = { + state with MonitorConfig = { state.MonitorConfig with Path = path } - } + } /// Sets the monitoring interval, in seconds, of the Traffic Manager profile (default 30) [] - member _.MonitorInterval(state: TrafficManagerConfig, interval) = - { state with - MonitorConfig = - { state.MonitorConfig with + member _.MonitorInterval(state: TrafficManagerConfig, interval) = { + state with + MonitorConfig = { + state.MonitorConfig with IntervalInSeconds = interval - } - } + } + } member this.MonitorInterval(state: TrafficManagerConfig, interval: TimeSpan) = this.MonitorInterval(state, (int interval.TotalSeconds) * 1) /// Sets the monitoring timeout, in seconds, of the Traffic Manager profile (default 10) [] - member _.MonitorTimeout(state: TrafficManagerConfig, timeout) = - { state with - MonitorConfig = - { state.MonitorConfig with + member _.MonitorTimeout(state: TrafficManagerConfig, timeout) = { + state with + MonitorConfig = { + state.MonitorConfig with TimeoutInSeconds = timeout - } - } + } + } member this.MonitorTimeout(state: TrafficManagerConfig, timeout: TimeSpan) = this.MonitorTimeout(state, (int timeout.TotalSeconds) * 1) /// Sets the monitoring tolerated number of failures, of the Traffic Manager profile (default 3) [] - member _.MonitorToleratedFailures(state: TrafficManagerConfig, failures) = - { state with - MonitorConfig = - { state.MonitorConfig with + member _.MonitorToleratedFailures(state: TrafficManagerConfig, failures) = { + state with + MonitorConfig = { + state.MonitorConfig with ToleratedNumberOfFailures = failures - } - } + } + } interface ITaggable with - member _.Add state tags = - { state with + member _.Add state tags = { + state with Tags = state.Tags |> Map.merge tags - } + } interface IDependable with - member _.Add state newDeps = - { state with + member _.Add state newDeps = { + state with Dependencies = state.Dependencies + newDeps - } + } // Expose Builders diff --git a/src/Farmer/Builders/Builders.UserAssignedIdentity.fs b/src/Farmer/Builders/Builders.UserAssignedIdentity.fs index 5d17c52b9..143fe5a85 100644 --- a/src/Farmer/Builders/Builders.UserAssignedIdentity.fs +++ b/src/Farmer/Builders/Builders.UserAssignedIdentity.fs @@ -5,23 +5,21 @@ open Farmer open Farmer.Identity open Farmer.Arm.ManagedIdentity -type UserAssignedIdentityConfig = - { - Name: ResourceName - Tags: Map - } +type UserAssignedIdentityConfig = { + Name: ResourceName + Tags: Map +} with interface IBuilder with member this.ResourceId = this.ResourceId - member this.BuildResources location = - [ - { - UserAssignedIdentity.Name = this.Name - Location = location - Tags = this.Tags - } - ] + member this.BuildResources location = [ + { + UserAssignedIdentity.Name = this.Name + Location = location + Tags = this.Tags + } + ] member this.ResourceId = userAssignedIdentities.resourceId this.Name @@ -31,21 +29,20 @@ type UserAssignedIdentityConfig = member this.PrincipalId = this.UserAssignedIdentity.PrincipalId type UserAssignedIdentityBuilder() = - member _.Yield _ = - { - Name = ResourceName.Empty - Tags = Map.empty - } + member _.Yield _ = { + Name = ResourceName.Empty + Tags = Map.empty + } /// Sets the name of the user assigned identity. [] member _.Name(state: UserAssignedIdentityConfig, name) = { state with Name = ResourceName name } /// Adds tags to the user assigned identity. interface ITaggable with - member _.Add state tags = - { state with + member _.Add state tags = { + state with Tags = state.Tags |> Map.merge tags - } + } /// Builds a user assigned identity. let userAssignedIdentity = UserAssignedIdentityBuilder() @@ -81,7 +78,7 @@ module Extensions = [] member this.SystemIdentity(state: 'TConfig) = - this.Add state (fun current -> - { current with + this.Add state (fun current -> { + current with SystemAssigned = Enabled - }) + }) diff --git a/src/Farmer/Builders/Builders.VirtualHub.fs b/src/Farmer/Builders/Builders.VirtualHub.fs index 2a8432185..a2ebbbe8c 100644 --- a/src/Farmer/Builders/Builders.VirtualHub.fs +++ b/src/Farmer/Builders/Builders.VirtualHub.fs @@ -7,59 +7,56 @@ open Farmer.Arm.VirtualWan open Farmer.Arm.VirtualHub open Farmer.VirtualHub.HubRouteTable -type VirtualHubConfig = - { - Name: ResourceName - Sku: Sku - AddressPrefix: IPAddressCidr option - Vwan: LinkedResource option - } +type VirtualHubConfig = { + Name: ResourceName + Sku: Sku + AddressPrefix: IPAddressCidr option + Vwan: LinkedResource option +} with interface IBuilder with member this.ResourceId = virtualHubs.resourceId this.Name - member this.BuildResources location = - [ - { - Name = this.Name - Location = location - Dependencies = - [ - match this.Vwan with - | Some (Managed resId) -> resId // Only generate dependency if this is managed by Farmer (same template) - | _ -> () - ] - |> Set.ofList - AddressPrefix = this.AddressPrefix - AllowBranchToBranchTraffic = None - AzureFirewall = None - ExpressRouteGateway = None - P2SVpnGateway = None - RouteTable = [] - RoutingState = None - SecurityProvider = None - SecurityPartnerProvider = None - VirtualHubRouteTableV2s = [] - VirtualHubSku = this.Sku - VirtualRouterAsn = None - VirtualRouterIps = [] - VpnGateway = None - Vwan = + member this.BuildResources location = [ + { + Name = this.Name + Location = location + Dependencies = + [ match this.Vwan with - | Some (Managed resId) -> Some resId - | Some (Unmanaged resId) -> Some resId - | _ -> None - } - ] + | Some(Managed resId) -> resId // Only generate dependency if this is managed by Farmer (same template) + | _ -> () + ] + |> Set.ofList + AddressPrefix = this.AddressPrefix + AllowBranchToBranchTraffic = None + AzureFirewall = None + ExpressRouteGateway = None + P2SVpnGateway = None + RouteTable = [] + RoutingState = None + SecurityProvider = None + SecurityPartnerProvider = None + VirtualHubRouteTableV2s = [] + VirtualHubSku = this.Sku + VirtualRouterAsn = None + VirtualRouterIps = [] + VpnGateway = None + Vwan = + match this.Vwan with + | Some(Managed resId) -> Some resId + | Some(Unmanaged resId) -> Some resId + | _ -> None + } + ] type VirtualHubBuilder() = - member _.Yield _ = - { - Name = ResourceName.Empty - AddressPrefix = None - Vwan = None - Sku = Standard - } + member _.Yield _ = { + Name = ResourceName.Empty + AddressPrefix = None + Vwan = None + Sku = Standard + } [] /// Sets the name of the virtual hub. @@ -69,41 +66,40 @@ type VirtualHubBuilder() = [] /// Sets the address prefix of the virtual hub. - member _.AddressPrefix(state: VirtualHubConfig, addressPrefix) = - { state with + member _.AddressPrefix(state: VirtualHubConfig, addressPrefix) = { + state with AddressPrefix = Some addressPrefix - } + } [] /// Links the VirtualHub to a Farmer-managed VirtualWAN instance - member _.LinkToVwan(state: VirtualHubConfig, vwan: VirtualWan) = - { state with + member _.LinkToVwan(state: VirtualHubConfig, vwan: VirtualWan) = { + state with Vwan = Some(LinkedResource.Managed (vwan :> IArmResource).ResourceId) - } + } - member _.LinkToVwan(state: VirtualHubConfig, vwan: VirtualWanConfig) = - { state with + member _.LinkToVwan(state: VirtualHubConfig, vwan: VirtualWanConfig) = { + state with Vwan = Some(LinkedResource.Managed (vwan :> IBuilder).ResourceId) - } + } [] /// Links the VirtualHub to an existing VirtualWAN instance - member _.LinkToExternalVwan(state: VirtualHubConfig, vwanResourceId) = - { state with + member _.LinkToExternalVwan(state: VirtualHubConfig, vwanResourceId) = { + state with Vwan = Some(LinkedResource.Unmanaged vwanResourceId) - } + } /// The SKU of the virtual hub. [] member _.Sku(state: VirtualHubConfig, sku) = { state with Sku = sku } -type HubRouteTableConfig = - { - Name: ResourceName - Vhub: LinkedResource - Routes: HubRoute list - Labels: string list - } +type HubRouteTableConfig = { + Name: ResourceName + Vhub: LinkedResource + Routes: HubRoute list + Labels: string list +} with interface IBuilder with member this.ResourceId = @@ -114,90 +110,87 @@ type HubRouteTableConfig = hubRouteTables.resourceId (vhubResourceId.Name / this.Name) - member this.BuildResources location = - [ - { - Name = this.Name - VirtualHub = + member this.BuildResources location = [ + { + Name = this.Name + VirtualHub = + match this.Vhub with + | Unmanaged resId + | Managed resId -> resId + Dependencies = + [ match this.Vhub with - | Unmanaged resId - | Managed resId -> resId - Dependencies = - [ - match this.Vhub with - | Managed resId -> resId // Only generate dependency if this is managed by Farmer (same template) - | _ -> () - - let routeDependencies = - this.Routes - |> List.map (fun r -> - [ - match r.NextHop with - | NextHop.ResourceId (Managed resId) -> resId - | _ -> () - match r.Destination with - | _ -> () - ]) - |> List.concat - - for routeDep in routeDependencies do - routeDep - ] - |> Set.ofList - Routes = this.Routes - Labels = this.Labels - } - ] + | Managed resId -> resId // Only generate dependency if this is managed by Farmer (same template) + | _ -> () + + let routeDependencies = + this.Routes + |> List.map (fun r -> [ + match r.NextHop with + | NextHop.ResourceId(Managed resId) -> resId + | _ -> () + match r.Destination with + | _ -> () + ]) + |> List.concat + + for routeDep in routeDependencies do + routeDep + ] + |> Set.ofList + Routes = this.Routes + Labels = this.Labels + } + ] type HubRouteTableBuilder() = - member _.Yield _ = - { - Name = ResourceName.Empty - Vhub = LinkedResource.Unmanaged(virtualHubs.resourceId ResourceName.Empty) - Routes = List.Empty - Labels = List.Empty - } + member _.Yield _ = { + Name = ResourceName.Empty + Vhub = LinkedResource.Unmanaged(virtualHubs.resourceId ResourceName.Empty) + Routes = List.Empty + Labels = List.Empty + } [] /// Sets the name of the virtual hub. member _.Name(state: HubRouteTableConfig, name) = { state with Name = name } - member _.Name(state: HubRouteTableConfig, name) = - { state with + member _.Name(state: HubRouteTableConfig, name) = { + state with Name = (ResourceName name) - } + } [] /// Links the HubRouteTable to a Farmer-managed VirtualHub instance - member _.LinkToVhub(state: HubRouteTableConfig, vhub: VirtualHub) = - { state with + member _.LinkToVhub(state: HubRouteTableConfig, vhub: VirtualHub) = { + state with Vhub = LinkedResource.Managed (vhub :> IArmResource).ResourceId - } + } - member _.LinkToVhub(state: HubRouteTableConfig, vhub: VirtualHubConfig) = - { state with + member _.LinkToVhub(state: HubRouteTableConfig, vhub: VirtualHubConfig) = { + state with Vhub = LinkedResource.Managed (vhub :> IBuilder).ResourceId - } + } [] /// Links the HubRouteTable to an existing VirtualHub instance - member _.LinkToExternalVhub(state: HubRouteTableConfig, resourceId) = - { state with + member _.LinkToExternalVhub(state: HubRouteTableConfig, resourceId) = { + state with Vhub = LinkedResource.Unmanaged resourceId - } + } [] /// Adds the routes to the HubRouteTable - member _.AddRoutes(state: HubRouteTableConfig, routes) = - { state with + member _.AddRoutes(state: HubRouteTableConfig, routes) = { + state with Routes = state.Routes @ routes - } + } [] - member _.AddLabels(state: HubRouteTableConfig, labels) = - { state with + member _.AddLabels(state: HubRouteTableConfig, labels) = { + state with Labels = state.Labels @ labels - } + } member _.Run(state: HubRouteTableConfig) = match state.Vhub with diff --git a/src/Farmer/Builders/Builders.VirtualNetwork.fs b/src/Farmer/Builders/Builders.VirtualNetwork.fs index 8c3c4b8e4..f64b4281c 100644 --- a/src/Farmer/Builders/Builders.VirtualNetwork.fs +++ b/src/Farmer/Builders/Builders.VirtualNetwork.fs @@ -11,41 +11,38 @@ type PeeringMode = | OneWayToRemote | OneWayFromRemote -type SubnetConfig = - { - Name: ResourceName - Prefix: IPAddressCidr - VirtualNetwork: LinkedResource option - NetworkSecurityGroup: LinkedResource option - Delegations: SubnetDelegationService list - NatGateway: LinkedResource option - ServiceEndpoints: (EndpointServiceType * Location list) list - AssociatedServiceEndpointPolicies: ResourceId list - AllowPrivateEndpoints: FeatureFlag option - PrivateLinkServiceNetworkPolicies: FeatureFlag option - } - - member internal this.AsSubnetResource = - { - Name = this.Name - Prefix = IPAddressCidr.format this.Prefix - VirtualNetwork = this.VirtualNetwork - NetworkSecurityGroup = this.NetworkSecurityGroup - Delegations = - this.Delegations - |> List.map (fun (SubnetDelegationService (delegation)) -> - { - Name = ResourceName delegation - ServiceName = delegation - }) - NatGateway = this.NatGateway - ServiceEndpoints = this.ServiceEndpoints - AssociatedServiceEndpointPolicies = this.AssociatedServiceEndpointPolicies - // PrivateEndpointNetworkPolicies prevents the use of private endpoints so - // to ENable private endpoints we have to DISable PrivateEndpointNetworkPolicies - PrivateEndpointNetworkPolicies = this.AllowPrivateEndpoints |> Option.map FeatureFlag.invert - PrivateLinkServiceNetworkPolicies = this.PrivateLinkServiceNetworkPolicies - } +type SubnetConfig = { + Name: ResourceName + Prefix: IPAddressCidr + VirtualNetwork: LinkedResource option + NetworkSecurityGroup: LinkedResource option + Delegations: SubnetDelegationService list + NatGateway: LinkedResource option + ServiceEndpoints: (EndpointServiceType * Location list) list + AssociatedServiceEndpointPolicies: ResourceId list + AllowPrivateEndpoints: FeatureFlag option + PrivateLinkServiceNetworkPolicies: FeatureFlag option +} with + + member internal this.AsSubnetResource = { + Name = this.Name + Prefix = IPAddressCidr.format this.Prefix + VirtualNetwork = this.VirtualNetwork + NetworkSecurityGroup = this.NetworkSecurityGroup + Delegations = + this.Delegations + |> List.map (fun (SubnetDelegationService(delegation)) -> { + Name = ResourceName delegation + ServiceName = delegation + }) + NatGateway = this.NatGateway + ServiceEndpoints = this.ServiceEndpoints + AssociatedServiceEndpointPolicies = this.AssociatedServiceEndpointPolicies + // PrivateEndpointNetworkPolicies prevents the use of private endpoints so + // to ENable private endpoints we have to DISable PrivateEndpointNetworkPolicies + PrivateEndpointNetworkPolicies = this.AllowPrivateEndpoints |> Option.map FeatureFlag.invert + PrivateLinkServiceNetworkPolicies = this.PrivateLinkServiceNetworkPolicies + } interface IBuilder with member this.ResourceId = @@ -56,23 +53,21 @@ type SubnetConfig = member this.BuildResources _ = [ this.AsSubnetResource ] type SubnetBuilder() = - member _.Yield _ = - { - Name = ResourceName.Empty - Prefix = - { - Address = System.Net.IPAddress.Parse("10.100.0.0") - Prefix = 16 - } - VirtualNetwork = None - NetworkSecurityGroup = None - Delegations = [] - NatGateway = None - ServiceEndpoints = [] - AssociatedServiceEndpointPolicies = [] - AllowPrivateEndpoints = None - PrivateLinkServiceNetworkPolicies = None + member _.Yield _ = { + Name = ResourceName.Empty + Prefix = { + Address = System.Net.IPAddress.Parse("10.100.0.0") + Prefix = 16 } + VirtualNetwork = None + NetworkSecurityGroup = None + Delegations = [] + NatGateway = None + ServiceEndpoints = [] + AssociatedServiceEndpointPolicies = [] + AllowPrivateEndpoints = None + PrivateLinkServiceNetworkPolicies = None + } /// Sets the name of the subnet [] @@ -80,194 +75,203 @@ type SubnetBuilder() = /// Sets the network prefix in CIDR notation [] - member _.Prefix(state: SubnetConfig, prefix) = - { state with + member _.Prefix(state: SubnetConfig, prefix) = { + state with Prefix = IPAddressCidr.parse prefix - } + } [] - member _.NatGateway(state: SubnetConfig, gw: IArmResource) = - { state with + member _.NatGateway(state: SubnetConfig, gw: IArmResource) = { + state with NatGateway = Some(Managed gw.ResourceId) - } + } - member _.NatGateway(state: SubnetConfig, resId: ResourceId) = - { state with + member _.NatGateway(state: SubnetConfig, resId: ResourceId) = { + state with NatGateway = Some(Managed resId) - } + } - member _.NatGateway(state: SubnetConfig, gw: NatGatewayConfig) = - { state with + member _.NatGateway(state: SubnetConfig, gw: NatGatewayConfig) = { + state with NatGateway = Some(Managed (gw :> IBuilder).ResourceId) - } + } [] - member _.LinkToNatGateway(state: SubnetConfig, gw: IArmResource) = - { state with + member _.LinkToNatGateway(state: SubnetConfig, gw: IArmResource) = { + state with NatGateway = Some(Unmanaged gw.ResourceId) - } + } - member _.LinkToNatGateway(state: SubnetConfig, resId: ResourceId) = - { state with + member _.LinkToNatGateway(state: SubnetConfig, resId: ResourceId) = { + state with NatGateway = Some(Unmanaged resId) - } + } - member _.LinkToNatGateway(state: SubnetConfig, gw: NatGatewayConfig) = - { state with + member _.LinkToNatGateway(state: SubnetConfig, gw: NatGatewayConfig) = { + state with NatGateway = Some(Unmanaged (gw :> IBuilder).ResourceId) - } + } /// Sets the network security group for subnet [] - member _.NetworkSecurityGroup(state: SubnetConfig, nsg: IArmResource) = - { state with + member _.NetworkSecurityGroup(state: SubnetConfig, nsg: IArmResource) = { + state with NetworkSecurityGroup = Some(Managed nsg.ResourceId) - } + } - member _.NetworkSecurityGroup(state: SubnetConfig, nsg: ResourceId) = - { state with + member _.NetworkSecurityGroup(state: SubnetConfig, nsg: ResourceId) = { + state with NetworkSecurityGroup = Some(Managed nsg) - } + } - member _.NetworkSecurityGroup(state: SubnetConfig, nsg: NsgConfig) = - { state with + member _.NetworkSecurityGroup(state: SubnetConfig, nsg: NsgConfig) = { + state with NetworkSecurityGroup = Some(Managed (nsg :> IBuilder).ResourceId) - } + } /// Links the subnet to an existing network security group. [] - member _.LinkToNetworkSecurityGroup(state: SubnetConfig, nsg: IArmResource) = - { state with + member _.LinkToNetworkSecurityGroup(state: SubnetConfig, nsg: IArmResource) = { + state with NetworkSecurityGroup = Some(Unmanaged(nsg.ResourceId)) - } + } - member _.LinkToNetworkSecurityGroup(state: SubnetConfig, nsg: ResourceId) = - { state with + member _.LinkToNetworkSecurityGroup(state: SubnetConfig, nsg: ResourceId) = { + state with NetworkSecurityGroup = Some(Unmanaged nsg) - } + } - member _.LinkToNetworkSecurityGroup(state: SubnetConfig, nsg: NsgConfig) = - { state with + member _.LinkToNetworkSecurityGroup(state: SubnetConfig, nsg: NsgConfig) = { + state with NetworkSecurityGroup = Some(Unmanaged (nsg :> IBuilder).ResourceId) - } + } /// Links the subnet to an managed virtual network. [] - member _.LinkToVirtualNetwork(state: SubnetConfig, vnet: IArmResource) = - { state with + member _.LinkToVirtualNetwork(state: SubnetConfig, vnet: IArmResource) = { + state with VirtualNetwork = Some(Managed(vnet.ResourceId)) - } + } - member _.LinkToVirtualNetwork(state: SubnetConfig, vnet: ResourceId) = - { state with + member _.LinkToVirtualNetwork(state: SubnetConfig, vnet: ResourceId) = { + state with VirtualNetwork = Some(Managed vnet) - } + } - member _.LinkToVirtualNetwork(state: SubnetConfig, vnet: NsgConfig) = - { state with + member _.LinkToVirtualNetwork(state: SubnetConfig, vnet: NsgConfig) = { + state with VirtualNetwork = Some(Managed (vnet :> IBuilder).ResourceId) - } + } /// Links the subnet to an existing, externally defined virtual network. [] - member _.LinkToUnmanagedVirtualNetwork(state: SubnetConfig, vnet: IArmResource) = - { state with + member _.LinkToUnmanagedVirtualNetwork(state: SubnetConfig, vnet: IArmResource) = { + state with VirtualNetwork = Some(Unmanaged(vnet.ResourceId)) - } + } - member _.LinkToUnmanagedVirtualNetwork(state: SubnetConfig, vnet: ResourceId) = - { state with + member _.LinkToUnmanagedVirtualNetwork(state: SubnetConfig, vnet: ResourceId) = { + state with VirtualNetwork = Some(Unmanaged vnet) - } + } - member _.LinkToUnmanagedVirtualNetwork(state: SubnetConfig, vnet: NsgConfig) = - { state with + member _.LinkToUnmanagedVirtualNetwork(state: SubnetConfig, vnet: NsgConfig) = { + state with VirtualNetwork = Some(Unmanaged (vnet :> IBuilder).ResourceId) - } + } /// Sets the network prefix in CIDR notation [] - member _.AddDelegations(state: SubnetConfig, delegations) = - { state with + member _.AddDelegations(state: SubnetConfig, delegations) = { + state with Delegations = state.Delegations @ delegations - } + } /// Add service endpoint types to this subnet [] - member _.AddServiceEndpoints(state: SubnetConfig, serviceEndpoints) = - { state with + member _.AddServiceEndpoints(state: SubnetConfig, serviceEndpoints) = { + state with ServiceEndpoints = state.ServiceEndpoints @ serviceEndpoints - } + } /// Associates service endpoint policies with this subnet [] - member _.AssociateServiceEndpointPolicies(state: SubnetConfig, servicePolicyIds) = - { state with + member _.AssociateServiceEndpointPolicies(state: SubnetConfig, servicePolicyIds) = { + state with AssociatedServiceEndpointPolicies = state.AssociatedServiceEndpointPolicies @ servicePolicyIds - } + } /// Disable private endpoint network policies [] - member _.PrivateEndpoints(state: SubnetConfig, value: FeatureFlag) = - { state with + member _.PrivateEndpoints(state: SubnetConfig, value: FeatureFlag) = { + state with AllowPrivateEndpoints = Some value - } + } /// Enable or disable private link service network policies on this subnet to allow specifying the private link IP. [] - member _.PrivateLinkServiceNetworkPolicies(state: SubnetConfig, flag: FeatureFlag) = - { state with + member _.PrivateLinkServiceNetworkPolicies(state: SubnetConfig, flag: FeatureFlag) = { + state with PrivateLinkServiceNetworkPolicies = Some flag - } + } let subnet = SubnetBuilder() /// Specification for a subnet to build from an address space. -type SubnetBuildSpec = - { - Name: string - Size: int - NetworkSecurityGroup: LinkedResource option - Delegations: SubnetDelegationService list - NatGateway: LinkedResource option - ServiceEndpoints: (EndpointServiceType * Location list) list - AssociatedServiceEndpointPolicies: ResourceId list - AllowPrivateEndpoints: FeatureFlag option - PrivateLinkServiceNetworkPolicies: FeatureFlag option - } +type SubnetBuildSpec = { + Name: string + Size: int + NetworkSecurityGroup: LinkedResource option + Delegations: SubnetDelegationService list + NatGateway: LinkedResource option + ServiceEndpoints: (EndpointServiceType * Location list) list + AssociatedServiceEndpointPolicies: ResourceId list + AllowPrivateEndpoints: FeatureFlag option + PrivateLinkServiceNetworkPolicies: FeatureFlag option +} /// Builds a subnet of a certain CIDR block size. -let buildSubnet name size = - { - Name = name - Size = size - NetworkSecurityGroup = None - Delegations = [] - NatGateway = None - ServiceEndpoints = [] - AssociatedServiceEndpointPolicies = [] - AllowPrivateEndpoints = None - PrivateLinkServiceNetworkPolicies = None - } +let buildSubnet name size = { + Name = name + Size = size + NetworkSecurityGroup = None + Delegations = [] + NatGateway = None + ServiceEndpoints = [] + AssociatedServiceEndpointPolicies = [] + AllowPrivateEndpoints = None + PrivateLinkServiceNetworkPolicies = None +} /// Builds a subnet of a certain CIDR block size with service delegations. -let buildSubnetDelegations name size delegations = - { - Name = name - Size = size - NetworkSecurityGroup = None - Delegations = delegations - NatGateway = None - ServiceEndpoints = [] - AssociatedServiceEndpointPolicies = [] - AllowPrivateEndpoints = None - PrivateLinkServiceNetworkPolicies = None - } +let buildSubnetDelegations name size delegations = { + Name = name + Size = size + NetworkSecurityGroup = None + Delegations = delegations + NatGateway = None + ServiceEndpoints = [] + AssociatedServiceEndpointPolicies = [] + AllowPrivateEndpoints = None + PrivateLinkServiceNetworkPolicies = None +} + +let buildSubnetAllowPrivateEndpoints name size = { + Name = name + Size = size + NetworkSecurityGroup = None + Delegations = [] + NatGateway = None + ServiceEndpoints = [] + AssociatedServiceEndpointPolicies = [] + AllowPrivateEndpoints = None + PrivateLinkServiceNetworkPolicies = None +} -let buildSubnetAllowPrivateEndpoints name size = - { - Name = name - Size = size +type SubnetSpecBuilder() = + member _.Yield _ = { + Name = "" + Size = 24 NetworkSecurityGroup = None Delegations = [] NatGateway = None @@ -277,20 +281,6 @@ let buildSubnetAllowPrivateEndpoints name size = PrivateLinkServiceNetworkPolicies = None } -type SubnetSpecBuilder() = - member _.Yield _ = - { - Name = "" - Size = 24 - NetworkSecurityGroup = None - Delegations = [] - NatGateway = None - ServiceEndpoints = [] - AssociatedServiceEndpointPolicies = [] - AllowPrivateEndpoints = None - PrivateLinkServiceNetworkPolicies = None - } - /// Sets the name of the subnet to build [] member _.Name(state: SubnetBuildSpec, name) = { state with Name = name } @@ -300,114 +290,113 @@ type SubnetSpecBuilder() = member _.Size(state: SubnetBuildSpec, size) = { state with Size = size } [] - member _.NatGateway(state: SubnetBuildSpec, gw: IArmResource) = - { state with + member _.NatGateway(state: SubnetBuildSpec, gw: IArmResource) = { + state with NatGateway = Some(Managed gw.ResourceId) - } + } - member _.NatGateway(state: SubnetBuildSpec, resId: ResourceId) = - { state with + member _.NatGateway(state: SubnetBuildSpec, resId: ResourceId) = { + state with NatGateway = Some(Managed resId) - } + } - member _.NatGateway(state: SubnetBuildSpec, gw: NatGatewayConfig) = - { state with + member _.NatGateway(state: SubnetBuildSpec, gw: NatGatewayConfig) = { + state with NatGateway = Some(Managed (gw :> IBuilder).ResourceId) - } + } [] - member _.LinkToNatGateway(state: SubnetBuildSpec, gw: IArmResource) = - { state with + member _.LinkToNatGateway(state: SubnetBuildSpec, gw: IArmResource) = { + state with NatGateway = Some(Unmanaged gw.ResourceId) - } + } - member _.LinkToNatGateway(state: SubnetBuildSpec, resId: ResourceId) = - { state with + member _.LinkToNatGateway(state: SubnetBuildSpec, resId: ResourceId) = { + state with NatGateway = Some(Unmanaged resId) - } + } - member _.LinkToNatGateway(state: SubnetBuildSpec, gw: NatGatewayConfig) = - { state with + member _.LinkToNatGateway(state: SubnetBuildSpec, gw: NatGatewayConfig) = { + state with NatGateway = Some(Unmanaged (gw :> IBuilder).ResourceId) - } + } /// Sets the network security group for subnet [] - member _.NetworkSecurityGroup(state: SubnetBuildSpec, nsg: IArmResource) = - { state with + member _.NetworkSecurityGroup(state: SubnetBuildSpec, nsg: IArmResource) = { + state with NetworkSecurityGroup = Some(Managed(nsg.ResourceId)) - } + } - member _.NetworkSecurityGroup(state: SubnetBuildSpec, nsg: ResourceId) = - { state with + member _.NetworkSecurityGroup(state: SubnetBuildSpec, nsg: ResourceId) = { + state with NetworkSecurityGroup = Some(Managed nsg) - } + } - member _.NetworkSecurityGroup(state: SubnetBuildSpec, nsg: NsgConfig) = - { state with + member _.NetworkSecurityGroup(state: SubnetBuildSpec, nsg: NsgConfig) = { + state with NetworkSecurityGroup = Some(Managed (nsg :> IBuilder).ResourceId) - } + } /// Links the subnet to an existing network security group. [] - member _.LinkToNetworkSecurityGroup(state: SubnetBuildSpec, nsg: IArmResource) = - { state with + member _.LinkToNetworkSecurityGroup(state: SubnetBuildSpec, nsg: IArmResource) = { + state with NetworkSecurityGroup = Some(Unmanaged(nsg.ResourceId)) - } + } - member _.LinkToNetworkSecurityGroup(state: SubnetBuildSpec, nsg: ResourceId) = - { state with + member _.LinkToNetworkSecurityGroup(state: SubnetBuildSpec, nsg: ResourceId) = { + state with NetworkSecurityGroup = Some(Unmanaged nsg) - } + } - member _.LinkToNetworkSecurityGroup(state: SubnetBuildSpec, nsg: NsgConfig) = - { state with + member _.LinkToNetworkSecurityGroup(state: SubnetBuildSpec, nsg: NsgConfig) = { + state with NetworkSecurityGroup = Some(Unmanaged (nsg :> IBuilder).ResourceId) - } + } /// Adds any services to delegate this subnet [] - member _.AddDelegations(state: SubnetBuildSpec, delegations) = - { state with + member _.AddDelegations(state: SubnetBuildSpec, delegations) = { + state with Delegations = state.Delegations @ delegations - } + } /// Adds service endpoints to build for this subnet [] - member _.AddServiceEndpoints(state: SubnetBuildSpec, serviceEndpoints) = - { state with + member _.AddServiceEndpoints(state: SubnetBuildSpec, serviceEndpoints) = { + state with ServiceEndpoints = state.ServiceEndpoints @ serviceEndpoints - } + } /// Associates the built subnet with service endpoint policies [] - member _.AddAssociatedServiceEndpointPolicies(state: SubnetBuildSpec, policies) = - { state with + member _.AddAssociatedServiceEndpointPolicies(state: SubnetBuildSpec, policies) = { + state with AssociatedServiceEndpointPolicies = state.AssociatedServiceEndpointPolicies @ policies - } + } /// Disable private endpoint netwokj security policies to enable use of private endpoints [] - member _.PrivateEndpoints(state: SubnetBuildSpec, flag: FeatureFlag) = - { state with + member _.PrivateEndpoints(state: SubnetBuildSpec, flag: FeatureFlag) = { + state with AllowPrivateEndpoints = Some flag - } + } /// Enable or disable private link service network policies on this subnet to allow specifying the private link IP. [] - member _.PrivateLinkServiceNetworkPolicies(state: SubnetBuildSpec, flag: FeatureFlag) = - { state with + member _.PrivateLinkServiceNetworkPolicies(state: SubnetBuildSpec, flag: FeatureFlag) = { + state with PrivateLinkServiceNetworkPolicies = Some flag - } + } let subnetSpec = SubnetSpecBuilder() /// A specification building an address space and subnets. -type AddressSpaceSpec = - { - Space: string - Subnets: SubnetBuildSpec list - } +type AddressSpaceSpec = { + Space: string + Subnets: SubnetBuildSpec list +} open System.Runtime.InteropServices @@ -419,10 +408,10 @@ type AddressSpaceBuilder() = member _.Space(state: AddressSpaceSpec, space) = { state with Space = space } [] - member _.Subnets(state: AddressSpaceSpec, subnets) = - { state with + member _.Subnets(state: AddressSpaceSpec, subnets) = { + state with Subnets = state.Subnets @ subnets - } + } member private _.buildSubnet ( @@ -436,21 +425,21 @@ type AddressSpaceBuilder() = ?privateLinkServiceNetworkPolicies: FeatureFlag, ?nsg: LinkedResource ) = - let subnetBuildSpec = - { - Name = name - Size = size - NetworkSecurityGroup = nsg - Delegations = delegations |> Option.defaultValue [] - NatGateway = None - ServiceEndpoints = serviceEndpoints |> Option.defaultValue [] - AssociatedServiceEndpointPolicies = associatedServiceEndpointPolicies |> Option.defaultValue [] - AllowPrivateEndpoints = allowPrivateEndpoints - PrivateLinkServiceNetworkPolicies = privateLinkServiceNetworkPolicies - } + let subnetBuildSpec = { + Name = name + Size = size + NetworkSecurityGroup = nsg + Delegations = delegations |> Option.defaultValue [] + NatGateway = None + ServiceEndpoints = serviceEndpoints |> Option.defaultValue [] + AssociatedServiceEndpointPolicies = associatedServiceEndpointPolicies |> Option.defaultValue [] + AllowPrivateEndpoints = allowPrivateEndpoints + PrivateLinkServiceNetworkPolicies = privateLinkServiceNetworkPolicies + } - { state with - Subnets = state.Subnets @ [ subnetBuildSpec ] + { + state with + Subnets = state.Subnets @ [ subnetBuildSpec ] } [] @@ -468,23 +457,21 @@ type AddressSpaceBuilder() = let addressSpace = AddressSpaceBuilder() -type VNetPeeringSpec = - { - RemoteVNet: LinkedResource - Direction: PeeringMode - Access: PeerAccess - Transit: GatewayTransit - DependsOn: ResourceId Set - } - -type VirtualNetworkConfig = - { - Name: ResourceName - AddressSpacePrefixes: string list - Subnets: SubnetConfig list - Peers: VNetPeeringSpec list - Tags: Map - } +type VNetPeeringSpec = { + RemoteVNet: LinkedResource + Direction: PeeringMode + Access: PeerAccess + Transit: GatewayTransit + DependsOn: ResourceId Set +} + +type VirtualNetworkConfig = { + Name: ResourceName + AddressSpacePrefixes: string list + Subnets: SubnetConfig list + Peers: VNetPeeringSpec list + Tags: Map +} with member this.SubnetIds = this.Subnets @@ -496,62 +483,58 @@ type VirtualNetworkConfig = interface IBuilder with member this.ResourceId = this.ResourceId - member this.BuildResources location = - [ - { - Name = this.Name + member this.BuildResources location = [ + { + Name = this.Name + Location = location + AddressSpacePrefixes = this.AddressSpacePrefixes + Subnets = this.Subnets |> List.map (fun subnetConfig -> subnetConfig.AsSubnetResource) + Tags = this.Tags + } + for { + RemoteVNet = remote + Direction = direction + Access = access + Transit = transit + DependsOn = deps + } in this.Peers do + match direction with + | OneWayToRemote + | TwoWay -> { Location = location - AddressSpacePrefixes = this.AddressSpacePrefixes - Subnets = this.Subnets |> List.map (fun subnetConfig -> subnetConfig.AsSubnetResource) - Tags = this.Tags - } - for { - RemoteVNet = remote - Direction = direction - Access = access - Transit = transit - DependsOn = deps - } in this.Peers do - match direction with - | OneWayToRemote - | TwoWay -> - { - Location = location - OwningVNet = Managed this.ResourceId - RemoteVNet = remote - RemoteAccess = access - GatewayTransit = transit - DependsOn = deps - } - | _ -> () - - match direction with - | OneWayFromRemote - | TwoWay -> - { - Location = location - OwningVNet = remote - RemoteVNet = Managed this.ResourceId - RemoteAccess = access - GatewayTransit = - match transit with - | UseRemoteGateway -> UseLocalGateway - | UseLocalGateway -> UseRemoteGateway - | GatewayTransitDisabled -> GatewayTransitDisabled - DependsOn = deps - } - | _ -> () - ] + OwningVNet = Managed this.ResourceId + RemoteVNet = remote + RemoteAccess = access + GatewayTransit = transit + DependsOn = deps + } + | _ -> () + + match direction with + | OneWayFromRemote + | TwoWay -> { + Location = location + OwningVNet = remote + RemoteVNet = Managed this.ResourceId + RemoteAccess = access + GatewayTransit = + match transit with + | UseRemoteGateway -> UseLocalGateway + | UseLocalGateway -> UseRemoteGateway + | GatewayTransitDisabled -> GatewayTransitDisabled + DependsOn = deps + } + | _ -> () + ] type VirtualNetworkBuilder() = - member _.Yield _ = - { - Name = ResourceName.Empty - AddressSpacePrefixes = [] - Subnets = List.empty - Peers = List.empty - Tags = Map.empty - } + member _.Yield _ = { + Name = ResourceName.Empty + AddressSpacePrefixes = [] + Subnets = List.empty + Peers = List.empty + Tags = Map.empty + } /// Sets the name of the virtual network [] @@ -559,34 +542,33 @@ type VirtualNetworkBuilder() = /// Adds address spaces prefixes [] - member _.AddAddressSpaces(state: VirtualNetworkConfig, prefixes) = - { state with + member _.AddAddressSpaces(state: VirtualNetworkConfig, prefixes) = { + state with AddressSpacePrefixes = state.AddressSpacePrefixes @ prefixes - } + } /// Adds subnets [] - member _.AddSubnets(state: VirtualNetworkConfig, subnets) = - { state with + member _.AddSubnets(state: VirtualNetworkConfig, subnets) = { + state with Subnets = state.Subnets @ subnets - } + } /// Peers this VNet with other VNets to allow communication between the VNets as if they were one [] - member _.AddPeers(state: VirtualNetworkConfig, peers) = - { state with + member _.AddPeers(state: VirtualNetworkConfig, peers) = { + state with Peers = state.Peers @ peers - } + } member this.AddPeers(state: VirtualNetworkConfig, peers: (LinkedResource * PeeringMode) list) = - let makeSpec (peer: LinkedResource, direction) = - { - RemoteVNet = Managed peer.ResourceId - Direction = direction - Access = AccessAndForward - Transit = GatewayTransitDisabled - DependsOn = Set.empty - } + let makeSpec (peer: LinkedResource, direction) = { + RemoteVNet = Managed peer.ResourceId + Direction = direction + Access = AccessAndForward + Transit = GatewayTransitDisabled + DependsOn = Set.empty + } this.AddPeers(state, peers |> List.map makeSpec) @@ -620,16 +602,15 @@ type VirtualNetworkBuilder() = |> List.collect (fun addressSpaceConfig -> let addressSpace = IPAddressCidr.parse addressSpaceConfig.Space - let sizes = - [ - for subnet in addressSpaceConfig.Subnets do - if subnet.Size > 29 then - invalidArg - "size" - $"Subnet must be of /29 or larger, cannot carve subnet {subnet.Name} of /{subnet.Size}" + let sizes = [ + for subnet in addressSpaceConfig.Subnets do + if subnet.Size > 29 then + invalidArg + "size" + $"Subnet must be of /29 or larger, cannot carve subnet {subnet.Name} of /{subnet.Size}" - subnet.Size - ] + subnet.Size + ] IPAddressCidr.carveAddressSpace addressSpace sizes |> List.zip ( @@ -645,61 +626,61 @@ type VirtualNetworkBuilder() = s.NetworkSecurityGroup) ) |> List.map - (fun ((name, - delegations, - serviceEndpoints, - serviceEndpointPolicies, - allowPrivateEndpoints, - privateLinkServiceNetworkPolicies, - natGateway, - nsg), - cidr) -> - { - Name = ResourceName name - Prefix = cidr - VirtualNetwork = Some(Managed(virtualNetworks.resourceId state.Name)) - NetworkSecurityGroup = nsg - Delegations = delegations - NatGateway = natGateway - ServiceEndpoints = serviceEndpoints - AssociatedServiceEndpointPolicies = serviceEndpointPolicies - AllowPrivateEndpoints = allowPrivateEndpoints - PrivateLinkServiceNetworkPolicies = privateLinkServiceNetworkPolicies - })) + (fun + ((name, + delegations, + serviceEndpoints, + serviceEndpointPolicies, + allowPrivateEndpoints, + privateLinkServiceNetworkPolicies, + natGateway, + nsg), + cidr) -> { + Name = ResourceName name + Prefix = cidr + VirtualNetwork = Some(Managed(virtualNetworks.resourceId state.Name)) + NetworkSecurityGroup = nsg + Delegations = delegations + NatGateway = natGateway + ServiceEndpoints = serviceEndpoints + AssociatedServiceEndpointPolicies = serviceEndpointPolicies + AllowPrivateEndpoints = allowPrivateEndpoints + PrivateLinkServiceNetworkPolicies = privateLinkServiceNetworkPolicies + })) let newAddressSpaces = addressSpaces |> List.map (fun addressSpace -> addressSpace.Space) - { state with - Subnets = state.Subnets @ newSubnets - AddressSpacePrefixes = state.AddressSpacePrefixes @ newAddressSpaces + { + state with + Subnets = state.Subnets @ newSubnets + AddressSpacePrefixes = state.AddressSpacePrefixes @ newAddressSpaces } interface ITaggable with - member _.Add state tags = - { state with + member _.Add state tags = { + state with Tags = state.Tags |> Map.merge tags - } + } let vnet = VirtualNetworkBuilder() type VNetPeeringSpecBuilder() = - member _.Yield _ = - { - RemoteVNet = Unmanaged(virtualNetworks.resourceId "") - Direction = TwoWay - Access = AccessAndForward - Transit = GatewayTransitDisabled - DependsOn = Set.empty - } + member _.Yield _ = { + RemoteVNet = Unmanaged(virtualNetworks.resourceId "") + Direction = TwoWay + Access = AccessAndForward + Transit = GatewayTransitDisabled + DependsOn = Set.empty + } [] member _.VNet(state: VNetPeeringSpec, vnet) = { state with RemoteVNet = vnet } - member _.VNet(state: VNetPeeringSpec, vnet: VirtualNetworkConfig) = - { state with + member _.VNet(state: VNetPeeringSpec, vnet: VirtualNetworkConfig) = { + state with RemoteVNet = Managed vnet.ResourceId - } + } [] member _.Mode(state: VNetPeeringSpec, direction) = { state with Direction = direction } @@ -711,10 +692,10 @@ type VNetPeeringSpecBuilder() = member _.GatewayTransit(state: VNetPeeringSpec, transit) = { state with Transit = transit } interface IDependable with - member _.Add state resources = - { state with + member _.Add state resources = { + state with DependsOn = state.DependsOn |> Set.union resources - } + } let vnetPeering = VNetPeeringSpecBuilder() diff --git a/src/Farmer/Builders/Builders.VirtualNetworkGateway.fs b/src/Farmer/Builders/Builders.VirtualNetworkGateway.fs index cef60cb7b..75a6b1859 100644 --- a/src/Farmer/Builders/Builders.VirtualNetworkGateway.fs +++ b/src/Farmer/Builders/Builders.VirtualNetworkGateway.fs @@ -6,113 +6,110 @@ open Farmer.PublicIpAddress open Farmer.VirtualNetworkGateway open Farmer.Arm.Network -type VpnClientConfig = - { - ClientAddressPools: IPAddressCidr list - ClientRootCertificates: {| Name: string - PublicCertData: string |} list - ClientRevokedCertificates: {| Name: string; Thumbprint: string |} list - ClientProtocols: VPNClientProtocol list - } - -type VNetGatewayConfig = - { - /// The name of the gateway - Name: ResourceName - /// Private IP allocation method for the gateway's primary interface - GatewayPrivateIpAllocationMethod: PrivateIpAddress.AllocationMethod - /// Public IP for the gateway's interface - GatewayPublicIpName: ResourceName - /// Private IP allocation method for the gateway's secondary interface if Active-Active - ActiveActivePrivateIpAllocationMethod: PrivateIpAddress.AllocationMethod - /// Public IP for the gateway's secondary interface if Active-Active - ActiveActivePublicIpName: ResourceName option - /// Virtual network where the gateway will be attached - VirtualNetwork: ResourceName - /// Gateway type - ExpressRoute or VPN - GatewayType: GatewayType - /// VPN type - RouteBased or PolicyBased - VpnType: VpnType - /// VPN client configuration for Point to Site connexion - VpnClientConfiguration: VpnClientConfig option - /// Enable Border Gateway Protocol on this gateway - EnableBgp: bool - Tags: Map - } +type VpnClientConfig = { + ClientAddressPools: IPAddressCidr list + ClientRootCertificates: + {| + Name: string + PublicCertData: string + |} list + ClientRevokedCertificates: {| Name: string; Thumbprint: string |} list + ClientProtocols: VPNClientProtocol list +} + +type VNetGatewayConfig = { + /// The name of the gateway + Name: ResourceName + /// Private IP allocation method for the gateway's primary interface + GatewayPrivateIpAllocationMethod: PrivateIpAddress.AllocationMethod + /// Public IP for the gateway's interface + GatewayPublicIpName: ResourceName + /// Private IP allocation method for the gateway's secondary interface if Active-Active + ActiveActivePrivateIpAllocationMethod: PrivateIpAddress.AllocationMethod + /// Public IP for the gateway's secondary interface if Active-Active + ActiveActivePublicIpName: ResourceName option + /// Virtual network where the gateway will be attached + VirtualNetwork: ResourceName + /// Gateway type - ExpressRoute or VPN + GatewayType: GatewayType + /// VPN type - RouteBased or PolicyBased + VpnType: VpnType + /// VPN client configuration for Point to Site connexion + VpnClientConfiguration: VpnClientConfig option + /// Enable Border Gateway Protocol on this gateway + EnableBgp: bool + Tags: Map +} with interface IBuilder with member this.ResourceId = virtualNetworkGateways.resourceId this.Name - member this.BuildResources location = - [ - if this.GatewayPublicIpName = ResourceName.Empty then - { // No public IP set, so generate one named after the gateway - Name = ResourceName $"{this.Name.Value}-ip" - AvailabilityZone = None - AllocationMethod = AllocationMethod.Dynamic - Location = location - Sku = PublicIpAddress.Sku.Basic - DomainNameLabel = None - Tags = this.Tags - } - { - Name = this.Name + member this.BuildResources location = [ + if this.GatewayPublicIpName = ResourceName.Empty then + { // No public IP set, so generate one named after the gateway + Name = ResourceName $"{this.Name.Value}-ip" + AvailabilityZone = None + AllocationMethod = AllocationMethod.Dynamic Location = location - IpConfigs = - [ - {| - Name = ResourceName "default" - PrivateIpAllocationMethod = this.GatewayPrivateIpAllocationMethod - PublicIpName = this.GatewayPublicIpName.IfEmpty $"{this.Name.Value}-ip" - |} - if this.ActiveActivePublicIpName.IsSome then - {| - Name = ResourceName "redundant" - PrivateIpAllocationMethod = this.ActiveActivePrivateIpAllocationMethod - PublicIpName = this.ActiveActivePublicIpName.Value - |} - ] - VirtualNetwork = this.VirtualNetwork - GatewayType = this.GatewayType - VpnType = this.VpnType - EnableBgp = this.EnableBgp - VpnClientConfiguration = - this.VpnClientConfiguration - |> Option.map (fun config -> - { - VpnClientConfiguration.ClientAddressPools = config.ClientAddressPools - ClientRootCertificates = config.ClientRootCertificates - ClientRevokedCertificates = config.ClientRevokedCertificates - ClientProtocols = config.ClientProtocols - }) + Sku = PublicIpAddress.Sku.Basic + DomainNameLabel = None Tags = this.Tags } - ] + { + Name = this.Name + Location = location + IpConfigs = [ + {| + Name = ResourceName "default" + PrivateIpAllocationMethod = this.GatewayPrivateIpAllocationMethod + PublicIpName = this.GatewayPublicIpName.IfEmpty $"{this.Name.Value}-ip" + |} + if this.ActiveActivePublicIpName.IsSome then + {| + Name = ResourceName "redundant" + PrivateIpAllocationMethod = this.ActiveActivePrivateIpAllocationMethod + PublicIpName = this.ActiveActivePublicIpName.Value + |} + ] + VirtualNetwork = this.VirtualNetwork + GatewayType = this.GatewayType + VpnType = this.VpnType + EnableBgp = this.EnableBgp + VpnClientConfiguration = + this.VpnClientConfiguration + |> Option.map (fun config -> { + VpnClientConfiguration.ClientAddressPools = config.ClientAddressPools + ClientRootCertificates = config.ClientRootCertificates + ClientRevokedCertificates = config.ClientRevokedCertificates + ClientProtocols = config.ClientProtocols + }) + Tags = this.Tags + } + ] type VpnClientConfigurationBuilder() = - member _.Yield _ = - { - ClientAddressPools = [] - ClientRootCertificates = [] - ClientRevokedCertificates = [] - ClientProtocols = [] - } + member _.Yield _ = { + ClientAddressPools = [] + ClientRootCertificates = [] + ClientRevokedCertificates = [] + ClientProtocols = [] + } member _.Run(state: VpnClientConfig) = match state.ClientProtocols with - | [] -> - { state with + | [] -> { + state with ClientProtocols = [ SSTP ] - } + } | _ -> state /// Adds an address pool which represents Address space for P2S VpnClient [] - member _.AddAddressPool(state: VpnClientConfig, prefix: IPAddressCidr) = - { state with + member _.AddAddressPool(state: VpnClientConfig, prefix: IPAddressCidr) = { + state with ClientAddressPools = state.ClientAddressPools @ [ prefix ] - } + } member this.AddAddressPool(state: VpnClientConfig, prefix: string) = this.AddAddressPool(state, IPAddressCidr.parse prefix) @@ -134,21 +131,22 @@ type VpnClientConfigurationBuilder() = else publicCertificate - { state with - ClientRootCertificates = - state.ClientRootCertificates - @ [ - {| - Name = name - PublicCertData = certData - |} - ] + { + state with + ClientRootCertificates = + state.ClientRootCertificates + @ [ + {| + Name = name + PublicCertData = certData + |} + ] } /// Adds the thumbprint of a revoked client certificate. [] - member _.AddRevokedCertificate(state: VpnClientConfig, name: string, thumbprint: string) = - { state with + member _.AddRevokedCertificate(state: VpnClientConfig, name: string, thumbprint: string) = { + state with ClientRevokedCertificates = state.ClientRevokedCertificates @ [ @@ -157,32 +155,31 @@ type VpnClientConfigurationBuilder() = Thumbprint = thumbprint |} ] - } + } /// Sets the protocols for the client VPN connexion. Default is SSTP [] - member _.SetProtocols(state: VpnClientConfig, protocols: VPNClientProtocol list) = - { state with + member _.SetProtocols(state: VpnClientConfig, protocols: VPNClientProtocol list) = { + state with ClientProtocols = protocols - } + } let vpnclient = VpnClientConfigurationBuilder() type VnetGatewayBuilder() = - member _.Yield _ = - { - Name = ResourceName.Empty - GatewayPrivateIpAllocationMethod = PrivateIpAddress.DynamicPrivateIp - GatewayPublicIpName = ResourceName.Empty - ActiveActivePrivateIpAllocationMethod = PrivateIpAddress.DynamicPrivateIp - ActiveActivePublicIpName = None - VirtualNetwork = ResourceName.Empty - GatewayType = GatewayType.Vpn VpnGatewaySku.VpnGw1 - VpnType = VpnType.RouteBased - EnableBgp = true - VpnClientConfiguration = None - Tags = Map.empty - } + member _.Yield _ = { + Name = ResourceName.Empty + GatewayPrivateIpAllocationMethod = PrivateIpAddress.DynamicPrivateIp + GatewayPublicIpName = ResourceName.Empty + ActiveActivePrivateIpAllocationMethod = PrivateIpAddress.DynamicPrivateIp + ActiveActivePublicIpName = None + VirtualNetwork = ResourceName.Empty + GatewayType = GatewayType.Vpn VpnGatewaySku.VpnGw1 + VpnType = VpnType.RouteBased + EnableBgp = true + VpnClientConfiguration = None + Tags = Map.empty + } /// Sets the name of the gateway [] @@ -190,29 +187,29 @@ type VnetGatewayBuilder() = /// Sets the virtual network where this gateway is attached. [] - member _.VNet(state: VNetGatewayConfig, vnet) = - { state with + member _.VNet(state: VNetGatewayConfig, vnet) = { + state with VirtualNetwork = ResourceName vnet - } + } - member _.VNet(state: VNetGatewayConfig, vnet: VirtualNetworkConfig) = - { state with + member _.VNet(state: VNetGatewayConfig, vnet: VirtualNetworkConfig) = { + state with VirtualNetwork = vnet.ResourceId.Name - } + } /// Sets the ExpressRoute gateway type with an ExpressRoute SKU. [] - member _.ErGatewaySku(state: VNetGatewayConfig, sku) = - { state with + member _.ErGatewaySku(state: VNetGatewayConfig, sku) = { + state with GatewayType = GatewayType.ExpressRoute sku - } + } /// Sets the VPN gateway type with a VPN SKU. [] - member _.VpnType(state: VNetGatewayConfig, sku) = - { state with + member _.VpnType(state: VNetGatewayConfig, sku) = { + state with GatewayType = GatewayType.Vpn sku - } + } /// Sets the VPN type with - RouteBased or PolicyBased. [] @@ -220,22 +217,22 @@ type VnetGatewayBuilder() = /// Sets the default gateway IP config. [] - member _.GatewayIpConfig(state: VNetGatewayConfig, allocationMethod, publicIp: PublicIpAddress) = - { state with + member _.GatewayIpConfig(state: VNetGatewayConfig, allocationMethod, publicIp: PublicIpAddress) = { + state with GatewayPrivateIpAllocationMethod = allocationMethod GatewayPublicIpName = publicIp.Name - } + } /// Sets the default gateway IP config and enables active-active if not already. [] member _.ActiveActiveIpConfig(state: VNetGatewayConfig, allocationMethod, publicIpName) = match state.GatewayType with | GatewayType.ExpressRoute _ -> state // No active-active config on ER gateways - | GatewayType.Vpn _ -> - { state with + | GatewayType.Vpn _ -> { + state with ActiveActivePrivateIpAllocationMethod = allocationMethod ActiveActivePublicIpName = Some(ResourceName publicIpName) - } + } /// Disable BGP (enabled by default). [] @@ -243,61 +240,58 @@ type VnetGatewayBuilder() = [] /// Sets the VPN Client configuration. - member _.SetVpnClient(state: VNetGatewayConfig, vpnClientConfig) = - { state with + member _.SetVpnClient(state: VNetGatewayConfig, vpnClientConfig) = { + state with VpnClientConfiguration = Some vpnClientConfig - } + } interface ITaggable with - member _.Add state tags = - { state with + member _.Add state tags = { + state with Tags = state.Tags |> Map.merge tags - } + } let gateway = VnetGatewayBuilder() -type ConnectionConfig = - { - Name: ResourceName - ConnectionType: ConnectionType - VirtualNetworkGateway1: ResourceName - VirtualNetworkGateway2: ResourceName option - LocalNetworkGateway: ResourceName option - PeerId: string option - AuthorizationKey: string option - Tags: Map - } +type ConnectionConfig = { + Name: ResourceName + ConnectionType: ConnectionType + VirtualNetworkGateway1: ResourceName + VirtualNetworkGateway2: ResourceName option + LocalNetworkGateway: ResourceName option + PeerId: string option + AuthorizationKey: string option + Tags: Map +} with interface IBuilder with member this.ResourceId = connections.resourceId this.Name - member this.BuildResources location = - [ - { - Name = this.Name - Location = location - ConnectionType = this.ConnectionType - VirtualNetworkGateway1 = this.VirtualNetworkGateway1 - VirtualNetworkGateway2 = this.VirtualNetworkGateway2 - LocalNetworkGateway = this.LocalNetworkGateway - PeerId = this.PeerId - AuthorizationKey = this.AuthorizationKey - Tags = this.Tags - } - ] + member this.BuildResources location = [ + { + Name = this.Name + Location = location + ConnectionType = this.ConnectionType + VirtualNetworkGateway1 = this.VirtualNetworkGateway1 + VirtualNetworkGateway2 = this.VirtualNetworkGateway2 + LocalNetworkGateway = this.LocalNetworkGateway + PeerId = this.PeerId + AuthorizationKey = this.AuthorizationKey + Tags = this.Tags + } + ] type ConnectionBuilder() = - member _.Yield _ = - { - Name = ResourceName.Empty - ConnectionType = ConnectionType.ExpressRoute - VirtualNetworkGateway1 = ResourceName.Empty - VirtualNetworkGateway2 = None - LocalNetworkGateway = None - PeerId = None - AuthorizationKey = None - Tags = Map.empty - } + member _.Yield _ = { + Name = ResourceName.Empty + ConnectionType = ConnectionType.ExpressRoute + VirtualNetworkGateway1 = ResourceName.Empty + VirtualNetworkGateway2 = None + LocalNetworkGateway = None + PeerId = None + AuthorizationKey = None + Tags = Map.empty + } /// Sets the name of the connection [] @@ -305,22 +299,21 @@ type ConnectionBuilder() = /// Sets the first vnet gateway [] - member _.VNetGateway1(state: ConnectionConfig, vng1) = - { state with + member _.VNetGateway1(state: ConnectionConfig, vng1) = { + state with VirtualNetworkGateway1 = vng1 - } + } /// Sets the first vnet gateway [] - member _.VNetGateway2(state: ConnectionConfig, vng2) = - { state with + member _.VNetGateway2(state: ConnectionConfig, vng2) = { + state with VirtualNetworkGateway2 = vng2 - } + } /// Sets the first vnet gateway [] - member _.LocalGateway(state: ConnectionConfig, lng) = - { state with LocalNetworkGateway = lng } + member _.LocalGateway(state: ConnectionConfig, lng) = { state with LocalNetworkGateway = lng } /// Sets the first vnet gateway [] @@ -328,15 +321,15 @@ type ConnectionBuilder() = /// Sets the first vnet gateway [] - member _.Authorization(state: ConnectionConfig, auth) = - { state with + member _.Authorization(state: ConnectionConfig, auth) = { + state with AuthorizationKey = Some auth - } + } interface ITaggable with - member _.Add state tags = - { state with + member _.Add state tags = { + state with Tags = state.Tags |> Map.merge tags - } + } let connection = ConnectionBuilder() diff --git a/src/Farmer/Builders/Builders.VirtualWan.fs b/src/Farmer/Builders/Builders.VirtualWan.fs index e9ef7496a..36873e782 100644 --- a/src/Farmer/Builders/Builders.VirtualWan.fs +++ b/src/Farmer/Builders/Builders.VirtualWan.fs @@ -5,40 +5,37 @@ module Farmer.Builders.VirtualWan open Farmer open Farmer.Arm.VirtualWan -type VirtualWanConfig = - { - Name: ResourceName - AllowBranchToBranchTraffic: bool option - DisableVpnEncryption: bool option - Office365LocalBreakoutCategory: Office365LocalBreakoutCategory option - VwanType: VwanType - } +type VirtualWanConfig = { + Name: ResourceName + AllowBranchToBranchTraffic: bool option + DisableVpnEncryption: bool option + Office365LocalBreakoutCategory: Office365LocalBreakoutCategory option + VwanType: VwanType +} with interface IBuilder with member this.ResourceId = virtualWans.resourceId this.Name - member this.BuildResources location = - [ - { - Name = this.Name - Location = location - AllowBranchToBranchTraffic = this.AllowBranchToBranchTraffic - DisableVpnEncryption = this.DisableVpnEncryption - Office365LocalBreakoutCategory = this.Office365LocalBreakoutCategory - VwanType = this.VwanType - } - ] + member this.BuildResources location = [ + { + Name = this.Name + Location = location + AllowBranchToBranchTraffic = this.AllowBranchToBranchTraffic + DisableVpnEncryption = this.DisableVpnEncryption + Office365LocalBreakoutCategory = this.Office365LocalBreakoutCategory + VwanType = this.VwanType + } + ] type VirtualWanBuilder() = /// Yield sets everything to sane defaults. - member _.Yield _ : VirtualWanConfig = - { - Name = ResourceName.Empty - AllowBranchToBranchTraffic = None - DisableVpnEncryption = None - Office365LocalBreakoutCategory = None - VwanType = VwanType.Basic - } + member _.Yield _ : VirtualWanConfig = { + Name = ResourceName.Empty + AllowBranchToBranchTraffic = None + DisableVpnEncryption = None + Office365LocalBreakoutCategory = None + VwanType = VwanType.Basic + } /// Sets the name to a ResourceName from the given string. [] @@ -46,31 +43,31 @@ type VirtualWanBuilder() = /// Sets the VWAN type to "standard" instead of the default "basic". [] - member _.StandardVwanType(state: VirtualWanConfig) = - { state with + member _.StandardVwanType(state: VirtualWanConfig) = { + state with VwanType = VwanType.Standard - } + } /// Allow branch to branch traffic. [] - member _.AllowBranchToBranchTraffic(state: VirtualWanConfig) = - { state with + member _.AllowBranchToBranchTraffic(state: VirtualWanConfig) = { + state with AllowBranchToBranchTraffic = Some true - } + } /// Disable vpn encryption [] - member _.DisableVpnEncryption(state: VirtualWanConfig) = - { state with + member _.DisableVpnEncryption(state: VirtualWanConfig) = { + state with DisableVpnEncryption = Some true - } + } /// Sets the office local breakout category [] - member _.Office365LocalBreakoutCategory(state: VirtualWanConfig, category) = - { state with + member _.Office365LocalBreakoutCategory(state: VirtualWanConfig, category) = { + state with Office365LocalBreakoutCategory = Some category - } + } /// This creates the keyword for a builder, such as `vwan { name "my-vwan" } let vwan = VirtualWanBuilder() diff --git a/src/Farmer/Builders/Builders.Vm.fs b/src/Farmer/Builders/Builders.Vm.fs index f3757b529..976e67323 100644 --- a/src/Farmer/Builders/Builders.Vm.fs +++ b/src/Farmer/Builders/Builders.Vm.fs @@ -17,47 +17,46 @@ open Farmer.Identity let makeName (vmName: ResourceName) elementType = ResourceName $"{vmName.Value}-%s{elementType}" -type VmConfig = - { - Name: ResourceName - AvailabilityZone: string option - DiagnosticsEnabled: bool option - DiagnosticsStorageAccount: ResourceRef option - - Priority: Priority option - - Username: string option - PasswordParameter: string option - Size: VMSize - OsDisk: OsDiskCreateOption - DataDisks: DataDiskCreateOption list option - - CustomScript: string option - CustomScriptFiles: Uri list - - DomainNamePrefix: string option - - CustomData: string option - DisablePasswordAuthentication: bool option - SshPathAndPublicKeys: (string * string) list option - AadSshLogin: FeatureFlag - - VNet: ResourceRef - AddressPrefix: string - SubnetPrefix: string - Subnet: AutoGeneratedResource - PublicIp: ResourceRef option - IpAllocation: PublicIpAddress.AllocationMethod option - AcceleratedNetworking: FeatureFlag option - IpForwarding: FeatureFlag option - IpConfigs: IpConfiguration list - PrivateIpAllocation: PrivateIpAddress.AllocationMethod option - LoadBalancerBackendAddressPools: LinkedResource list - Identity: Identity.ManagedIdentity - NetworkSecurityGroup: LinkedResource option - - Tags: Map - } +type VmConfig = { + Name: ResourceName + AvailabilityZone: string option + DiagnosticsEnabled: bool option + DiagnosticsStorageAccount: ResourceRef option + + Priority: Priority option + + Username: string option + PasswordParameter: string option + Size: VMSize + OsDisk: OsDiskCreateOption + DataDisks: DataDiskCreateOption list option + + CustomScript: string option + CustomScriptFiles: Uri list + + DomainNamePrefix: string option + + CustomData: string option + DisablePasswordAuthentication: bool option + SshPathAndPublicKeys: (string * string) list option + AadSshLogin: FeatureFlag + + VNet: ResourceRef + AddressPrefix: string + SubnetPrefix: string + Subnet: AutoGeneratedResource + PublicIp: ResourceRef option + IpAllocation: PublicIpAddress.AllocationMethod option + AcceleratedNetworking: FeatureFlag option + IpForwarding: FeatureFlag option + IpConfigs: IpConfiguration list + PrivateIpAllocation: PrivateIpAddress.AllocationMethod option + LoadBalancerBackendAddressPools: LinkedResource list + Identity: Identity.ManagedIdentity + NetworkSecurityGroup: LinkedResource option + + Tags: Map +} with member internal this.DeriveResourceName (resourceType: ResourceType) elementName = resourceType.resourceId (makeName this.Name elementName) @@ -92,8 +91,9 @@ type VmConfig = :: this.IpConfigs |> List.map (fun ipconfig -> // Ensure all IP configs have a subnet IP, defaulting to the one for the VM. - { ipconfig with - SubnetName = ipconfig.SubnetName.IfEmpty(subnetId.Name.Value) + { + ipconfig with + SubnetName = ipconfig.SubnetName.IfEmpty(subnetId.Name.Value) }) /// Builds NICs for this VM, one for each subnet. @@ -157,11 +157,10 @@ type VmConfig = Priority = this.Priority Credentials = match this.Username with - | Some username -> - {| - Username = username - Password = SecureParameter this.PasswordParameterArm - |} + | Some username -> {| + Username = username + Password = SecureParameter this.PasswordParameterArm + |} | None -> raiseFarmer $"You must specify a username for virtual machine {this.Name.Value}" CustomData = this.CustomData DisablePasswordAuthentication = this.DisablePasswordAuthentication @@ -189,90 +188,84 @@ type VmConfig = // VNET match this.VNet with - | DeployableResource this vnet -> - { - Name = this.VNet.resourceId(this).Name - Location = location - AddressSpacePrefixes = [ this.AddressPrefix ] - Subnets = - [ - { - Name = subnetId.Name - Prefix = this.SubnetPrefix - VirtualNetwork = Some(Managed vnet) - NetworkSecurityGroup = nsgId |> Option.map (fun x -> Managed x) - Delegations = [] - NatGateway = None - ServiceEndpoints = [] - AssociatedServiceEndpointPolicies = [] - PrivateEndpointNetworkPolicies = None - PrivateLinkServiceNetworkPolicies = None - } - ] - Tags = this.Tags - } + | DeployableResource this vnet -> { + Name = this.VNet.resourceId(this).Name + Location = location + AddressSpacePrefixes = [ this.AddressPrefix ] + Subnets = [ + { + Name = subnetId.Name + Prefix = this.SubnetPrefix + VirtualNetwork = Some(Managed vnet) + NetworkSecurityGroup = nsgId |> Option.map (fun x -> Managed x) + Delegations = [] + NatGateway = None + ServiceEndpoints = [] + AssociatedServiceEndpointPolicies = [] + PrivateEndpointNetworkPolicies = None + PrivateLinkServiceNetworkPolicies = None + } + ] + Tags = this.Tags + } | _ -> () // IP Address match this.PublicIp with - | Some ref -> - { - Name = (ref.resourceId this).Name - Location = location - AllocationMethod = - match this.IpAllocation with - | Some x -> x - | None when this.AvailabilityZone.IsSome -> PublicIpAddress.AllocationMethod.Static - | None -> PublicIpAddress.AllocationMethod.Dynamic - Sku = - if this.AvailabilityZone.IsSome then - PublicIpAddress.Sku.Standard - else - PublicIpAddress.Sku.Basic - DomainNameLabel = this.DomainNamePrefix - Tags = this.Tags - AvailabilityZone = this.AvailabilityZone - } + | Some ref -> { + Name = (ref.resourceId this).Name + Location = location + AllocationMethod = + match this.IpAllocation with + | Some x -> x + | None when this.AvailabilityZone.IsSome -> PublicIpAddress.AllocationMethod.Static + | None -> PublicIpAddress.AllocationMethod.Dynamic + Sku = + if this.AvailabilityZone.IsSome then + PublicIpAddress.Sku.Standard + else + PublicIpAddress.Sku.Basic + DomainNameLabel = this.DomainNamePrefix + Tags = this.Tags + AvailabilityZone = this.AvailabilityZone + } | None -> () // Storage account - optional match this.DiagnosticsStorageAccount with - | Some (DeployableResource this resourceId) -> - { - Name = Storage.StorageAccountName.Create(resourceId.Name).OkValue - Location = location - Dependencies = [] - Sku = Storage.Sku.Standard_LRS - NetworkAcls = None - StaticWebsite = None - EnableHierarchicalNamespace = None - MinTlsVersion = None - Tags = this.Tags - DnsZoneType = None - DisablePublicNetworkAccess = None - DisableBlobPublicAccess = None - DisableSharedKeyAccess = None - DefaultToOAuthAuthentication = None - } + | Some(DeployableResource this resourceId) -> { + Name = Storage.StorageAccountName.Create(resourceId.Name).OkValue + Location = location + Dependencies = [] + Sku = Storage.Sku.Standard_LRS + NetworkAcls = None + StaticWebsite = None + EnableHierarchicalNamespace = None + MinTlsVersion = None + Tags = this.Tags + DnsZoneType = None + DisablePublicNetworkAccess = None + DisableBlobPublicAccess = None + DisableSharedKeyAccess = None + DefaultToOAuthAuthentication = None + } | Some _ | None -> () // Custom Script - optional match this.CustomScript, this.CustomScriptFiles with - | Some script, files -> - { - Name = this.Name.Map(sprintf "%s-custom-script") - Location = location - VirtualMachine = this.Name - OS = - match this.OsDisk with - | FromImage (image, _) -> image.OS - | _ -> - raiseFarmer "Unable to determine OS for custom script when attaching an existing disk" - ScriptContents = script - FileUris = files - Tags = this.Tags - } + | Some script, files -> { + Name = this.Name.Map(sprintf "%s-custom-script") + Location = location + VirtualMachine = this.Name + OS = + match this.OsDisk with + | FromImage(image, _) -> image.OS + | _ -> raiseFarmer "Unable to determine OS for custom script when attaching an existing disk" + ScriptContents = script + FileUris = files + Tags = this.Tags + } | None, [] -> () | None, _ -> raiseFarmer @@ -280,20 +273,19 @@ type VmConfig = // Azure AD SSH login extension match this.AadSshLogin, this.OsDisk with - | FeatureFlag.Enabled, FromImage (image, _) when + | FeatureFlag.Enabled, FromImage(image, _) when image.OS = Linux && this.Identity.SystemAssigned = Disabled -> raiseFarmer "AAD SSH login requires that system assigned identity be enabled on the virtual machine." - | FeatureFlag.Enabled, FromImage (image, _) when image.OS = Windows -> + | FeatureFlag.Enabled, FromImage(image, _) when image.OS = Windows -> raiseFarmer "AAD SSH login is only supported for Linux Virtual Machines" // Assuming a user that attaches a disk knows to only using this extension for Linux images. - | FeatureFlag.Enabled, _ -> - { - AadSshLoginExtension.Location = location - VirtualMachine = this.Name - Tags = this.Tags - } + | FeatureFlag.Enabled, _ -> { + AadSshLoginExtension.Location = location + VirtualMachine = this.Name + Tags = this.Tags + } | FeatureFlag.Disabled, _ -> () ] @@ -303,63 +295,62 @@ type VirtualMachineBuilder() = |> AutoGeneratedResource |> Some - member _.Yield _ = - { - Name = ResourceName.Empty - AvailabilityZone = None - DiagnosticsEnabled = None - DiagnosticsStorageAccount = None - Priority = None - Size = Basic_A0 - Username = None - PasswordParameter = None - DataDisks = Some [] - Identity = ManagedIdentity.Empty - CustomScript = None - CustomScriptFiles = [] - DomainNamePrefix = None - CustomData = None - DisablePasswordAuthentication = None - SshPathAndPublicKeys = None - AadSshLogin = FeatureFlag.Disabled - OsDisk = FromImage(WindowsServer_2012Datacenter, { Size = 128; DiskType = Standard_LRS }) - AddressPrefix = "10.0.0.0/16" - SubnetPrefix = "10.0.0.0/24" - VNet = derived (fun config -> config.DeriveResourceName virtualNetworks "vnet") - Subnet = Derived(fun config -> config.DeriveResourceName subnets "subnet") - PublicIp = automaticPublicIp - IpAllocation = None - AcceleratedNetworking = None - IpForwarding = None - IpConfigs = [] - PrivateIpAllocation = None - LoadBalancerBackendAddressPools = [] - NetworkSecurityGroup = None - Tags = Map.empty - } + member _.Yield _ = { + Name = ResourceName.Empty + AvailabilityZone = None + DiagnosticsEnabled = None + DiagnosticsStorageAccount = None + Priority = None + Size = Basic_A0 + Username = None + PasswordParameter = None + DataDisks = Some [] + Identity = ManagedIdentity.Empty + CustomScript = None + CustomScriptFiles = [] + DomainNamePrefix = None + CustomData = None + DisablePasswordAuthentication = None + SshPathAndPublicKeys = None + AadSshLogin = FeatureFlag.Disabled + OsDisk = FromImage(WindowsServer_2012Datacenter, { Size = 128; DiskType = Standard_LRS }) + AddressPrefix = "10.0.0.0/16" + SubnetPrefix = "10.0.0.0/24" + VNet = derived (fun config -> config.DeriveResourceName virtualNetworks "vnet") + Subnet = Derived(fun config -> config.DeriveResourceName subnets "subnet") + PublicIp = automaticPublicIp + IpAllocation = None + AcceleratedNetworking = None + IpForwarding = None + IpConfigs = [] + PrivateIpAllocation = None + LoadBalancerBackendAddressPools = [] + NetworkSecurityGroup = None + Tags = Map.empty + } member _.Run(state: VmConfig) = match state.AcceleratedNetworking with - | Some (Enabled) -> + | Some(Enabled) -> match state.Size with | NetworkInterface.AcceleratedNetworkingUnsupported -> raiseFarmer $"Accelerated networking unsupported for specified VM size '{state.Size.ArmValue}'." | NetworkInterface.AcceleratedNetworkingSupported -> () | _ -> () - { state with - DataDisks = - state.DataDisks - |> Option.map (function - | [] -> - [ + { + state with + DataDisks = + state.DataDisks + |> Option.map (function + | [] -> [ { Size = 1024 DiskType = DiskType.Standard_LRS } |> DataDiskCreateOption.Empty - ] - | other -> other) + ] + | other -> other) } /// Sets the name of the VM. @@ -369,10 +360,10 @@ type VirtualMachineBuilder() = member this.Name(state: VmConfig, name) = this.Name(state, ResourceName name) [] - member _.AddAvailabilityZone(state: VmConfig, az: string) = - { state with + member _.AddAvailabilityZone(state: VmConfig, az: string) = { + state with AvailabilityZone = Some az - } + } /// Turns on diagnostics support using an automatically created storage account. [] @@ -382,32 +373,33 @@ type VirtualMachineBuilder() = let name = config.Name.Map(sprintf "%sstorage") |> sanitiseStorage |> ResourceName storageAccounts.resourceId name) - { state with - DiagnosticsEnabled = Some true - DiagnosticsStorageAccount = Some storageResourceRef + { + state with + DiagnosticsEnabled = Some true + DiagnosticsStorageAccount = Some storageResourceRef } /// Turns on diagnostics support using an externally managed storage account. [] - member _.StorageAccountNameExternal(state: VmConfig, linkedResource: LinkedResource) = - { state with + member _.StorageAccountNameExternal(state: VmConfig, linkedResource: LinkedResource) = { + state with DiagnosticsEnabled = Some true DiagnosticsStorageAccount = Some(LinkedResource linkedResource) - } + } - member _.StorageAccountNameExternal(state: VmConfig, id: ResourceId) = - { state with + member _.StorageAccountNameExternal(state: VmConfig, id: ResourceId) = { + state with DiagnosticsEnabled = Some true DiagnosticsStorageAccount = Some(LinkedResource(Unmanaged id)) - } + } /// Turns on diagnostics support using an Azure-managed storage account. [] - member _.DiagnosticsSupportManagedStorage(state: VmConfig) = - { state with + member _.DiagnosticsSupportManagedStorage(state: VmConfig) = { + state with DiagnosticsEnabled = Some true DiagnosticsStorageAccount = None - } + } /// Sets the size of the VM. [] @@ -419,29 +411,28 @@ type VirtualMachineBuilder() = /// Sets the name of the template parameter which will contain the admin password for this VM. defaults to "password-for-" [] - member _.PasswordParameter(state: VmConfig, parameterName) = - { state with + member _.PasswordParameter(state: VmConfig, parameterName) = { + state with PasswordParameter = Some parameterName - } + } /// Sets the operating system of the VM. A set of samples is provided in the `CommonImages` module. [] member _.ConfigureOs(state: VmConfig, image) = let osDisk = match state.OsDisk with - | FromImage (_, diskInfo) -> FromImage(image, diskInfo) + | FromImage(_, diskInfo) -> FromImage(image, diskInfo) | AttachOsDisk _ -> raiseFarmer "Operating system from attached disk will be used" { state with OsDisk = osDisk } member this.ConfigureOs(state: VmConfig, (os, offer, publisher, sku)) = - let image = - { - OS = os - Offer = Offer offer - Publisher = Publisher publisher - Sku = ImageSku sku - } + let image = { + OS = os + Offer = Offer offer + Publisher = Publisher publisher + Sku = ImageSku sku + } this.ConfigureOs(state, image) @@ -453,12 +444,12 @@ type VirtualMachineBuilder() = let osDisk = match state.OsDisk with - | FromImage (image, diskInfo) -> - let updatedDiskInfo = - { diskInfo with + | FromImage(image, diskInfo) -> + let updatedDiskInfo = { + diskInfo with DiskType = diskType Size = size - } + } FromImage(image, updatedDiskInfo) | AttachOsDisk _ -> state.OsDisk // uses the size and type from the attached disk @@ -466,56 +457,56 @@ type VirtualMachineBuilder() = { state with OsDisk = osDisk } [] - member _.AttachOsDisk(state: VmConfig, os: OS, disk: DiskConfig) = - { state with + member _.AttachOsDisk(state: VmConfig, os: OS, disk: DiskConfig) = { + state with OsDisk = AttachOsDisk(os, Managed((disk :> IBuilder).ResourceId)) - } + } - member _.AttachOsDisk(state: VmConfig, os: OS, diskId: ResourceId) = - { state with + member _.AttachOsDisk(state: VmConfig, os: OS, diskId: ResourceId) = { + state with OsDisk = AttachOsDisk(os, Managed diskId) - } + } [] - member _.AttachExistingOsDisk(state: VmConfig, os: OS, disk: DiskConfig) = - { state with + member _.AttachExistingOsDisk(state: VmConfig, os: OS, disk: DiskConfig) = { + state with OsDisk = AttachOsDisk(os, Unmanaged((disk :> IBuilder).ResourceId)) - } + } - member _.AttachExistingOsDisk(state: VmConfig, os: OS, diskId: ResourceId) = - { state with + member _.AttachExistingOsDisk(state: VmConfig, os: OS, diskId: ResourceId) = { + state with OsDisk = AttachOsDisk(os, Unmanaged diskId) - } + } [] member _.AttachDataDisk(state: VmConfig, diskId: ResourceId) = let existingDisks = state.DataDisks match existingDisks with - | Some disks -> - { state with + | Some disks -> { + state with DataDisks = disks @ [ AttachDataDisk(Managed diskId) ] |> Some - } - | None -> - { state with + } + | None -> { + state with DataDisks = [ AttachDataDisk(Managed diskId) ] |> Some - } + } member this.AttachDataDisk(state: VmConfig, disk: DiskConfig) = match disk.Sku with - | Some (UltraSSD_LRS) -> + | Some(UltraSSD_LRS) -> let existingDisks = state.DataDisks let diskId = (disk :> IBuilder).ResourceId match existingDisks with - | Some disks -> - { state with + | Some disks -> { + state with DataDisks = disks @ [ AttachUltra(Managed diskId) ] |> Some - } - | None -> - { state with + } + | None -> { + state with DataDisks = [ AttachUltra(Managed diskId) ] |> Some - } + } | _ -> this.AttachDataDisk(state, (disk :> IBuilder).ResourceId) @@ -524,30 +515,30 @@ type VirtualMachineBuilder() = let existingDisks = state.DataDisks match existingDisks with - | Some disks -> - { state with + | Some disks -> { + state with DataDisks = disks @ [ AttachDataDisk(Unmanaged diskId) ] |> Some - } - | None -> - { state with + } + | None -> { + state with DataDisks = [ AttachDataDisk(Unmanaged diskId) ] |> Some - } + } member this.AttachExistingDataDisk(state: VmConfig, disk: DiskConfig) = match disk.Sku with - | Some (UltraSSD_LRS) -> + | Some(UltraSSD_LRS) -> let existingDisks = state.DataDisks let diskId = (disk :> IBuilder).ResourceId match existingDisks with - | Some disks -> - { state with + | Some disks -> { + state with DataDisks = disks @ [ AttachUltra(Unmanaged diskId) ] |> Some - } - | None -> - { state with + } + | None -> { + state with DataDisks = [ AttachUltra(Unmanaged diskId) ] |> Some - } + } | _ -> this.AttachExistingDataDisk(state, (disk :> IBuilder).ResourceId) /// Adds a data disk to the VM with a specific size and type. @@ -558,10 +549,11 @@ type VirtualMachineBuilder() = | Some disks -> disks | None -> [] - { state with - DataDisks = - DataDiskCreateOption.Empty { Size = size; DiskType = diskType } :: existingDisks - |> Some + { + state with + DataDisks = + DataDiskCreateOption.Empty { Size = size; DiskType = diskType } :: existingDisks + |> Some } /// Provision the VM without generating a data disk (OS-only). @@ -584,10 +576,10 @@ type VirtualMachineBuilder() = | Some priority -> raiseFarmer $"Priority is already set to {priority}. Only one priority or spot_instance setting per VM is allowed" - | None -> - { state with + | None -> { + state with Priority = (evictionPolicy, maxPrice) |> Spot |> Some - } + } member this.Spot(state: VmConfig, evictionPolicy: EvictionPolicy) : VmConfig = this.Spot(state, (evictionPolicy, -1m)) @@ -606,8 +598,7 @@ type VirtualMachineBuilder() = /// Sets the prefix for the domain name of the VM. [] - member _.DomainNamePrefix(state: VmConfig, prefix) = - { state with DomainNamePrefix = prefix } + member _.DomainNamePrefix(state: VmConfig, prefix) = { state with DomainNamePrefix = prefix } /// Sets the IP address prefix of the VM. [] @@ -619,20 +610,20 @@ type VirtualMachineBuilder() = /// Sets the subnet name of the VM. [] - member _.SubnetName(state: VmConfig, name: ResourceName) = - { state with + member _.SubnetName(state: VmConfig, name: ResourceName) = { + state with Subnet = Named(subnets.resourceId name) - } + } member this.SubnetName(state: VmConfig, name) = this.SubnetName(state, ResourceName name) /// Control accelerated networking for the VM network interfaces [] - member _.AcceleratedNetworking(state: VmConfig, flag: FeatureFlag) = - { state with + member _.AcceleratedNetworking(state: VmConfig, flag: FeatureFlag) = { + state with AcceleratedNetworking = Some flag - } + } /// Enable or disable IP forwarding on the primary VM network interface. [] @@ -640,10 +631,10 @@ type VirtualMachineBuilder() = /// Uses an external VNet instead of creating a new one. [] - member _.LinkToVNet(state: VmConfig, name: ResourceName) = - { state with + member _.LinkToVNet(state: VmConfig, name: ResourceName) = { + state with VNet = LinkedResource(Managed(virtualNetworks.resourceId name)) - } + } member this.LinkToVNet(state: VmConfig, name) = this.LinkToVNet(state, ResourceName name) @@ -652,15 +643,15 @@ type VirtualMachineBuilder() = member this.LinkToVNet(state: VmConfig, vnet: VirtualNetworkConfig) = this.LinkToVNet(state, vnet.Name) [] - member _.LinkToUnmanagedVNet(state: VmConfig, id: ResourceId) = - { state with + member _.LinkToUnmanagedVNet(state: VmConfig, id: ResourceId) = { + state with VNet = LinkedResource(Unmanaged(id)) - } + } - member _.LinkToUnmanagedVNet(state: VmConfig, name: ResourceName) = - { state with + member _.LinkToUnmanagedVNet(state: VmConfig, name: ResourceName) = { + state with VNet = LinkedResource(Unmanaged(virtualNetworks.resourceId name)) - } + } member this.LinkToUnmanagedVNet(state: VmConfig, name) = this.LinkToUnmanagedVNet(state, ResourceName name) @@ -673,32 +664,32 @@ type VirtualMachineBuilder() = /// Adds the VM network interface to a load balancer backend address pool that is deployed with this VM. [] - member _.LinkToBackendAddressPool(state: VmConfig, backendResourceId: ResourceId) = - { state with + member _.LinkToBackendAddressPool(state: VmConfig, backendResourceId: ResourceId) = { + state with LoadBalancerBackendAddressPools = Managed(backendResourceId) :: state.LoadBalancerBackendAddressPools - } + } - member _.LinkToBackendAddressPool(state: VmConfig, backend: BackendAddressPoolConfig) = - { state with + member _.LinkToBackendAddressPool(state: VmConfig, backend: BackendAddressPoolConfig) = { + state with LoadBalancerBackendAddressPools = Managed((backend :> IBuilder).ResourceId) :: state.LoadBalancerBackendAddressPools - } + } /// Adds the VM network interface to an existing load balancer backend address pool. [] - member _.LinkToExistingBackendAddressPool(state: VmConfig, backendResourceId: ResourceId) = - { state with + member _.LinkToExistingBackendAddressPool(state: VmConfig, backendResourceId: ResourceId) = { + state with LoadBalancerBackendAddressPools = Unmanaged(backendResourceId) :: state.LoadBalancerBackendAddressPools - } + } [] member _.CustomScript(state: VmConfig, script: string) = match state.CustomScript with - | None -> - { state with + | None -> { + state with CustomScript = Some script - } + } | Some previousScript -> let firstScript = if script.Length > 10 then @@ -716,40 +707,40 @@ type VirtualMachineBuilder() = $"Only single custom_script execution is supported (and it can contain ARM-expressions). You have to merge your scripts. You have defined multiple custom_script: {firstScript} and {secondScript}" [] - member _.CustomScriptFiles(state: VmConfig, uris: string list) = - { state with + member _.CustomScriptFiles(state: VmConfig, uris: string list) = { + state with CustomScriptFiles = uris |> List.map Uri - } + } interface ITaggable with - member _.Add state tags = - { state with + member _.Add state tags = { + state with Tags = state.Tags |> Map.merge tags - } + } interface IIdentity with - member _.Add state updater = - { state with + member _.Add state updater = { + state with Identity = updater state.Identity - } + } [] - member _.CustomData(state: VmConfig, customData: string) = - { state with + member _.CustomData(state: VmConfig, customData: string) = { + state with CustomData = Some customData - } + } [] - member _.DisablePasswordAuthentication(state: VmConfig, disablePasswordAuthentication: bool) = - { state with + member _.DisablePasswordAuthentication(state: VmConfig, disablePasswordAuthentication: bool) = { + state with DisablePasswordAuthentication = Some disablePasswordAuthentication - } + } [] - member _.AddAuthorizedKeys(state: VmConfig, sshObjects: (string * string) list) = - { state with + member _.AddAuthorizedKeys(state: VmConfig, sshObjects: (string * string) list) = { + state with SshPathAndPublicKeys = Some sshObjects - } + } [] member this.AddAuthorizedKey(state: VmConfig, path: string, keyData: string) = @@ -757,8 +748,7 @@ type VirtualMachineBuilder() = /// Azure AD login extension may be enabled for Linux VM's. [] - member this.AadSshLoginEnabled(state: VmConfig, featureFlag: FeatureFlag) = - { state with AadSshLogin = featureFlag } + member this.AadSshLoginEnabled(state: VmConfig, featureFlag: FeatureFlag) = { state with AadSshLogin = featureFlag } [] /// Set the public IP for this VM @@ -766,123 +756,130 @@ type VirtualMachineBuilder() = member _.PublicIp(state: VmConfig, ref: ResourceRef<_>) = { state with PublicIp = Some ref } - member _.PublicIp(state: VmConfig, ref: LinkedResource) = - { state with + member _.PublicIp(state: VmConfig, ref: LinkedResource) = { + state with PublicIp = Some(LinkedResource ref) - } + } - member _.PublicIp(state: VmConfig, _: Automatic) = - { state with + member _.PublicIp(state: VmConfig, _: Automatic) = { + state with PublicIp = automaticPublicIp - } + } [] /// IP allocation method - member _.IpAllocation(state: VmConfig, ref: PublicIpAddress.AllocationMethod Option) = - { state with IpAllocation = ref } + member _.IpAllocation(state: VmConfig, ref: PublicIpAddress.AllocationMethod Option) = { + state with + IpAllocation = ref + } - member _.IpAllocation(state: VmConfig, ref: PublicIpAddress.AllocationMethod) = - { state with IpAllocation = Some ref } + member _.IpAllocation(state: VmConfig, ref: PublicIpAddress.AllocationMethod) = { + state with + IpAllocation = Some ref + } [] /// IP allocation method - member _.PrivateIpAllocation(state: VmConfig, ref: PrivateIpAddress.AllocationMethod Option) = - { state with PrivateIpAllocation = ref } + member _.PrivateIpAllocation(state: VmConfig, ref: PrivateIpAddress.AllocationMethod Option) = { + state with + PrivateIpAllocation = ref + } - member _.PrivateIpAllocation(state: VmConfig, ref: PrivateIpAddress.AllocationMethod) = - { state with + member _.PrivateIpAllocation(state: VmConfig, ref: PrivateIpAddress.AllocationMethod) = { + state with PrivateIpAllocation = Some ref - } + } [] /// Add additional IP configurations - member _.AddIpConfigurations(state: VmConfig, ipConfigs: IpConfiguration list) = - { state with + member _.AddIpConfigurations(state: VmConfig, ipConfigs: IpConfiguration list) = { + state with IpConfigs = state.IpConfigs @ ipConfigs - } + } /// Sets the network security group [] - member _.NetworkSecurityGroup(state: VmConfig, nsg: IArmResource) = - { state with + member _.NetworkSecurityGroup(state: VmConfig, nsg: IArmResource) = { + state with NetworkSecurityGroup = Some(Managed nsg.ResourceId) - } + } - member _.NetworkSecurityGroup(state: VmConfig, nsg: ResourceId) = - { state with + member _.NetworkSecurityGroup(state: VmConfig, nsg: ResourceId) = { + state with NetworkSecurityGroup = Some(Managed nsg) - } + } - member _.NetworkSecurityGroup(state: VmConfig, nsg: NsgConfig) = - { state with + member _.NetworkSecurityGroup(state: VmConfig, nsg: NsgConfig) = { + state with NetworkSecurityGroup = Some(Managed (nsg :> IBuilder).ResourceId) - } + } /// Links the VM to an existing network security group. [] - member _.LinkToNetworkSecurityGroup(state: VmConfig, nsg: IArmResource) = - { state with + member _.LinkToNetworkSecurityGroup(state: VmConfig, nsg: IArmResource) = { + state with NetworkSecurityGroup = Some(Unmanaged(nsg.ResourceId)) - } + } - member _.LinkToNetworkSecurityGroup(state: VmConfig, nsg: ResourceId) = - { state with + member _.LinkToNetworkSecurityGroup(state: VmConfig, nsg: ResourceId) = { + state with NetworkSecurityGroup = Some(Unmanaged nsg) - } + } - member _.LinkToNetworkSecurityGroup(state: VmConfig, nsg: NsgConfig) = - { state with + member _.LinkToNetworkSecurityGroup(state: VmConfig, nsg: NsgConfig) = { + state with NetworkSecurityGroup = Some(Unmanaged (nsg :> IBuilder).ResourceId) - } + } let vm = VirtualMachineBuilder() type IpConfigBuilder() = - member _.Yield _ = - { - SubnetName = ResourceName.Empty - PublicIpAddress = None - LoadBalancerBackendAddressPools = [] - PrivateIpAllocation = None - Primary = None - } + member _.Yield _ = { + SubnetName = ResourceName.Empty + PublicIpAddress = None + LoadBalancerBackendAddressPools = [] + PrivateIpAllocation = None + Primary = None + } [] member _.SubnetName(state: IpConfiguration, name: ResourceName) = { state with SubnetName = name } [] - member _.PublicIp(state: IpConfiguration, ref: LinkedResource) = - { state with + member _.PublicIp(state: IpConfiguration, ref: LinkedResource) = { + state with PublicIpAddress = Some ref - } + } [] - member _.LinkToBackendAddressPool(state: IpConfiguration, backendResourceId: ResourceId) = - { state with + member _.LinkToBackendAddressPool(state: IpConfiguration, backendResourceId: ResourceId) = { + state with LoadBalancerBackendAddressPools = Managed(backendResourceId) :: state.LoadBalancerBackendAddressPools - } + } - member _.LinkToBackendAddressPool(state: IpConfiguration, backend: BackendAddressPoolConfig) = - { state with + member _.LinkToBackendAddressPool(state: IpConfiguration, backend: BackendAddressPoolConfig) = { + state with LoadBalancerBackendAddressPools = Managed((backend :> IBuilder).ResourceId) :: state.LoadBalancerBackendAddressPools - } + } [] - member _.LinkToExistingBackendAddressPool(state: IpConfiguration, backendResourceId: ResourceId) = - { state with + member _.LinkToExistingBackendAddressPool(state: IpConfiguration, backendResourceId: ResourceId) = { + state with LoadBalancerBackendAddressPools = Unmanaged(backendResourceId) :: state.LoadBalancerBackendAddressPools - } + } [] - member _.PrivateIpAllocation(state: IpConfiguration, ref: AllocationMethod Option) = - { state with PrivateIpAllocation = ref } + member _.PrivateIpAllocation(state: IpConfiguration, ref: AllocationMethod Option) = { + state with + PrivateIpAllocation = ref + } - member _.PrivateIpAllocation(state: IpConfiguration, ref: AllocationMethod) = - { state with + member _.PrivateIpAllocation(state: IpConfiguration, ref: AllocationMethod) = { + state with PrivateIpAllocation = Some ref - } + } let ipConfig = IpConfigBuilder() diff --git a/src/Farmer/Builders/Builders.WebApp.fs b/src/Farmer/Builders/Builders.WebApp.fs index a6f67a187..c86125847 100644 --- a/src/Farmer/Builders/Builders.WebApp.fs +++ b/src/Farmer/Builders/Builders.WebApp.fs @@ -96,23 +96,22 @@ type SecretStore = | AppService | KeyVault of ResourceRef -type SlotConfig = - { - Name: string - AutoSwapSlotName: string option - AppSettings: Map - ConnectionStrings: Map - DockerRegistryPath: string option - StartupCommand: string option - Identity: ManagedIdentity - KeyVaultReferenceIdentity: UserAssignedIdentity option - Tags: Map - Dependencies: ResourceId Set - IpSecurityRestrictions: IpSecurityRestriction list - } - - member this.ToSite(owner: Arm.Web.Site) = - { owner with +type SlotConfig = { + Name: string + AutoSwapSlotName: string option + AppSettings: Map + ConnectionStrings: Map + DockerRegistryPath: string option + StartupCommand: string option + Identity: ManagedIdentity + KeyVaultReferenceIdentity: UserAssignedIdentity option + Tags: Map + Dependencies: ResourceId Set + IpSecurityRestrictions: IpSecurityRestriction list +} with + + member this.ToSite(owner: Arm.Web.Site) = { + owner with SiteType = SiteType.Slot(owner.Name / this.Name) Dependencies = owner.Dependencies |> Set.add (owner.ResourceType.resourceId owner.Name) AutoSwapSlotName = this.AutoSwapSlotName @@ -126,70 +125,69 @@ type SlotConfig = KeyVaultReferenceIdentity = this.KeyVaultReferenceIdentity |> Option.orElse owner.KeyVaultReferenceIdentity IpSecurityRestrictions = this.IpSecurityRestrictions ZipDeployPath = None - } + } type SlotBuilder() = - member this.Yield _ = - { - Name = "staging" - AutoSwapSlotName = None - AppSettings = Map.empty - ConnectionStrings = Map.empty - DockerRegistryPath = None - StartupCommand = None - Identity = ManagedIdentity.Empty - KeyVaultReferenceIdentity = None - Tags = Map.empty - Dependencies = Set.empty - IpSecurityRestrictions = [] - } + member this.Yield _ = { + Name = "staging" + AutoSwapSlotName = None + AppSettings = Map.empty + ConnectionStrings = Map.empty + DockerRegistryPath = None + StartupCommand = None + Identity = ManagedIdentity.Empty + KeyVaultReferenceIdentity = None + Tags = Map.empty + Dependencies = Set.empty + IpSecurityRestrictions = [] + } [] member this.Name(state, name) : SlotConfig = { state with Name = name } [] - member this.AutoSlotSwapName(state, autoSlotSwapName) : SlotConfig = - { state with + member this.AutoSlotSwapName(state, autoSlotSwapName) : SlotConfig = { + state with AutoSwapSlotName = Some autoSlotSwapName - } + } /// Sets an app setting of the web app in the form "key" "value". [] - member this.AddIdentity(state: SlotConfig, identity: UserAssignedIdentity) = - { state with + member this.AddIdentity(state: SlotConfig, identity: UserAssignedIdentity) = { + state with Identity = (state.Identity + identity) AppSettings = state.AppSettings.Add("AZURE_CLIENT_ID", Setting.ExpressionSetting identity.ClientId) - } + } member this.AddIdentity(state, identity: UserAssignedIdentityConfig) = this.AddIdentity(state, identity.UserAssignedIdentity) [] - member this.SystemIdentity(state: SlotConfig) = - { state with - Identity = - { state.Identity with + member this.SystemIdentity(state: SlotConfig) = { + state with + Identity = { + state.Identity with SystemAssigned = Enabled - } - } + } + } [] - member this.AddKeyVaultIdentity(state: SlotConfig, identity: UserAssignedIdentity) = - { state with + member this.AddKeyVaultIdentity(state: SlotConfig, identity: UserAssignedIdentity) = { + state with Identity = state.Identity + identity KeyVaultReferenceIdentity = Some identity AppSettings = state.AppSettings.Add("AZURE_CLIENT_ID", Setting.ExpressionSetting identity.ClientId) - } + } member this.AddKeyVaultIdentity(state: SlotConfig, identity: UserAssignedIdentityConfig) = this.AddKeyVaultIdentity(state, identity.UserAssignedIdentity) [] /// Adds an AppSetting to this deployment slot - member this.AddSetting(state, key, value) : SlotConfig = - { state with + member this.AddSetting(state, key, value) : SlotConfig = { + state with AppSettings = state.AppSettings.Add(key, value) - } + } member this.AddSetting(state, key, value) = this.AddSetting(state, key, LiteralSetting value) @@ -202,25 +200,25 @@ type SlotBuilder() = /// Sets a list of app setting of the web app in the form "key" "value". [] - member this.AddSettings(state, settings: (string * Setting) list) : SlotConfig = - { state with + member this.AddSettings(state, settings: (string * Setting) list) : SlotConfig = { + state with AppSettings = Map.merge settings state.AppSettings - } + } member this.AddSettings(state, settings: (string * string) list) = this.AddSettings(state, List.map (fun (k, v) -> k, LiteralSetting v) settings) /// Creates a set of connection strings of the web app whose values will be supplied as secret parameters. [] - member _.AddConnectionString(state, key) : SlotConfig = - { state with + member _.AddConnectionString(state, key) : SlotConfig = { + state with ConnectionStrings = state.ConnectionStrings.Add(key, (ParameterSetting(SecureParameter key), Custom)) - } + } - member _.AddConnectionString(state, (key, value: ArmExpression)) : SlotConfig = - { state with + member _.AddConnectionString(state, (key, value: ArmExpression)) : SlotConfig = { + 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. [] @@ -230,18 +228,18 @@ type SlotBuilder() = /// Specifies a docker image to use from the registry (linux only), and the startup command to execute. [] - member _.DockerImage(state, registryPath, startupFile) : SlotConfig = - { state with + member _.DockerImage(state, registryPath, startupFile) : SlotConfig = { + state with DockerRegistryPath = Some registryPath StartupCommand = Some startupFile - } + } /// Add Allowed ip for ip security restrictions [] - member _.AllowIp(state, name, cidr: IPAddressCidr) : SlotConfig = - { state with + 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 } @@ -253,10 +251,10 @@ type SlotBuilder() = /// Add Denied ip for ip security restrictions [] - member _.DenyIp(state, name, cidr: IPAddressCidr) : SlotConfig = - { state with + 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 } @@ -267,33 +265,31 @@ type SlotBuilder() = this.DenyIp(state, name, cidr) interface ITaggable with - member _.Add state tags = - { state with + member _.Add state tags = { + state with Tags = state.Tags |> Map.merge tags - } + } interface IDependable with - member _.Add state newDeps = - { state with + member _.Add state newDeps = { + state with Dependencies = state.Dependencies + newDeps - } + } let appSlot = SlotBuilder() -type VirtualApplicationConfig = - { - VirtualPath: string - PhysicalPath: string - PreloadEnabled: bool option - } +type VirtualApplicationConfig = { + VirtualPath: string + PhysicalPath: string + PreloadEnabled: bool option +} type VirtualApplicationBuilder() = - member this.Yield _ = - { - VirtualPath = "" - PhysicalPath = "" - PreloadEnabled = None - } + member this.Yield _ = { + VirtualPath = "" + PhysicalPath = "" + PreloadEnabled = None + } member _.Run(config: VirtualApplicationConfig) = if String.IsNullOrWhiteSpace config.VirtualPath then @@ -307,48 +303,46 @@ type VirtualApplicationBuilder() = config [] - member _.VirtualPath(state, virtualPath) : VirtualApplicationConfig = - { state with VirtualPath = virtualPath } + member _.VirtualPath(state, virtualPath) : VirtualApplicationConfig = { state with VirtualPath = virtualPath } [] - member _.PhysicalPath(state, physicalPath) : VirtualApplicationConfig = - { state with + member _.PhysicalPath(state, physicalPath) : VirtualApplicationConfig = { + state with PhysicalPath = physicalPath - } + } [] - member _.Preloaded state : VirtualApplicationConfig = - { state with + member _.Preloaded state : VirtualApplicationConfig = { + state with PreloadEnabled = Some true - } + } let virtualApplication = VirtualApplicationBuilder() /// Common fields between WebApp and Functions -type CommonWebConfig = - { - Name: WebAppName - AlwaysOn: bool - AppInsights: ResourceRef option - ConnectionStrings: Map - Cors: Cors option - FTPState: FTPState option - HTTPSOnly: bool - Identity: Identity.ManagedIdentity - KeyVaultReferenceIdentity: UserAssignedIdentity Option - OperatingSystem: OS - SecretStore: SecretStore - ServicePlan: ResourceRef - Settings: Map - Sku: Sku - Slots: Map - WorkerProcess: Bitness option - ZipDeployPath: (string * ZipDeploy.ZipDeploySlot) option - HealthCheckPath: string option - IpSecurityRestrictions: IpSecurityRestriction list - IntegratedSubnet: SubnetReference option - PrivateEndpoints: (SubnetReference * string option) Set - } +type CommonWebConfig = { + Name: WebAppName + AlwaysOn: bool + AppInsights: ResourceRef option + ConnectionStrings: Map + Cors: Cors option + FTPState: FTPState option + HTTPSOnly: bool + Identity: Identity.ManagedIdentity + KeyVaultReferenceIdentity: UserAssignedIdentity Option + OperatingSystem: OS + SecretStore: SecretStore + ServicePlan: ResourceRef + Settings: Map + Sku: Sku + Slots: Map + WorkerProcess: Bitness option + ZipDeployPath: (string * ZipDeploy.ZipDeploySlot) option + HealthCheckPath: string option + IpSecurityRestrictions: IpSecurityRestriction list + IntegratedSubnet: SubnetReference option + PrivateEndpoints: (SubnetReference * string option) Set +} with member this.Validate() = match this with @@ -372,36 +366,41 @@ type CommonWebConfig = 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 - Dependencies: ResourceId Set - Tags: Map - WorkerSize: WorkerSize - WorkerCount: int - MaximumElasticWorkerCount: int option - RunFromPackage: bool - WebsiteNodeDefaultVersion: string option - Runtime: Runtime - SourceControlSettings: {| Repository: Uri - Branch: string - ContinuousIntegration: FeatureFlag |} option - DockerRegistryPath: string option - StartupCommand: string option - DockerCi: bool - DockerAcrCredentials: {| RegistryName: string - Password: SecureParameter |} option - AutomaticLoggingExtension: bool - SiteExtensions: ExtensionName Set - PrivateEndpoints: (LinkedResource * string option) Set - CustomDomains: Map - DockerPort: int option - ZoneRedundant: FeatureFlag option - VirtualApplications: Map - } +type WebAppConfig = { + CommonWebConfig: CommonWebConfig + HTTP20Enabled: bool option + ClientAffinityEnabled: bool option + WebSocketsEnabled: bool option + Dependencies: ResourceId Set + Tags: Map + WorkerSize: WorkerSize + WorkerCount: int + MaximumElasticWorkerCount: int option + RunFromPackage: bool + WebsiteNodeDefaultVersion: string option + Runtime: Runtime + SourceControlSettings: + {| + Repository: Uri + Branch: string + ContinuousIntegration: FeatureFlag + |} option + DockerRegistryPath: string option + StartupCommand: string option + DockerCi: bool + DockerAcrCredentials: + {| + RegistryName: string + Password: SecureParameter + |} option + AutomaticLoggingExtension: bool + SiteExtensions: ExtensionName Set + PrivateEndpoints: (LinkedResource * string option) Set + CustomDomains: Map + DockerPort: int option + ZoneRedundant: FeatureFlag option + VirtualApplications: Map +} with member this.Name = this.CommonWebConfig.Name @@ -426,479 +425,459 @@ type WebAppConfig = interface IBuilder with member this.ResourceId = this.ResourceId - member this.BuildResources location = - [ - let keyVault, secrets = - match this.CommonWebConfig.SecretStore with - | KeyVault (DeployableResource (this.CommonWebConfig) vaultName) -> - let store = - keyVault { - name vaultName.Name - - add_access_policy ( - AccessPolicy.create (this.SystemIdentity.PrincipalId, [ KeyVault.Secret.Get ]) - ) - - add_secrets - [ - for setting in this.CommonWebConfig.Settings do - match setting.Value with - | LiteralSetting _ -> () - | ParameterSetting _ -> SecretConfig.create (setting.Key) - | ExpressionSetting expr -> SecretConfig.create (setting.Key, expr) - ] - } - - Some store, [] - | KeyVault (ExternalResource vaultName) -> - let secrets = - [ - for setting in this.CommonWebConfig.Settings do - let secret = - match setting.Value with - | LiteralSetting _ -> None - | ParameterSetting _ -> SecretConfig.create setting.Key |> Some - | ExpressionSetting expr -> SecretConfig.create (setting.Key, expr) |> Some - - match secret with - | Some secret -> - { - Secret.Name = vaultName.Name / secret.SecretName - Value = secret.Value - ContentType = secret.ContentType - Enabled = secret.Enabled - ActivationDate = secret.ActivationDate - ExpirationDate = secret.ExpirationDate - Location = location - Dependencies = secret.Dependencies.Add vaultName - Tags = secret.Tags - } - :> IArmResource - | None -> () - ] - - None, secrets - | KeyVault _ - | AppService -> None, [] - - yield! secrets - - let siteSettings = - let literalSettings = - [ - if this.RunFromPackage then - AppSettings.RunFromPackage - yield! - this.WebsiteNodeDefaultVersion - |> Option.mapList AppSettings.WebsiteNodeDefaultVersion - yield! - this.CommonWebConfig.AppInsights - |> Option.mapList (fun resource -> - "APPINSIGHTS_INSTRUMENTATIONKEY", - AppInsights - .getInstrumentationKey(resource.resourceId this.Name.ResourceName) - .Eval()) - match this.CommonWebConfig.OperatingSystem, this.CommonWebConfig.AppInsights with - | Windows, Some _ -> - "APPINSIGHTS_PROFILERFEATURE_VERSION", "1.0.0" - "APPINSIGHTS_SNAPSHOTFEATURE_VERSION", "1.0.0" - "ApplicationInsightsAgent_EXTENSION_VERSION", "~2" - "DiagnosticServices_EXTENSION_VERSION", "~3" - "InstrumentationEngine_EXTENSION_VERSION", "~1" - "SnapshotDebugger_EXTENSION_VERSION", "~1" - "XDT_MicrosoftApplicationInsights_BaseExtensions", "~1" - "XDT_MicrosoftApplicationInsights_Mode", "recommended" - | Linux, Some _ - | _, None -> () - - yield! this.DockerPort |> Option.mapList AppSettings.WebsitesPort - - if this.DockerCi then - "DOCKER_ENABLE_CI", "true" + member this.BuildResources location = [ + let keyVault, secrets = + match this.CommonWebConfig.SecretStore with + | KeyVault(DeployableResource (this.CommonWebConfig) vaultName) -> + let store = keyVault { + name vaultName.Name + + add_access_policy ( + AccessPolicy.create (this.SystemIdentity.PrincipalId, [ KeyVault.Secret.Get ]) + ) + + add_secrets [ + for setting in this.CommonWebConfig.Settings do + match setting.Value with + | LiteralSetting _ -> () + | ParameterSetting _ -> SecretConfig.create (setting.Key) + | ExpressionSetting expr -> SecretConfig.create (setting.Key, expr) ] + } - let dockerSettings = - [ - match this.DockerAcrCredentials with - | Some credentials -> - "DOCKER_REGISTRY_SERVER_PASSWORD", ParameterSetting credentials.Password - - Setting.AsLiteral( - "DOCKER_REGISTRY_SERVER_URL", - $"https://{credentials.RegistryName}.azurecr.io" - ) - - Setting.AsLiteral("DOCKER_REGISTRY_SERVER_USERNAME", credentials.RegistryName) + Some store, [] + | KeyVault(ExternalResource vaultName) -> + let secrets = [ + for setting in this.CommonWebConfig.Settings do + let secret = + match setting.Value with + | LiteralSetting _ -> None + | ParameterSetting _ -> SecretConfig.create setting.Key |> Some + | ExpressionSetting expr -> SecretConfig.create (setting.Key, expr) |> Some + + match secret with + | Some secret -> + { + Secret.Name = vaultName.Name / secret.SecretName + Value = secret.Value + ContentType = secret.ContentType + Enabled = secret.Enabled + ActivationDate = secret.ActivationDate + ExpirationDate = secret.ExpirationDate + Location = location + Dependencies = secret.Dependencies.Add vaultName + Tags = secret.Tags + } + :> IArmResource | None -> () - ] + ] - literalSettings - |> List.map Setting.AsLiteral - |> List.append dockerSettings - |> List.append ( - (match this.CommonWebConfig.SecretStore with - | AppService -> this.CommonWebConfig.Settings - | KeyVault r -> - let name = r.resourceId (this.CommonWebConfig) - - [ - for setting in this.CommonWebConfig.Settings do - match setting.Value with - | LiteralSetting _ -> setting.Key, setting.Value - | ParameterSetting _ - | ExpressionSetting _ -> - setting.Key, - LiteralSetting - $"@Microsoft.KeyVault(SecretUri=https://{name.Name.Value}.vault.azure.net/secrets/{setting.Key})" - ] - |> Map.ofList) - |> Map.toList - ) - |> Map - - let site = - { - SiteType = Site this.Name - Location = location - ServicePlan = this.ServicePlanId - HTTPSOnly = this.CommonWebConfig.HTTPSOnly - FTPState = this.CommonWebConfig.FTPState - HTTP20Enabled = this.HTTP20Enabled - ClientAffinityEnabled = this.ClientAffinityEnabled - WebSocketsEnabled = this.WebSocketsEnabled - Identity = this.CommonWebConfig.Identity - KeyVaultReferenceIdentity = this.CommonWebConfig.KeyVaultReferenceIdentity - Cors = this.CommonWebConfig.Cors - Tags = this.Tags - ConnectionStrings = Some this.CommonWebConfig.ConnectionStrings - WorkerProcess = this.CommonWebConfig.WorkerProcess - AppSettings = Some siteSettings - Kind = - [ - "app" - match this.CommonWebConfig.OperatingSystem with - | Linux -> "linux" - | Windows -> () - if this.DockerRegistryPath.IsSome then - "container" - ] - |> String.concat "," - Dependencies = - Set - [ - match this.CommonWebConfig.ServicePlan with - | DependableResource this.Name.ResourceName resourceId -> resourceId - | _ -> () - - yield! this.Dependencies - - match this.CommonWebConfig.SecretStore with - | AppService -> - for setting in this.CommonWebConfig.Settings do - match setting.Value with - | ExpressionSetting expr -> yield! Option.toList expr.Owner - | ParameterSetting _ - | LiteralSetting _ -> () - | KeyVault _ -> () - - match this.CommonWebConfig.AppInsights with - | Some (DependableResource this.Name.ResourceName resourceId) -> resourceId - | Some _ - | None -> () - ] - AlwaysOn = this.CommonWebConfig.AlwaysOn - LinuxFxVersion = - match this.CommonWebConfig.OperatingSystem with - | Windows -> None - | Linux -> - match this.DockerRegistryPath with - | Some image -> Some("DOCKER|" + image) - | None -> - match this.Runtime with - | DotNetCore version -> Some $"DOTNETCORE|{version}" - | DotNet version -> Some $"DOTNETCORE|{version}" - | Node version -> Some $"NODE|{version}" - | Php version -> Some $"PHP|{version}" - | Ruby version -> Some $"RUBY|{version}" - | Java (runtime, JavaSE) -> Some $"JAVA|{runtime.Version}-{runtime.Jre}" - | Java (runtime, (Tomcat version)) -> Some $"TOMCAT|{version}-{runtime.Jre}" - | Java (Java8, WildFly14) -> Some $"WILDFLY|14-{Java8.Jre}" - | Python (linuxVersion, _) -> Some $"PYTHON|{linuxVersion}" - | _ -> None - NetFrameworkVersion = + None, secrets + | KeyVault _ + | AppService -> None, [] + + yield! secrets + + let siteSettings = + let literalSettings = [ + if this.RunFromPackage then + AppSettings.RunFromPackage + yield! + this.WebsiteNodeDefaultVersion + |> Option.mapList AppSettings.WebsiteNodeDefaultVersion + yield! + this.CommonWebConfig.AppInsights + |> Option.mapList (fun resource -> + "APPINSIGHTS_INSTRUMENTATIONKEY", + AppInsights + .getInstrumentationKey(resource.resourceId this.Name.ResourceName) + .Eval()) + match this.CommonWebConfig.OperatingSystem, this.CommonWebConfig.AppInsights with + | Windows, Some _ -> + "APPINSIGHTS_PROFILERFEATURE_VERSION", "1.0.0" + "APPINSIGHTS_SNAPSHOTFEATURE_VERSION", "1.0.0" + "ApplicationInsightsAgent_EXTENSION_VERSION", "~2" + "DiagnosticServices_EXTENSION_VERSION", "~3" + "InstrumentationEngine_EXTENSION_VERSION", "~1" + "SnapshotDebugger_EXTENSION_VERSION", "~1" + "XDT_MicrosoftApplicationInsights_BaseExtensions", "~1" + "XDT_MicrosoftApplicationInsights_Mode", "recommended" + | Linux, Some _ + | _, None -> () + + yield! this.DockerPort |> Option.mapList AppSettings.WebsitesPort + + if this.DockerCi then + "DOCKER_ENABLE_CI", "true" + ] + + let dockerSettings = [ + match this.DockerAcrCredentials with + | Some credentials -> + "DOCKER_REGISTRY_SERVER_PASSWORD", ParameterSetting credentials.Password + + Setting.AsLiteral( + "DOCKER_REGISTRY_SERVER_URL", + $"https://{credentials.RegistryName}.azurecr.io" + ) + + Setting.AsLiteral("DOCKER_REGISTRY_SERVER_USERNAME", credentials.RegistryName) + | None -> () + ] + + literalSettings + |> List.map Setting.AsLiteral + |> List.append dockerSettings + |> List.append ( + (match this.CommonWebConfig.SecretStore with + | AppService -> this.CommonWebConfig.Settings + | KeyVault r -> + let name = r.resourceId (this.CommonWebConfig) + + [ + for setting in this.CommonWebConfig.Settings do + match setting.Value with + | LiteralSetting _ -> setting.Key, setting.Value + | ParameterSetting _ + | ExpressionSetting _ -> + setting.Key, + LiteralSetting + $"@Microsoft.KeyVault(SecretUri=https://{name.Name.Value}.vault.azure.net/secrets/{setting.Key})" + ] + |> Map.ofList) + |> Map.toList + ) + |> Map + + let site = { + SiteType = Site this.Name + Location = location + ServicePlan = this.ServicePlanId + HTTPSOnly = this.CommonWebConfig.HTTPSOnly + FTPState = this.CommonWebConfig.FTPState + HTTP20Enabled = this.HTTP20Enabled + ClientAffinityEnabled = this.ClientAffinityEnabled + WebSocketsEnabled = this.WebSocketsEnabled + Identity = this.CommonWebConfig.Identity + KeyVaultReferenceIdentity = this.CommonWebConfig.KeyVaultReferenceIdentity + Cors = this.CommonWebConfig.Cors + Tags = this.Tags + ConnectionStrings = Some this.CommonWebConfig.ConnectionStrings + WorkerProcess = this.CommonWebConfig.WorkerProcess + AppSettings = Some siteSettings + Kind = + [ + "app" + match this.CommonWebConfig.OperatingSystem with + | Linux -> "linux" + | Windows -> () + if this.DockerRegistryPath.IsSome then + "container" + ] + |> String.concat "," + Dependencies = + Set [ + match this.CommonWebConfig.ServicePlan with + | DependableResource this.Name.ResourceName resourceId -> resourceId + | _ -> () + + yield! this.Dependencies + + match this.CommonWebConfig.SecretStore with + | AppService -> + for setting in this.CommonWebConfig.Settings do + match setting.Value with + | ExpressionSetting expr -> yield! Option.toList expr.Owner + | ParameterSetting _ + | LiteralSetting _ -> () + | KeyVault _ -> () + + match this.CommonWebConfig.AppInsights with + | Some(DependableResource this.Name.ResourceName resourceId) -> resourceId + | Some _ + | None -> () + ] + AlwaysOn = this.CommonWebConfig.AlwaysOn + LinuxFxVersion = + match this.CommonWebConfig.OperatingSystem with + | Windows -> None + | Linux -> + match this.DockerRegistryPath with + | Some image -> Some("DOCKER|" + image) + | None -> match this.Runtime with - | AspNet version - | DotNet ("5.0" as version) - | DotNet version -> Some $"v{version}" - | _ -> None - JavaVersion = - match this.Runtime, this.CommonWebConfig.OperatingSystem with - | Java (Java11, Tomcat _), Windows -> Some "11" - | Java (Java8, Tomcat _), Windows -> Some "1.8" + | DotNetCore version -> Some $"DOTNETCORE|{version}" + | DotNet version -> Some $"DOTNETCORE|{version}" + | Node version -> Some $"NODE|{version}" + | Php version -> Some $"PHP|{version}" + | Ruby version -> Some $"RUBY|{version}" + | Java(runtime, JavaSE) -> Some $"JAVA|{runtime.Version}-{runtime.Jre}" + | Java(runtime, (Tomcat version)) -> Some $"TOMCAT|{version}-{runtime.Jre}" + | Java(Java8, WildFly14) -> Some $"WILDFLY|14-{Java8.Jre}" + | Python(linuxVersion, _) -> Some $"PYTHON|{linuxVersion}" | _ -> None - JavaContainer = - match this.Runtime, this.CommonWebConfig.OperatingSystem with - | Java (_, Tomcat _), Windows -> Some "Tomcat" - | _ -> None - JavaContainerVersion = - match this.Runtime, this.CommonWebConfig.OperatingSystem with - | Java (_, Tomcat version), Windows -> Some version - | _ -> None - PhpVersion = - match this.Runtime, this.CommonWebConfig.OperatingSystem with - | Php version, Windows -> Some version - | _ -> None - PythonVersion = - match this.Runtime, this.CommonWebConfig.OperatingSystem with - | Python (_, windowsVersion), Windows -> Some windowsVersion - | _ -> None - Metadata = - match this.Runtime, this.CommonWebConfig.OperatingSystem with - | Java (_, Tomcat _), Windows -> Some "java" - | Php _, _ -> Some "php" - | Python _, Windows -> Some "python" - | DotNetCore _, Windows -> Some "dotnetcore" - | AspNet _, _ - | DotNet _, Windows -> Some "dotnet" - | _ -> None - |> Option.map (fun stack -> "CURRENT_STACK", stack) - |> Option.toList - AppCommandLine = this.StartupCommand - 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 - VirtualApplications = this.VirtualApplications - } - - match keyVault with - | Some keyVault -> - let builder = keyVault :> IBuilder - yield! builder.BuildResources location - | None -> () + NetFrameworkVersion = + match this.Runtime with + | AspNet version + | DotNet("5.0" as version) + | DotNet version -> Some $"v{version}" + | _ -> None + JavaVersion = + match this.Runtime, this.CommonWebConfig.OperatingSystem with + | Java(Java11, Tomcat _), Windows -> Some "11" + | Java(Java8, Tomcat _), Windows -> Some "1.8" + | _ -> None + JavaContainer = + match this.Runtime, this.CommonWebConfig.OperatingSystem with + | Java(_, Tomcat _), Windows -> Some "Tomcat" + | _ -> None + JavaContainerVersion = + match this.Runtime, this.CommonWebConfig.OperatingSystem with + | Java(_, Tomcat version), Windows -> Some version + | _ -> None + PhpVersion = + match this.Runtime, this.CommonWebConfig.OperatingSystem with + | Php version, Windows -> Some version + | _ -> None + PythonVersion = + match this.Runtime, this.CommonWebConfig.OperatingSystem with + | Python(_, windowsVersion), Windows -> Some windowsVersion + | _ -> None + Metadata = + match this.Runtime, this.CommonWebConfig.OperatingSystem with + | Java(_, Tomcat _), Windows -> Some "java" + | Php _, _ -> Some "php" + | Python _, Windows -> Some "python" + | DotNetCore _, Windows -> Some "dotnetcore" + | AspNet _, _ + | DotNet _, Windows -> Some "dotnet" + | _ -> None + |> Option.map (fun stack -> "CURRENT_STACK", stack) + |> Option.toList + AppCommandLine = this.StartupCommand + 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 + VirtualApplications = this.VirtualApplications + } - match this.SourceControlSettings with - | Some settings -> - { - Website = this.Name.ResourceName - Location = location - Repository = settings.Repository - Branch = settings.Branch - ContinuousIntegration = settings.ContinuousIntegration - } - | None -> () + match keyVault with + | Some keyVault -> + let builder = keyVault :> IBuilder + yield! builder.BuildResources location + | None -> () - match this.CommonWebConfig.AppInsights with - | Some (DeployableResource this.Name.ResourceName resourceId) -> - { - Name = resourceId.Name - Location = location - DisableIpMasking = false - SamplingPercentage = 100 - InstanceKind = Classic - Dependencies = Set.empty - LinkedWebsite = - match this.CommonWebConfig.OperatingSystem with - | Windows -> Some this.Name.ResourceName - | Linux -> None - Tags = this.Tags - } - | Some _ - | None -> () + match this.SourceControlSettings with + | Some settings -> { + Website = this.Name.ResourceName + Location = location + Repository = settings.Repository + Branch = settings.Branch + ContinuousIntegration = settings.ContinuousIntegration + } + | None -> () - match this.CommonWebConfig.ServicePlan with - | DeployableResource this.Name.ResourceName resourceId -> - { - Name = resourceId.Name - Location = location - Sku = this.CommonWebConfig.Sku - WorkerSize = this.WorkerSize - WorkerCount = this.WorkerCount - MaximumElasticWorkerCount = this.MaximumElasticWorkerCount - OperatingSystem = this.CommonWebConfig.OperatingSystem - ZoneRedundant = this.ZoneRedundant - Tags = this.Tags - } - | _ -> () + match this.CommonWebConfig.AppInsights with + | Some(DeployableResource this.Name.ResourceName resourceId) -> { + Name = resourceId.Name + Location = location + DisableIpMasking = false + SamplingPercentage = 100 + InstanceKind = Classic + Dependencies = Set.empty + LinkedWebsite = + match this.CommonWebConfig.OperatingSystem with + | Windows -> Some this.Name.ResourceName + | Linux -> None + Tags = this.Tags + } + | Some _ + | None -> () - for (ExtensionName extension) in this.SiteExtensions do - { - Name = ResourceName extension - SiteName = this.Name.ResourceName - Location = location - } + match this.CommonWebConfig.ServicePlan with + | DeployableResource this.Name.ResourceName resourceId -> { + Name = resourceId.Name + Location = location + Sku = this.CommonWebConfig.Sku + WorkerSize = this.WorkerSize + WorkerCount = this.WorkerCount + MaximumElasticWorkerCount = this.MaximumElasticWorkerCount + OperatingSystem = this.CommonWebConfig.OperatingSystem + ZoneRedundant = this.ZoneRedundant + Tags = this.Tags + } + | _ -> () + + for (ExtensionName extension) in this.SiteExtensions do + { + Name = ResourceName extension + SiteName = this.Name.ResourceName + Location = location + } - if Map.isEmpty this.CommonWebConfig.Slots then - site - else - { site with + if Map.isEmpty this.CommonWebConfig.Slots then + site + else + { + site with AppSettings = None ConnectionStrings = None - } // Don't deploy production slot settings as they could cause an app restart - - for (_, slot) in this.CommonWebConfig.Slots |> Map.toSeq do - slot.ToSite site - - // Need to rename `location` binding to prevent conflict with `location` operator in resource group - let resourceLocation = location - - // 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 deployment. - let mutable previousHostNameCertificateLinkingDeployment = None - - for customDomain in this.CustomDomains |> Map.toSeq |> Seq.map snd do - let hostNameBinding = - { - Location = location - SiteId = Managed(Arm.Web.sites.resourceId this.Name.ResourceName) - DomainName = customDomain.DomainName - SslState = SslDisabled - } // Initially create non-secure host name binding, we link the certificate in a nested deployment below - - let dependsOn: ResourceId list = - match previousHostNameCertificateLinkingDeployment with - | Some previous -> [ previous; this.ResourceId ] - | None -> [ this.ResourceId ] - - let hostNameBindingDeployment = - resourceGroup { - name "[resourceGroup().name]" - location resourceLocation - add_resource hostNameBinding - depends_on dependsOn - } + } // Don't deploy production slot settings as they could cause an app restart + + for (_, slot) in this.CommonWebConfig.Slots |> Map.toSeq do + slot.ToSite site + + // Need to rename `location` binding to prevent conflict with `location` operator in resource group + let resourceLocation = location + + // 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 deployment. + let mutable previousHostNameCertificateLinkingDeployment = None + + for customDomain in this.CustomDomains |> Map.toSeq |> Seq.map snd do + let hostNameBinding = { + Location = location + SiteId = Managed(Arm.Web.sites.resourceId this.Name.ResourceName) + DomainName = customDomain.DomainName + SslState = SslDisabled + } // Initially create non-secure host name binding, we link the certificate in a nested deployment below + + let dependsOn: ResourceId list = + match previousHostNameCertificateLinkingDeployment with + | Some previous -> [ previous; this.ResourceId ] + | None -> [ this.ResourceId ] + + let hostNameBindingDeployment = resourceGroup { + name "[resourceGroup().name]" + location resourceLocation + add_resource hostNameBinding + depends_on dependsOn + } - yield! ((hostNameBindingDeployment :> IBuilder).BuildResources location) + yield! ((hostNameBindingDeployment :> IBuilder).BuildResources location) - match customDomain with - | SecureDomain (customDomain, certOptions) -> - let cert = - { - Location = location - SiteId = Managed this.ResourceId - ServicePlanId = Managed this.ServicePlanId - DomainName = customDomain - } + match customDomain with + | SecureDomain(customDomain, certOptions) -> + let cert = { + Location = location + SiteId = Managed this.ResourceId + ServicePlanId = Managed this.ServicePlanId + DomainName = customDomain + } - // Get the resource group which contains the app service plan - let aspRgName = - match this.CommonWebConfig.ServicePlan with - | LinkedResource linked -> linked.ResourceId.ResourceGroup - | _ -> None + // 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 certificateDeployment = resourceGroup { + name (aspRgName |> Option.defaultValue "[resourceGroup().name]") + + add_resource { + cert with + SiteId = Unmanaged cert.SiteId.ResourceId + ServicePlanId = Unmanaged cert.ServicePlanId.ResourceId + } - // 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 certificateDeployment = - 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 hostNameBindingDeployment.ResourceId - } + depends_on cert.SiteId + depends_on hostNameBindingDeployment.ResourceId + } - yield! ((certificateDeployment :> IBuilder).BuildResources location) - - // Deployment to update hostname binding with specified SSL options - let hostNameCertificateLinkingDeployment = - 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 certificateDeployment.ResourceId - } + yield! ((certificateDeployment :> IBuilder).BuildResources location) + + // Deployment to update hostname binding with specified SSL options + let hostNameCertificateLinkingDeployment = 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 + } - yield! ((hostNameCertificateLinkingDeployment :> IBuilder).BuildResources location) + depends_on certificateDeployment.ResourceId + } - previousHostNameCertificateLinkingDeployment <- - Some hostNameCertificateLinkingDeployment.ResourceId - | _ -> () + yield! ((hostNameCertificateLinkingDeployment :> IBuilder).BuildResources location) - match this.CommonWebConfig.IntegratedSubnet with - | None -> () - | Some subnetRef -> - { - Site = site - Subnet = subnetRef.ResourceId - Dependencies = subnetRef.Dependency |> Option.toList - } + previousHostNameCertificateLinkingDeployment <- Some hostNameCertificateLinkingDeployment.ResourceId + | _ -> () + + 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) - ] + yield! (PrivateEndpoint.create location this.ResourceId [ "sites" ] this.CommonWebConfig.PrivateEndpoints) + ] type WebAppBuilder() = - member _.Yield _ = - { - CommonWebConfig = - { - Name = WebAppName.Empty - AlwaysOn = false - AppInsights = Some(derived (fun name -> components.resourceId (name - "ai"))) - ConnectionStrings = Map.empty - Cors = None - HTTPSOnly = false - Identity = ManagedIdentity.Empty - FTPState = None - KeyVaultReferenceIdentity = None - OperatingSystem = Windows - 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 - IpSecurityRestrictions = [] - IntegratedSubnet = None - PrivateEndpoints = Set.empty - } - WorkerSize = Small - WorkerCount = 1 - MaximumElasticWorkerCount = None - RunFromPackage = false - WebsiteNodeDefaultVersion = None - HTTP20Enabled = None - ClientAffinityEnabled = None - WebSocketsEnabled = None - Tags = Map.empty - Dependencies = Set.empty - Runtime = Runtime.DotNetCoreLts - DockerRegistryPath = None - StartupCommand = None - DockerCi = false - SourceControlSettings = None - DockerAcrCredentials = None - AutomaticLoggingExtension = true - SiteExtensions = Set.empty + member _.Yield _ = { + CommonWebConfig = { + Name = WebAppName.Empty + AlwaysOn = false + AppInsights = Some(derived (fun name -> components.resourceId (name - "ai"))) + ConnectionStrings = Map.empty + Cors = None + HTTPSOnly = false + Identity = ManagedIdentity.Empty + FTPState = None + KeyVaultReferenceIdentity = None + OperatingSystem = Windows + 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 + IpSecurityRestrictions = [] + IntegratedSubnet = None PrivateEndpoints = Set.empty - CustomDomains = Map.empty - DockerPort = None - ZoneRedundant = None - VirtualApplications = Map [] } + WorkerSize = Small + WorkerCount = 1 + MaximumElasticWorkerCount = None + RunFromPackage = false + WebsiteNodeDefaultVersion = None + HTTP20Enabled = None + ClientAffinityEnabled = None + WebSocketsEnabled = None + Tags = Map.empty + Dependencies = Set.empty + Runtime = Runtime.DotNetCoreLts + DockerRegistryPath = None + StartupCommand = None + DockerCi = false + SourceControlSettings = None + DockerAcrCredentials = None + AutomaticLoggingExtension = true + SiteExtensions = Set.empty + PrivateEndpoints = Set.empty + CustomDomains = Map.empty + DockerPort = None + ZoneRedundant = None + VirtualApplications = Map [] + } member _.Run(state: WebAppConfig) = if state.Name.ResourceName = ResourceName.Empty then @@ -906,32 +885,33 @@ type WebAppBuilder() = state.CommonWebConfig.Validate() - { state with - SiteExtensions = - match state with - // it is 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 = DotNetCore _ - AutomaticLoggingExtension = true - DockerRegistryPath = None - CommonWebConfig = { OperatingSystem = Windows } - } -> state.SiteExtensions.Add Extensions.Logging - | _ -> state.SiteExtensions - DockerRegistryPath = - match state.DockerRegistryPath, state.DockerAcrCredentials with - | Some image, Some credentials when not (image.Contains "azurecr.io") -> - Some $"{credentials.RegistryName}.azurecr.io/{image}" - | Some registryPath, _ -> Some registryPath - | None, _ -> None + { + state with + SiteExtensions = + match state with + // it is 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 = DotNetCore _ + AutomaticLoggingExtension = true + DockerRegistryPath = None + CommonWebConfig = { OperatingSystem = Windows } + } -> state.SiteExtensions.Add Extensions.Logging + | _ -> state.SiteExtensions + DockerRegistryPath = + match state.DockerRegistryPath, state.DockerAcrCredentials with + | Some image, Some credentials when not (image.Contains "azurecr.io") -> + Some $"{credentials.RegistryName}.azurecr.io/{image}" + | Some registryPath, _ -> Some registryPath + | None, _ -> None } [] - member _.Sku(state: WebAppConfig, sku) = - { state with + member _.Sku(state: WebAppConfig, sku) = { + state with CommonWebConfig = { state.CommonWebConfig with Sku = sku } - } + } /// Sets the size of the service plan worker. [] @@ -939,8 +919,7 @@ type WebAppBuilder() = /// Sets the number of instances on the service plan. [] - member _.NumberOfWorkers(state: WebAppConfig, workerCount) = - { state with WorkerCount = workerCount } + member _.NumberOfWorkers(state: WebAppConfig, workerCount) = { state with WorkerCount = workerCount } /// Sets the web app to use "run from package" deployment capabilities. [] @@ -948,29 +927,28 @@ type WebAppBuilder() = /// Sets the node version of the web app. [] - member _.NodeVersion(state: WebAppConfig, version) = - { state with + member _.NodeVersion(state: WebAppConfig, version) = { + state with WebsiteNodeDefaultVersion = Some version - } + } /// Enables HTTP 2.0 for this webapp. [] - member _.Http20Enabled(state: WebAppConfig) = - { state with HTTP20Enabled = Some true } + member _.Http20Enabled(state: WebAppConfig) = { state with HTTP20Enabled = Some true } /// Disables client affinity for this webapp. [] - member _.ClientAffinityEnabled(state: WebAppConfig) = - { state with + member _.ClientAffinityEnabled(state: WebAppConfig) = { + state with ClientAffinityEnabled = Some false - } + } /// Enables websockets for this webapp. [] - member _.WebSockets(state: WebAppConfig) = - { state with + member _.WebSockets(state: WebAppConfig) = { + state with WebSocketsEnabled = Some true - } + } /// Sets the runtime stack [] @@ -978,15 +956,15 @@ type WebAppBuilder() = /// Specifies a docker image to use from the registry (linux only), and the startup command to execute. [] - member _.DockerImage(state: WebAppConfig, registryPath, startupFile) = - { state with - CommonWebConfig = - { state.CommonWebConfig with + member _.DockerImage(state: WebAppConfig, registryPath, startupFile) = { + state with + CommonWebConfig = { + state.CommonWebConfig with OperatingSystem = Linux - } + } DockerRegistryPath = Some registryPath StartupCommand = Some startupFile - } + } /// Have your custom Docker image automatically re-deployed when a new version is pushed to e.g. Docker hub. [] @@ -994,44 +972,42 @@ type WebAppBuilder() = /// Supply a specific startup command - typically used when using "raw" app deployments to App Service Linux. [] - member _.StartupCommand(state: WebAppConfig, startupCommand) = - { state with + member _.StartupCommand(state: WebAppConfig, startupCommand) = { + state with StartupCommand = Some startupCommand - } + } /// Have your custom Docker image automatically re-deployed when a new version is pushed to e.g. Docker hub. [] - member _.DockerAcrCredentials(state: WebAppConfig, registryName) = - { state with + member _.DockerAcrCredentials(state: WebAppConfig, registryName) = { + state with DockerAcrCredentials = - Some - {| - RegistryName = registryName - Password = SecureParameter $"docker-password-for-{registryName}" - |} - } + Some {| + RegistryName = registryName + Password = SecureParameter $"docker-password-for-{registryName}" + |} + } [] - member _.SourceControl(state: WebAppConfig, url, branch) = - { state with + member _.SourceControl(state: WebAppConfig, url, branch) = { + state with SourceControlSettings = - Some - {| - Repository = Uri url - Branch = branch - ContinuousIntegration = Enabled - |} - } + Some {| + Repository = Uri url + Branch = branch + ContinuousIntegration = Enabled + |} + } - member _.SourceControlCi(state: WebAppConfig, featureFlag) = - { state with + member _.SourceControlCi(state: WebAppConfig, featureFlag) = { + state with SourceControlSettings = state.SourceControlSettings - |> Option.map (fun s -> - {| s with + |> Option.map (fun s -> {| + s with ContinuousIntegration = featureFlag - |}) - } + |}) + } [] member this.EnableCi(state: WebAppConfig) = this.SourceControlCi(state, Enabled) @@ -1040,26 +1016,26 @@ type WebAppBuilder() = member this.DisableCi(state: WebAppConfig) = this.SourceControlCi(state, Disabled) [] - member _.AddExtension(state: WebAppConfig, extension) = - { state with + member _.AddExtension(state: WebAppConfig, extension) = { + state with SiteExtensions = state.SiteExtensions.Add extension - } + } member this.AddExtension(state: WebAppConfig, name) = this.AddExtension(state, ExtensionName name) /// Automatically add the ASP.NET Core logging extension. [] - member _.DefaultLogging(state: WebAppConfig, setting) = - { state with + member _.DefaultLogging(state: WebAppConfig, setting) = { + state with AutomaticLoggingExtension = setting - } + } //Add Custom domain to you web app [] - member _.AddCustomDomain(state: WebAppConfig, domainConfig: DomainConfig) = - { state with + 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)) @@ -1082,64 +1058,63 @@ type WebAppBuilder() = /// Map specified port traffic from your docker container to port 80 for App Service [] - member _.DockerPort(state: WebAppConfig, dockerPort: int) = - { state with + 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 } + member this.ZoneRedundant(state: WebAppConfig, flag: FeatureFlag) = { state with ZoneRedundant = Some flag } [] member this.AddVirtualApplications(state: WebAppConfig, newVirtualApps) = let currentVirtualApps = if state.VirtualApplications.IsEmpty then - Map - [ - ("/", - { - PhysicalPath = "site\\wwwroot" - PreloadEnabled = None - }) - ] + Map [ + ("/", + { + PhysicalPath = "site\\wwwroot" + PreloadEnabled = None + }) + ] else state.VirtualApplications - { state with - VirtualApplications = - (currentVirtualApps, newVirtualApps) - ||> List.fold (fun map config -> - Map.add - config.VirtualPath - { - PhysicalPath = "site\\" + config.PhysicalPath - PreloadEnabled = config.PreloadEnabled - } - map) + { + state with + VirtualApplications = + (currentVirtualApps, newVirtualApps) + ||> List.fold (fun map config -> + Map.add + config.VirtualPath + { + PhysicalPath = "site\\" + config.PhysicalPath + PreloadEnabled = config.PreloadEnabled + } + map) } interface IPrivateEndpoints with - member _.Add state endpoints = - { state with - CommonWebConfig = - { state.CommonWebConfig 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 + member _.Add state tags = { + state with Tags = state.Tags |> Map.merge tags - } + } interface IDependable with - member _.Add state newDeps = - { state with + member _.Add state newDeps = { + state with Dependencies = state.Dependencies + newDeps - } + } interface IServicePlanApp with member _.Get state = state.CommonWebConfig @@ -1180,8 +1155,9 @@ module Extensions = /// Sets the name of the service plan. [] member this.SetServicePlanName(state: 'T, name) = - { this.Get state with - ServicePlan = named serverFarms name + { + this.Get state with + ServicePlan = named serverFarms name } |> this.Wrap state @@ -1192,14 +1168,16 @@ module Extensions = /// A dependency will automatically be set for this instance. [] member this.LinkToServicePlan(state: 'T, name) = - { this.Get state with - ServicePlan = managed serverFarms name + { + this.Get state with + ServicePlan = managed serverFarms name } |> this.Wrap state member this.LinkToServicePlan(state: 'T, servPlanApp: WebAppConfig) = - { this.Get state with - ServicePlan = managed serverFarms servPlanApp.ServicePlanName + { + this.Get state with + ServicePlan = managed serverFarms servPlanApp.ServicePlanName } |> this.Wrap state @@ -1213,16 +1191,18 @@ module Extensions = /// A dependency will automatically be set for this instance. [] member this.LinkToUnmanagedServicePlan(state: 'T, resourceId) = - { this.Get state with - ServicePlan = unmanaged resourceId + { + this.Get state with + ServicePlan = unmanaged resourceId } |> this.Wrap state /// Sets the name of the automatically-created app insights instance. [] member this.UseAppInsights(state: 'T, name) = - { this.Get state with - AppInsights = Some(named components name) + { + this.Get state with + AppInsights = Some(named components name) } |> this.Wrap state @@ -1232,8 +1212,9 @@ module Extensions = /// Removes any automatic app insights creation, configuration and settings for this webapp. [] member this.DeactivateAppInsights(state: 'T) = - { this.Get state with - AppInsights = None + { + this.Get state with + AppInsights = None } |> this.Wrap state @@ -1241,8 +1222,9 @@ module Extensions = /// A dependency will automatically be set for this instance. [] member this.LinkToAi(state: 'T, name) = - { this.Get state with - AppInsights = Some(managed components name) + { + this.Get state with + AppInsights = Some(managed components name) } |> this.Wrap state @@ -1259,8 +1241,9 @@ module Extensions = /// A dependency will not be set for this instance. [] member this.LinkUnmanagedAppInsights(state: 'T, resourceId) = - { this.Get state with - AppInsights = Some(unmanaged resourceId) + { + this.Get state with + AppInsights = Some(unmanaged resourceId) } |> this.Wrap state @@ -1269,8 +1252,9 @@ module Extensions = member this.AddSetting(state: 'T, key, value) = let current = this.Get state - { current with - Settings = current.Settings.Add(key, LiteralSetting value) + { + current with + Settings = current.Settings.Add(key, LiteralSetting value) } |> this.Wrap state @@ -1280,8 +1264,9 @@ module Extensions = member this.AddSetting(state: 'T, key, value: ArmExpression) = let current = this.Get state - { current with - Settings = current.Settings.Add(key, ExpressionSetting value) + { + current with + Settings = current.Settings.Add(key, ExpressionSetting value) } |> this.Wrap state @@ -1292,10 +1277,10 @@ module Extensions = settings |> List.fold - (fun (state: CommonWebConfig) (key, value: string) -> - { state with + (fun (state: CommonWebConfig) (key, value: string) -> { + state with Settings = state.Settings.Add(key, LiteralSetting value) - }) + }) current |> this.Wrap state @@ -1304,10 +1289,10 @@ module Extensions = settings |> List.fold - (fun (state: CommonWebConfig) (key, value: ArmExpression) -> - { state with + (fun (state: CommonWebConfig) (key, value: ArmExpression) -> { + state with Settings = state.Settings.Add(key, ExpressionSetting value) - }) + }) current |> this.Wrap state @@ -1316,8 +1301,10 @@ module Extensions = member this.AddConnectionString(state: 'T, key) = let current = this.Get state - { current with - ConnectionStrings = current.ConnectionStrings.Add(key, (ParameterSetting(SecureParameter key), Custom)) + { + current with + ConnectionStrings = + current.ConnectionStrings.Add(key, (ParameterSetting(SecureParameter key), Custom)) } |> this.Wrap state @@ -1327,8 +1314,9 @@ module Extensions = member this.AddConnectionString(state: 'T, (key, value: ArmExpression, kind)) = let current = this.Get state - { current with - ConnectionStrings = current.ConnectionStrings.Add(key, (ExpressionSetting value, kind)) + { + current with + ConnectionStrings = current.ConnectionStrings.Add(key, (ExpressionSetting value, kind)) } |> this.Wrap state @@ -1339,10 +1327,10 @@ module Extensions = connectionStrings |> List.fold - (fun (state: CommonWebConfig) (key, value: ArmExpression) -> - { state with + (fun (state: CommonWebConfig) (key, value: ArmExpression) -> { + state with ConnectionStrings = state.ConnectionStrings.Add(key, (ExpressionSetting value, Custom)) - }) + }) current |> this.Wrap state @@ -1351,9 +1339,10 @@ module Extensions = member this.AddIdentity(state: 'T, identity: UserAssignedIdentity) = let current = this.Get state - { current with - Identity = current.Identity + identity - Settings = current.Settings.Add("AZURE_CLIENT_ID", Setting.ExpressionSetting identity.ClientId) + { + current with + Identity = current.Identity + identity + Settings = current.Settings.Add("AZURE_CLIENT_ID", Setting.ExpressionSetting identity.ClientId) } |> this.Wrap state @@ -1364,10 +1353,11 @@ module Extensions = member this.AddKeyVaultIdentity(state: 'T, identity: UserAssignedIdentity) = let current = this.Get state - { current with - Identity = current.Identity + identity - KeyVaultReferenceIdentity = Some identity - Settings = current.Settings.Add("AZURE_CLIENT_ID", Setting.ExpressionSetting identity.ClientId) + { + current with + Identity = current.Identity + identity + KeyVaultReferenceIdentity = Some identity + Settings = current.Settings.Add("AZURE_CLIENT_ID", Setting.ExpressionSetting identity.ClientId) } |> this.Wrap state @@ -1378,10 +1368,11 @@ module Extensions = member this.SystemIdentity(state: 'T) = let current = this.Get state - { current with - Identity = - { current.Identity with - SystemAssigned = Enabled + { + current with + Identity = { + current.Identity with + SystemAssigned = Enabled } } |> this.Wrap state @@ -1389,17 +1380,19 @@ module Extensions = /// sets the list of origins that should be allowed to make cross-origin calls. Use AllOrigins to allow all. [] member this.EnableCors(state: 'T, origins) = - { this.Get state with - Cors = - match origins with - | [ "*" ] -> Some AllOrigins - | origins -> Some(SpecificOrigins(List.map Uri origins, None)) + { + this.Get state with + Cors = + match origins with + | [ "*" ] -> Some AllOrigins + | origins -> Some(SpecificOrigins(List.map Uri origins, None)) } |> this.Wrap state member this.EnableCors(state: 'T, origins) = - { this.Get state with - Cors = Some origins + { + this.Get state with + Cors = Some origins } |> this.Wrap state @@ -1408,37 +1401,42 @@ module Extensions = member this.EnableCorsCredentials(state: 'T) = let current = this.Get state - { current with - Cors = - current.Cors - |> Option.map (function - | SpecificOrigins (origins, _) -> SpecificOrigins(origins, Some true) - | AllOrigins -> - raiseFarmer "You cannot enable CORS Credentials if you have already set CORS to AllOrigins.") + { + current with + Cors = + current.Cors + |> Option.map (function + | SpecificOrigins(origins, _) -> SpecificOrigins(origins, Some true) + | AllOrigins -> + raiseFarmer + "You cannot enable CORS Credentials if you have already set CORS to AllOrigins.") } |> this.Wrap state /// Sets the operating system [] member this.OperatingSystem(state: 'T, os) = - { this.Get state with - OperatingSystem = os + { + this.Get state with + OperatingSystem = os } |> this.Wrap state /// Specifies a folder path or a zip file containing the web application to install as a post-deployment task. [] member this.ZipDeploy(state: 'T, path) = - { this.Get state with - ZipDeployPath = Some(path, ZipDeploy.ProductionSlot) + { + this.Get state with + ZipDeployPath = Some(path, ZipDeploy.ProductionSlot) } |> this.Wrap state /// Specifies a folder path or a zip file containing the web application to install as a post-deployment task. [] member this.ZipDeploySlot(state: 'T, slotName, path) = - { this.Get state with - ZipDeployPath = Some(path, ZipDeploy.NamedSlot slotName) + { + this.Get state with + ZipDeployPath = Some(path, ZipDeploy.NamedSlot slotName) } |> this.Wrap state @@ -1447,8 +1445,9 @@ module Extensions = member this.AddSecret(state: 'T, key) = let current = this.Get state - { current with - Settings = current.Settings.Add(key, ParameterSetting(SecureParameter key)) + { + current with + Settings = current.Settings.Add(key, ParameterSetting(SecureParameter key)) } |> this.Wrap state @@ -1460,8 +1459,9 @@ module Extensions = ///Chooses the bitness (32 or 64) of the worker process [] member this.WorkerProcess(state: 'T, bitness) = - { this.Get state with - WorkerProcess = Some bitness + { + this.Get state with + WorkerProcess = Some bitness } |> this.Wrap state @@ -1470,13 +1470,16 @@ module Extensions = member this.UseKeyVault(state: 'T) = let current = this.Get state - { current with - Identity = - { current.Identity with - SystemAssigned = Enabled + { + current with + Identity = { + current.Identity with + SystemAssigned = Enabled } - SecretStore = - KeyVault(derived (fun c -> vaults.resourceId (ResourceName(c.Name.ResourceName.Value + "vault")))) + SecretStore = + KeyVault( + derived (fun c -> vaults.resourceId (ResourceName(c.Name.ResourceName.Value + "vault"))) + ) } |> this.Wrap state @@ -1485,12 +1488,13 @@ module Extensions = member this.LinkToKeyVault(state: 'T, vaultName: ResourceName) = let current = this.Get state - { current with - Identity = - { current.Identity with - SystemAssigned = Enabled + { + current with + Identity = { + current.Identity with + SystemAssigned = Enabled } - SecretStore = KeyVault(managed vaults vaultName) + SecretStore = KeyVault(managed vaults vaultName) } |> this.Wrap state @@ -1499,12 +1503,13 @@ module Extensions = member this.LinkToExternalKeyVault(state: 'T, resourceId) = let current = this.Get state - { current with - Identity = - { current.Identity with - SystemAssigned = Enabled + { + current with + Identity = { + current.Identity with + SystemAssigned = Enabled } - SecretStore = KeyVault(unmanaged resourceId) + SecretStore = KeyVault(unmanaged resourceId) } |> this.Wrap state @@ -1513,8 +1518,9 @@ module Extensions = member this.AddSlot(state: 'T, slot: SlotConfig) = let current = this.Get state - { current with - Slots = current.Slots |> Map.add slot.Name slot + { + current with + Slots = current.Slots |> Map.add slot.Name slot } |> this.Wrap state @@ -1526,8 +1532,9 @@ module Extensions = member this.AddSlots(state: 'T, slots: SlotConfig list) = let current = this.Get state - { current with - Slots = slots |> List.fold (fun m s -> Map.add s.Name s m) current.Slots + { + current with + Slots = slots |> List.fold (fun m s -> Map.add s.Name s m) current.Slots } |> this.Wrap state @@ -1544,42 +1551,42 @@ 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 + 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 + 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 + 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 + 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 + this.Map state (fun x -> { + x with IpSecurityRestrictions = IpSecurityRestriction.Create name ip Deny :: x.IpSecurityRestrictions - }) + }) /// Integrate this app with a virtual network subnet [] diff --git a/src/Farmer/Common.fs b/src/Farmer/Common.fs index 0d340d974..a3448a5dd 100644 --- a/src/Farmer/Common.fs +++ b/src/Farmer/Common.fs @@ -789,21 +789,19 @@ module Vm = match this with | ImageSku i -> i - type ImageDefinition = - { - Offer: Offer - Publisher: Publisher - Sku: VmImageSku - OS: OS - } + type ImageDefinition = { + Offer: Offer + Publisher: Publisher + Sku: VmImageSku + OS: OS + } - let makeVm os offer publisher sku = - { - Offer = Offer offer - Publisher = Publisher publisher - OS = os - Sku = ImageSku sku - } + let makeVm os offer publisher sku = { + Offer = Offer offer + Publisher = Publisher publisher + OS = os + Sku = ImageSku sku + } let makeWindowsVm = makeVm Windows "WindowsServer" "MicrosoftWindowsServer" let makeLinuxVm = makeVm Linux @@ -844,11 +842,10 @@ module Vm = | x -> x.ToString() /// Represents a disk in a VM. - type DiskInfo = - { - Size: int - DiskType: DiskType - } + type DiskInfo = { + Size: int + DiskType: DiskType + } with member this.IsUltraDisk = match this.DiskType with @@ -1312,24 +1309,22 @@ module Storage = | PUT -> "PUT" | PATCH -> "PATCH" - type CorsRule = - { - AllowedOrigins: AllOrSpecific - AllowedMethods: HttpMethod NonEmptyList - MaxAgeInSeconds: int - ExposedHeaders: AllOrSpecific - AllowedHeaders: AllOrSpecific + type CorsRule = { + AllowedOrigins: AllOrSpecific + AllowedMethods: HttpMethod NonEmptyList + MaxAgeInSeconds: int + ExposedHeaders: AllOrSpecific + AllowedHeaders: AllOrSpecific + } with + + static member AllowAll = { + AllowedOrigins = All + AllowedMethods = HttpMethod.All + MaxAgeInSeconds = 0 + ExposedHeaders = All + AllowedHeaders = All } - static member AllowAll = - { - AllowedOrigins = All - AllowedMethods = HttpMethod.All - MaxAgeInSeconds = 0 - ExposedHeaders = All - AllowedHeaders = All - } - /// Creates a new CORS rule with static member create(?allowedOrigins, ?allowedMethods, ?maxAgeInSeconds, ?exposedHeaders, ?allowedHeaders) = let mapDefault mapper defaultValue = @@ -1351,11 +1346,10 @@ module Storage = type RestorePolicy = DeleteRetentionPolicy - type LastAccessTimeTrackingPolicy = - { - Enabled: bool - TrackingGranularityInDays: int - } + type LastAccessTimeTrackingPolicy = { + Enabled: bool + TrackingGranularityInDays: int + } type ChangeFeed = { Enabled: bool; RetentionInDays: int } @@ -1380,16 +1374,14 @@ type public IPAddressCidr = { Address: Net.IPAddress; Prefix: int } 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 - } + | [| 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 = @@ -1437,43 +1429,40 @@ module IPAddressCidr = } /// 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 + 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 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 + let _, last = cidr |> ipRangeNums + last, cidr else - raise (IndexOutOfRangeException $"Unable to create subnet {index} of /{size}") - ] + 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. @@ -1554,26 +1543,23 @@ module WebApp = | Allow | Deny - type IpSecurityRestriction = - { - Name: string - IpAddressCidr: IPAddressCidr - Action: IpSecurityAction - } - - static member Create name cidr action = - { - Name = name - IpAddressCidr = cidr - Action = action - } + type IpSecurityRestriction = { + Name: string + IpAddressCidr: IPAddressCidr + Action: IpSecurityAction + } with - type VirtualApplication = - { - PhysicalPath: string - PreloadEnabled: bool option + static member Create name cidr action = { + Name = name + IpAddressCidr = cidr + Action = action } + type VirtualApplication = { + PhysicalPath: string + PreloadEnabled: bool option + } + module Extensions = /// The Microsoft.AspNetCore.AzureAppServices logging extension. let Logging = ExtensionName "Microsoft.AspNetCore.AzureAppServices.SiteExtension" @@ -1841,12 +1827,12 @@ module Sql = member this.Edition = match this with | DTU d -> d.Edition - | VCore (v, _) -> v.Edition + | VCore(v, _) -> v.Edition member this.Name = match this with | DTU d -> d.Name - | VCore (v, _) -> v.Name + | VCore(v, _) -> v.Name type PoolSku = | BasicPool of int @@ -1924,15 +1910,14 @@ module Sql = match this with | SqlAccountName name -> name - type GeoReplicationSettings = - { - /// Suffix name for server and database name - NameSuffix: string - /// Replication location, different from the original one - Location: Farmer.Location - /// Override database Skus - DbSku: DtuSku option - } + type GeoReplicationSettings = { + /// Suffix name for server and database name + NameSuffix: string + /// Replication location, different from the original one + Location: Farmer.Location + /// Override database Skus + DbSku: DtuSku option + } /// Represents a role that can be granted to an identity. type RoleId = @@ -1995,11 +1980,10 @@ module Identity = member this.ClientId = this.CreateExpression "clientId" /// Represents an identity that can be assigned to a resource for impersonation. - type ManagedIdentity = - { - SystemAssigned: FeatureFlag - UserAssigned: UserAssignedIdentity list - } + type ManagedIdentity = { + SystemAssigned: FeatureFlag + UserAssigned: UserAssignedIdentity list + } with member this.Dependencies = this.UserAssigned @@ -2008,22 +1992,20 @@ module Identity = | UserAssignedIdentity rid -> Some rid | LinkedUserAssignedIdentity _ -> None) - static member Empty = - { - SystemAssigned = Disabled - UserAssigned = [] - } + static member Empty = { + SystemAssigned = Disabled + UserAssigned = [] + } - static member (+)(a, b) = - { - SystemAssigned = (a.SystemAssigned.AsBoolean || b.SystemAssigned.AsBoolean) |> FeatureFlag.ofBool - UserAssigned = a.UserAssigned @ b.UserAssigned |> List.distinct - } + static member (+)(a, b) = { + SystemAssigned = (a.SystemAssigned.AsBoolean || b.SystemAssigned.AsBoolean) |> FeatureFlag.ofBool + UserAssigned = a.UserAssigned @ b.UserAssigned |> List.distinct + } - static member (+)(managedIdentity, userAssignedIdentity: UserAssignedIdentity) = - { managedIdentity with + static member (+)(managedIdentity, userAssignedIdentity: UserAssignedIdentity) = { + managedIdentity with UserAssigned = userAssignedIdentity :: managedIdentity.UserAssigned - } + } open Identity @@ -2034,10 +2016,10 @@ module Containers = member this.ImageTag = match this with - | PrivateImage (registry, container, version) -> + | PrivateImage(registry, container, version) -> let version = version |> Option.defaultValue "latest" $"{registry}/{container}:{version}" - | PublicImage (container, version) -> + | PublicImage(container, version) -> let version = version |> Option.defaultValue "latest" $"{container}:{version}" @@ -2057,13 +2039,12 @@ module Containers = | _ -> raiseFarmer $"Malformed docker image tag - incorrect number of version segments: '{tag}'" /// Credential for accessing an image registry. -type ImageRegistryCredential = - { - Server: string - Username: string - Password: SecureParameter - Identity: ManagedIdentity - } +type ImageRegistryCredential = { + Server: string + Username: string + Password: SecureParameter + Identity: ManagedIdentity +} [] type ImageRegistryAuthentication = @@ -2408,12 +2389,11 @@ module ApplicationGateway = | WAF_Medium -> "WAF_Medium" | WAF_v2 -> "WAF_v2" - type ApplicationGatewaySku = - { - Name: Sku - Capacity: int option - Tier: Tier - } + type ApplicationGatewaySku = { + Name: Sku + Capacity: int option + Tier: Tier + } [] type BackendAddress = @@ -2698,8 +2678,8 @@ module ServiceBus = member this.Name = match this with - | SqlFilter (name, _) - | CorrelationFilter (name, _, _) -> name + | SqlFilter(name, _) + | CorrelationFilter(name, _, _) -> name static member CreateCorrelationFilter(name, properties, ?correlationId) = CorrelationFilter(ResourceName name, correlationId, Map properties) @@ -2932,7 +2912,7 @@ module NetworkSecurity = member this.ArmValue = match this with | Port num -> num |> string - | Range (first, last) -> $"{first}-{last}" + | Range(first, last) -> $"{first}-{last}" | AnyPort -> "*" module Port = @@ -3192,21 +3172,18 @@ module DeliveryPolicy = member this.ArmValue = match this with - | Override t -> - {| - Behaviour = "Override" - CacheDuration = Some t - |} - | BypassCache -> - {| - Behaviour = "BypassCache" - CacheDuration = None - |} - | SetIfMissing t -> - {| - Behaviour = "SetIfMissing" - CacheDuration = Some t - |} + | Override t -> {| + Behaviour = "Override" + CacheDuration = Some t + |} + | BypassCache -> {| + Behaviour = "BypassCache" + CacheDuration = None + |} + | SetIfMissing t -> {| + Behaviour = "SetIfMissing" + CacheDuration = Some t + |} type QueryStringCacheBehavior = | Include @@ -3261,24 +3238,22 @@ module Dns = | Public | Private - type SrvRecord = - { - Priority: int option - Weight: int option - Port: int option - Target: string option - } + type SrvRecord = { + Priority: int option + Weight: int option + Port: int option + Target: string option + } - type SoaRecord = - { - Host: string option - Email: string option - SerialNumber: int64 option - RefreshTime: int64 option - RetryTime: int64 option - ExpireTime: int64 option - MinimumTTL: int64 option - } + type SoaRecord = { + Host: string option + Email: string option + SerialNumber: int64 option + RefreshTime: int64 option + RetryTime: int64 option + ExpireTime: int64 option + MinimumTTL: int64 option + } [] type NsRecords = @@ -3331,15 +3306,14 @@ module TrafficManager = member this.ArmValue = this.ToString().ToUpperInvariant() - type MonitorConfig = - { - Protocol: MonitorProtocol - Port: int - Path: string - IntervalInSeconds: int - ToleratedNumberOfFailures: int - TimeoutInSeconds: int - } + type MonitorConfig = { + Protocol: MonitorProtocol + Port: int + Path: string + IntervalInSeconds: int + ToleratedNumberOfFailures: int + TimeoutInSeconds: int + } type EndpointTarget = | Website of ResourceName @@ -3348,7 +3322,7 @@ module TrafficManager = member this.ArmValue = match this with | Website name -> name.Value - | External (target, _) -> target + | External(target, _) -> target module Serialization = open System.Text.Json @@ -3475,31 +3449,28 @@ module AvailabilityTest = module ContainerApp = //type SecretRef = SecretRef of string - type EventHubScaleRule = - { - ConsumerGroup: string - UnprocessedEventThreshold: int - CheckpointBlobContainerName: string - EventHubConnectionSecretRef: string - StorageConnectionSecretRef: string - } + type EventHubScaleRule = { + ConsumerGroup: string + UnprocessedEventThreshold: int + CheckpointBlobContainerName: string + EventHubConnectionSecretRef: string + StorageConnectionSecretRef: string + } - type ServiceBusScaleRule = - { - QueueName: string - MessageCount: int - SecretRef: string - } + type ServiceBusScaleRule = { + QueueName: string + MessageCount: int + SecretRef: string + } type HttpScaleRule = { ConcurrentRequests: int } - type StorageQueueScaleRule = - { - QueueName: string - QueueLength: int - StorageConnectionSecretRef: string - AccountName: string - } + type StorageQueueScaleRule = { + QueueName: string + QueueLength: int + StorageConnectionSecretRef: string + AccountName: string + } type UtilizationRule = { Utilization: int } type AverageValueRule = { AverageValue: int } @@ -3564,52 +3535,46 @@ type LogCategory = match this with | LogCategory v -> v -type RetentionPolicy = - { - Enabled: bool - RetentionPeriod: int - } +type RetentionPolicy = { + Enabled: bool + RetentionPeriod: int +} with static member Create(retentionPeriod, ?enabled) = match retentionPeriod with | OutOfBounds days -> raiseFarmer $"The retention period must be between 1 and 365 days. It is currently {days}." - | InBounds _ -> - { - Enabled = defaultArg enabled true - RetentionPeriod = retentionPeriod - } - -type MetricSetting = - { - Category: string - TimeGrain: TimeSpan option - Enabled: bool - RetentionPolicy: RetentionPolicy option + | InBounds _ -> { + Enabled = defaultArg enabled true + RetentionPeriod = retentionPeriod + } + +type MetricSetting = { + Category: string + TimeGrain: TimeSpan option + Enabled: bool + RetentionPolicy: RetentionPolicy option +} with + + static member Create(category, ?retentionPeriod, ?timeGrain) = { + Category = category + TimeGrain = timeGrain + Enabled = true + RetentionPolicy = retentionPeriod |> Option.map (fun days -> RetentionPolicy.Create(days, true)) } - static member Create(category, ?retentionPeriod, ?timeGrain) = - { - Category = category - TimeGrain = timeGrain - Enabled = true - RetentionPolicy = retentionPeriod |> Option.map (fun days -> RetentionPolicy.Create(days, true)) - } +type LogSetting = { + Category: LogCategory + Enabled: bool + RetentionPolicy: RetentionPolicy option +} with -type LogSetting = - { - Category: LogCategory - Enabled: bool - RetentionPolicy: RetentionPolicy option + static member Create(category, ?retentionPeriod) = { + Category = category + Enabled = true + RetentionPolicy = retentionPeriod |> Option.map (fun days -> RetentionPolicy.Create(days, true)) } - static member Create(category, ?retentionPeriod) = - { - Category = category - Enabled = true - RetentionPolicy = retentionPeriod |> Option.map (fun days -> RetentionPolicy.Create(days, true)) - } - static member Create(category, ?retentionPeriod) = LogSetting.Create(LogCategory category, ?retentionPeriod = retentionPeriod) diff --git a/src/Farmer/Deploy.fs b/src/Farmer/Deploy.fs index 71f8f2285..0ff92e29d 100644 --- a/src/Farmer/Deploy.fs +++ b/src/Farmer/Deploy.fs @@ -100,36 +100,35 @@ module Az = let version () = az "--version" /// Checks that the version of the Azure CLI meets minimum version. - let checkVersion minimum = - result { - let! versionOutput = version () - - let! version = - versionOutput - .Replace("\r\n", "\n") - .Replace("\r", "\n") - .Split([| "\n" |], StringSplitOptions.RemoveEmptyEntries) - |> Array.tryHead - |> Option.bind (fun text -> - match text.Split([| ' ' |], StringSplitOptions.RemoveEmptyEntries) with - | [| _; version |] - | [| _; version; _ |] -> Some version - | _ -> None) - |> Option.bind (fun versionText -> - try - Some(Version versionText) - with _ -> - None) - |> Result.ofOption - $"Unable to determine Azure CLI version. You need to have at least {minimum} installed. Output was: %s{versionOutput}" - - return! - if version < minimum then - Error - $"You have {version} of the Azure CLI installed, but the minimum version is {minimum}. Please upgrade." - else - Ok version - } + let checkVersion minimum = result { + let! versionOutput = version () + + let! version = + versionOutput + .Replace("\r\n", "\n") + .Replace("\r", "\n") + .Split([| "\n" |], StringSplitOptions.RemoveEmptyEntries) + |> Array.tryHead + |> Option.bind (fun text -> + match text.Split([| ' ' |], StringSplitOptions.RemoveEmptyEntries) with + | [| _; version |] + | [| _; version; _ |] -> Some version + | _ -> None) + |> Option.bind (fun versionText -> + try + Some(Version versionText) + with _ -> + None) + |> Result.ofOption + $"Unable to determine Azure CLI version. You need to have at least {minimum} installed. Output was: %s{versionOutput}" + + return! + if version < minimum then + Error + $"You have {version} of the Azure CLI installed, but the minimum version is {minimum}. Please upgrade." + else + Ok version + } /// Lists all subscriptions let listSubscriptions () = az "account list --all" @@ -262,12 +261,11 @@ module Az = error /// Represents an Azure subscription -type Subscription = - { - ID: Guid - Name: string - IsDefault: bool - } +type Subscription = { + ID: Guid + Name: string + IsDefault: bool +} /// Authenticates the Az CLI using the supplied ApplicationId, Client Secret and Tenant Id. /// Returns the list of subscriptions, including which one the default is. @@ -276,11 +274,10 @@ let authenticate appId secret tenantId = |> Result.map (Serialization.ofJson) /// Lists all subscriptions that the logged in identity has access to. -let listSubscriptions () = - result { - let! response = Az.listSubscriptions () - return response |> Serialization.ofJson - } +let listSubscriptions () = result { + let! response = Az.listSubscriptions () + return response |> Serialization.ofJson +} /// Sets the currently active (default) subscription. let setSubscription (subscriptionId: Guid) = @@ -308,115 +305,107 @@ let validateParameters suppliedParameters (deployment: IDeploymentSource) = let NoParameters: (string * string) list = [] -let private prepareForDeployment parameters resourceGroupName (deployment: IDeploymentSource) = - result { - do! deployment |> validateParameters parameters - - let! version = Az.checkVersion Az.MinimumVersion - printfn "Compatible version of Azure CLI %O detected" version - - prepareDeploymentFolder () - - let! subscriptionDetails = - printf "Checking Azure CLI logged in status... " - - match Az.showAccount () with - | Ok response -> - printfn "you are already logged in, nothing to do." - Ok response - | Error _ -> - printfn "logging you in." - Az.login () |> Result.bind (fun _ -> Az.showAccount ()) - - let subscriptionDetails = - subscriptionDetails |> Serialization.ofJson<{| id: Guid; name: string |}> - - printfn "Using subscription '%s' (%O)." subscriptionDetails.name subscriptionDetails.id - - let resourceGroups = - (resourceGroupName :: deployment.Deployment.RequiredResourceGroups) - |> List.distinct - // Filter out any resource groups that are an ARM expression calculated at deploy-time - |> List.filter (fun resGroupName -> not (resGroupName.StartsWith("["))) - |> List.mapi (fun i x -> i, x) - - for (i, rg) in resourceGroups do - printfn $"Creating resource group {rg} ({i + 1}/{resourceGroups.Length})..." - do! Az.createResourceGroup deployment.Deployment.Location.ArmValue deployment.Deployment.Tags rg - - return - {| - DeploymentName = $"farmer-deploy-{generateDeployNumber ()}" - TemplateFilename = - deployment.Deployment.Template - |> Writer.toJson - |> Writer.toFile deployFolder "farmer-deploy" - |} - } +let private prepareForDeployment parameters resourceGroupName (deployment: IDeploymentSource) = result { + do! deployment |> validateParameters parameters + + let! version = Az.checkVersion Az.MinimumVersion + printfn "Compatible version of Azure CLI %O detected" version + + prepareDeploymentFolder () + + let! subscriptionDetails = + printf "Checking Azure CLI logged in status... " + + match Az.showAccount () with + | Ok response -> + printfn "you are already logged in, nothing to do." + Ok response + | Error _ -> + printfn "logging you in." + Az.login () |> Result.bind (fun _ -> Az.showAccount ()) + + let subscriptionDetails = + subscriptionDetails |> Serialization.ofJson<{| id: Guid; name: string |}> + + printfn "Using subscription '%s' (%O)." subscriptionDetails.name subscriptionDetails.id + + let resourceGroups = + (resourceGroupName :: deployment.Deployment.RequiredResourceGroups) + |> List.distinct + // Filter out any resource groups that are an ARM expression calculated at deploy-time + |> List.filter (fun resGroupName -> not (resGroupName.StartsWith("["))) + |> List.mapi (fun i x -> i, x) + + for (i, rg) in resourceGroups do + printfn $"Creating resource group {rg} ({i + 1}/{resourceGroups.Length})..." + do! Az.createResourceGroup deployment.Deployment.Location.ArmValue deployment.Deployment.Tags rg + + return {| + DeploymentName = $"farmer-deploy-{generateDeployNumber ()}" + TemplateFilename = + deployment.Deployment.Template + |> Writer.toJson + |> Writer.toFile deployFolder "farmer-deploy" + |} +} /// Validates a deployment against a resource group. If the resource group does not exist, it will be created automatically. -let tryValidate resourceGroupName parameters (deployment: IDeploymentSource) = - result { - let! deploymentParameters = deployment |> prepareForDeployment parameters resourceGroupName +let tryValidate resourceGroupName parameters (deployment: IDeploymentSource) = result { + let! deploymentParameters = deployment |> prepareForDeployment parameters resourceGroupName - return! - Az.validate - resourceGroupName - deploymentParameters.DeploymentName - deploymentParameters.TemplateFilename - parameters - } + return! + Az.validate + resourceGroupName + deploymentParameters.DeploymentName + deploymentParameters.TemplateFilename + parameters +} /// Validates a deployment against a resource group. If the resource group does not exist, it will be created automatically. -let tryWhatIf resourceGroupName parameters (deployment: IDeploymentSource) = - result { - let! deploymentParameters = deployment |> prepareForDeployment parameters resourceGroupName +let tryWhatIf resourceGroupName parameters (deployment: IDeploymentSource) = result { + let! deploymentParameters = deployment |> prepareForDeployment parameters resourceGroupName - return! - Az.whatIf - resourceGroupName - deploymentParameters.DeploymentName - deploymentParameters.TemplateFilename - parameters - } + return! + Az.whatIf resourceGroupName deploymentParameters.DeploymentName deploymentParameters.TemplateFilename parameters +} /// Executes the supplied Deployment against a resource group using the Azure CLI. /// If successful, returns a Map of the output keys and values. -let tryExecute resourceGroupName parameters (deployment: IDeploymentSource) = - result { - let! deploymentParameters = deployment |> prepareForDeployment parameters resourceGroupName - - printfn "Deploying ARM template (please be patient, this can take a while)..." - - let! response = - Az.deploy - resourceGroupName - deploymentParameters.DeploymentName - deploymentParameters.TemplateFilename - parameters - - do! - [ - for task in deployment.Deployment.PostDeployTasks do - task.Run resourceGroupName - ] - |> List.choose id - |> Result.sequence - |> Result.ignore - - printfn "All done, now parsing ARM response to get any outputs..." - - let! response = - response - |> Result.ofExn - Serialization.ofJson<{| properties: {| outputs: IDictionary |} |}> - |> Result.mapError (fun _ -> response) - - return - response.properties.outputs - |> Seq.map (fun r -> r.Key, r.Value.value) - |> Map.ofSeq - } +let tryExecute resourceGroupName parameters (deployment: IDeploymentSource) = result { + let! deploymentParameters = deployment |> prepareForDeployment parameters resourceGroupName + + printfn "Deploying ARM template (please be patient, this can take a while)..." + + let! response = + Az.deploy resourceGroupName deploymentParameters.DeploymentName deploymentParameters.TemplateFilename parameters + + do! + [ + for task in deployment.Deployment.PostDeployTasks do + task.Run resourceGroupName + ] + |> List.choose id + |> Result.sequence + |> Result.ignore + + printfn "All done, now parsing ARM response to get any outputs..." + + let! response = + response + |> Result.ofExn + Serialization.ofJson<{| + properties: + {| + outputs: IDictionary + |} + |}> + |> Result.mapError (fun _ -> response) + + return + response.properties.outputs + |> Seq.map (fun r -> r.Key, r.Value.value) + |> Map.ofSeq +} /// Executes the supplied Deployment against a resource group using the Azure CLI. /// If successful, returns a Map of the output keys and values, otherwise returns any error as an exception. diff --git a/src/Farmer/IdentityExtensions.fs b/src/Farmer/IdentityExtensions.fs index 1f06c69da..ca23725eb 100644 --- a/src/Farmer/IdentityExtensions.fs +++ b/src/Farmer/IdentityExtensions.fs @@ -9,43 +9,38 @@ module ManagedIdentityExtensions = type ManagedIdentity with /// Creates a single User-Assigned ResourceIdentity from a ResourceId - static member create(resourceId: ResourceId) = - { - SystemAssigned = Disabled - UserAssigned = [ UserAssignedIdentity resourceId ] - } + static member create(resourceId: ResourceId) = { + SystemAssigned = Disabled + UserAssigned = [ UserAssignedIdentity resourceId ] + } static member create(identity: Identity.UserAssignedIdentity) = match identity with - | LinkedUserAssignedIdentity rid -> - { - SystemAssigned = Disabled - UserAssigned = [ LinkedUserAssignedIdentity rid ] - } - | UserAssignedIdentity rid -> - { - SystemAssigned = Disabled - UserAssigned = [ UserAssignedIdentity rid ] - } + | LinkedUserAssignedIdentity rid -> { + SystemAssigned = Disabled + UserAssigned = [ LinkedUserAssignedIdentity rid ] + } + | UserAssignedIdentity rid -> { + SystemAssigned = Disabled + UserAssigned = [ UserAssignedIdentity rid ] + } /// Creates a resource identity from a resource name static member create(name: ResourceName) = userAssignedIdentities.resourceId name |> ManagedIdentity.create module Roles = - type RoleAssignment = - { - Role: RoleId - Principal: PrincipalId - Owner: ResourceId option - } + type RoleAssignment = { + Role: RoleId + Principal: PrincipalId + Owner: ResourceId option + } let private makeRoleId name (roleId: string) = - RoleId - {| - Name = name - Id = Guid.Parse roleId - |} + RoleId {| + Name = name + Id = Guid.Parse roleId + |} /// Can customize the developer portal, edit its content, and publish it. let APIManagementDeveloperPortalContentEditor = diff --git a/src/Farmer/Types.fs b/src/Farmer/Types.fs index 47d3fd1a4..71366a38b 100644 --- a/src/Farmer/Types.fs +++ b/src/Farmer/Types.fs @@ -76,30 +76,28 @@ type ResourceType = /// Returns the ARM resource type string value. member this.Type = match this with - | ResourceType (p, _) -> p + | ResourceType(p, _) -> p member this.ApiVersion = match this with - | ResourceType (_, v) -> v - -type ResourceId = - { - Type: ResourceType - ResourceGroup: string option - Subscription: string option - Name: ResourceName - Segments: ResourceName list + | ResourceType(_, v) -> v + +type ResourceId = { + Type: ResourceType + ResourceGroup: string option + Subscription: string option + Name: ResourceName + Segments: ResourceName list +} with + + static member create(resourceType: ResourceType, name: ResourceName, ?group: string, ?subscription: string) = { + Type = resourceType + ResourceGroup = group + Subscription = subscription + Name = name + Segments = [] } - static member create(resourceType: ResourceType, name: ResourceName, ?group: string, ?subscription: string) = - { - Type = resourceType - ResourceGroup = group - Subscription = subscription - Name = name - Segments = [] - } - static member create ( resourceType: ResourceType, @@ -166,17 +164,17 @@ type ArmExpression = /// Gets the raw value of this expression. member this.Value = match this with - | ArmExpression (e, _) -> e + | ArmExpression(e, _) -> e /// Tries to get the owning resource of this expression. member this.Owner = match this with - | ArmExpression (_, o) -> o + | ArmExpression(_, o) -> o /// Applies a mapping function to the expression. member this.Map mapper = match this with - | ArmExpression (e, r) -> ArmExpression(mapper e, r) + | ArmExpression(e, r) -> ArmExpression(mapper e, r) /// Evaluates the expression for emitting into an ARM template. That is, wraps it in []. member this.Eval() = @@ -186,13 +184,13 @@ type ArmExpression = specialCases |> List.tryFind (fun (case, _, _) -> System.Text.RegularExpressions.Regex.IsMatch(this.Value, case)) with - | Some (_, start, finish) -> this.Value.Substring(start, this.Value.Length - finish) + | Some(_, start, finish) -> this.Value.Substring(start, this.Value.Length - finish) | None -> $"[{this.Value}]" /// Sets the owning resource on this ARM Expression. member this.WithOwner(owner: ResourceId) = match this with - | ArmExpression (e, _) -> ArmExpression(e, Some owner) + | ArmExpression(e, _) -> ArmExpression(e, Some owner) // /// Sets the owning resource on this ARM Expression. // member this.WithOwner(owner:ResourceName) = this.WithOwner(ResourceId.create owner) @@ -266,18 +264,17 @@ type ResourceType with ?tags: Map ) = match this with - | ResourceType (path, version) -> - {| - ``type`` = path - apiVersion = version - name = name.Value - location = location |> Option.map (fun r -> r.ArmValue) |> Option.toObj - dependsOn = - dependsOn - |> Option.map (Seq.map (fun r -> r.Eval()) >> Seq.toArray >> box) - |> Option.toObj - tags = tags |> Option.map box |> Option.toObj - |} + | ResourceType(path, version) -> {| + ``type`` = path + apiVersion = version + name = name.Value + location = location |> Option.map (fun r -> r.ArmValue) |> Option.toObj + dependsOn = + dependsOn + |> Option.map (Seq.map (fun r -> r.Eval()) >> Seq.toArray >> box) + |> Option.toObj + tags = tags |> Option.map box |> Option.toObj + |} /// A secure parameter to be captured in an ARM template. type SecureParameter = @@ -370,8 +367,8 @@ type DomainConfig = member this.DomainName = match this with - | SecureDomain (domainName, _) - | InsecureDomain (domainName) -> domainName + | SecureDomain(domainName, _) + | InsecureDomain(domainName) -> domainName [] @@ -393,8 +390,8 @@ module ResourceRef = let (|DependableResource|_|) config = function | AutoGeneratedResource r -> Some(DependableResource(r.resourceId config)) - | LinkedResource (Managed r) -> Some(DependableResource r) - | LinkedResource (Unmanaged _) -> None + | LinkedResource(Managed r) -> Some(DependableResource r) + | LinkedResource(Unmanaged _) -> None /// An active pattern that returns the resource name if the resource should be deployed. In other /// words, AutoCreate only. @@ -407,8 +404,8 @@ module ResourceRef = let (|ExternalResource|_|) = function | AutoGeneratedResource _ -> None - | LinkedResource (Managed r) - | LinkedResource (Unmanaged r) -> Some r + | LinkedResource(Managed r) + | LinkedResource(Unmanaged r) -> Some r /// Whether a specific feature is active or not. type FeatureFlag = @@ -467,21 +464,19 @@ type Setting = static member AsLiteral(a, b) = a, LiteralSetting b -type ArmTemplate = - { - Parameters: SecureParameter list - Outputs: (string * string) list - Resources: IArmResource list - } - -type Deployment = - { - Location: Location - Template: ArmTemplate - PostDeployTasks: IPostDeploy list - RequiredResourceGroups: string list - Tags: Map - } +type ArmTemplate = { + Parameters: SecureParameter list + Outputs: (string * string) list + Resources: IArmResource list +} + +type Deployment = { + Location: Location + Template: ArmTemplate + PostDeployTasks: IPostDeploy list + RequiredResourceGroups: string list + Tags: Map +} with interface IDeploymentSource with member this.Deployment = this diff --git a/src/Farmer/Writer.fs b/src/Farmer/Writer.fs index e4050972f..6262138cc 100644 --- a/src/Farmer/Writer.fs +++ b/src/Farmer/Writer.fs @@ -6,24 +6,25 @@ open System.Reflection open Farmer module TemplateGeneration = - let processTemplate (template: ArmTemplate) = - {| - ``$schema`` = "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#" - contentVersion = "1.0.0.0" - resources = template.Resources |> List.map (fun r -> r.JsonModel) - parameters = - template.Parameters - |> List.map (fun (SecureParameter p) -> p, {| ``type`` = "securestring" |}) - |> Map.ofList - outputs = - template.Outputs - |> List.map (fun (k, v) -> k, {| ``type`` = "string"; value = v |}) - |> Map.ofList - |} + let processTemplate (template: ArmTemplate) = {| + ``$schema`` = "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#" + contentVersion = "1.0.0.0" + resources = template.Resources |> List.map (fun r -> r.JsonModel) + parameters = + template.Parameters + |> List.map (fun (SecureParameter p) -> p, {| ``type`` = "securestring" |}) + |> Map.ofList + outputs = + template.Outputs + |> List.map (fun (k, v) -> k, {| ``type`` = "string"; value = v |}) + |> Map.ofList + |} let branding () = let version = - Assembly.GetExecutingAssembly().GetCustomAttribute() + Assembly + .GetExecutingAssembly() + .GetCustomAttribute() .InformationalVersion printfn "==================================================" diff --git a/src/Tests/Alerts.fs b/src/Tests/Alerts.fs index f6eec46ce..7f311dfd3 100644 --- a/src/Tests/Alerts.fs +++ b/src/Tests/Alerts.fs @@ -7,245 +7,228 @@ open Farmer.Builders open Farmer.Arm.InsightsAlerts let tests = - testList - "Alerts" - [ - test "Create a VM alert" { - let vm = - vm { - name "foo" - username "foo" - } + testList "Alerts" [ + test "Create a VM alert" { + let vm = vm { + name "foo" + username "foo" + } - let vmAlert = - alert { - name "myVmAlert2" - description "Alert if VM CPU goes over 80% for 15 minutes" - frequency (System.TimeSpan.FromMinutes(5.0) |> IsoDateTime.OfTimeSpan) - window (System.TimeSpan.FromMinutes(15.0) |> IsoDateTime.OfTimeSpan) - add_linked_resource vm - severity AlertSeverity.Warning - - single_resource_multiple_metric_criteria - [ - { - MetricNamespace = vm.ResourceId.Type - MetricName = MetricsName.PercentageCPU - Threshold = 80 - Comparison = GreaterThan - Aggregation = Average - } - ] + let vmAlert = alert { + name "myVmAlert2" + description "Alert if VM CPU goes over 80% for 15 minutes" + frequency (System.TimeSpan.FromMinutes(5.0) |> IsoDateTime.OfTimeSpan) + window (System.TimeSpan.FromMinutes(15.0) |> IsoDateTime.OfTimeSpan) + add_linked_resource vm + severity AlertSeverity.Warning + + single_resource_multiple_metric_criteria [ + { + MetricNamespace = vm.ResourceId.Type + MetricName = MetricsName.PercentageCPU + Threshold = 80 + Comparison = GreaterThan + Aggregation = Average } + ] + } - let template = arm { add_resources [ vm; vmAlert ] } - let jsn = template.Template |> Writer.toJson - let jobj = jsn |> Newtonsoft.Json.Linq.JObject.Parse + let template = arm { add_resources [ vm; vmAlert ] } + let jsn = template.Template |> Writer.toJson + let jobj = jsn |> Newtonsoft.Json.Linq.JObject.Parse - let isenabled = - jobj - .SelectToken("resources[?(@.name=='myVmAlert2')].properties.enabled") - .ToString() - .ToLower() + let isenabled = + jobj + .SelectToken("resources[?(@.name=='myVmAlert2')].properties.enabled") + .ToString() + .ToLower() - Expect.equal isenabled "true" "Webtest not enabled" - } + Expect.equal isenabled "true" "Webtest not enabled" + } - test "Create a SQL Database heavy usage alert" { - let sql = - sqlServer { - name "my37server" - admin_username "isaac" - - add_databases - [ - sqlDb { - name "mydb23" - sku Farmer.Sql.DtuSku.S0 - } - ] - } + test "Create a SQL Database heavy usage alert" { + let sql = sqlServer { + name "my37server" + admin_username "isaac" - let db = sql.Databases.Head - - let resId = - Farmer.Arm.Sql.databases.resourceId (sql.Name.ResourceName, db.Name) |> Managed - - let myAlert = - alert { - name "myDbAlert" - description "Alert if DB DTU goes over 80% for 5 minutes" - frequency (System.TimeSpan.FromMinutes(5.0) |> IsoDateTime.OfTimeSpan) - window (System.TimeSpan.FromMinutes(5.0) |> IsoDateTime.OfTimeSpan) - add_linked_resource resId - severity AlertSeverity.Error - - single_resource_multiple_metric_criteria - [ - { - MetricNamespace = resId.ResourceId.Type - MetricName = MetricsName.SQL_DB_DTU - Threshold = 80 - Comparison = GreaterThan - Aggregation = Average - } - ] + add_databases [ + sqlDb { + name "mydb23" + sku Farmer.Sql.DtuSku.S0 } + ] + } - let template = - arm { - add_resource sql - add_resource myAlert + let db = sql.Databases.Head + + let resId = + Farmer.Arm.Sql.databases.resourceId (sql.Name.ResourceName, db.Name) |> Managed + + let myAlert = alert { + name "myDbAlert" + description "Alert if DB DTU goes over 80% for 5 minutes" + frequency (System.TimeSpan.FromMinutes(5.0) |> IsoDateTime.OfTimeSpan) + window (System.TimeSpan.FromMinutes(5.0) |> IsoDateTime.OfTimeSpan) + add_linked_resource resId + severity AlertSeverity.Error + + single_resource_multiple_metric_criteria [ + { + MetricNamespace = resId.ResourceId.Type + MetricName = MetricsName.SQL_DB_DTU + Threshold = 80 + Comparison = GreaterThan + Aggregation = Average } + ] + } - let jsn = template.Template |> Writer.toJson - let jobj = jsn |> Newtonsoft.Json.Linq.JObject.Parse - - let allOf = - jobj.SelectToken("resources[?(@.name=='myDbAlert')].properties.criteria.allOf[0]") + let template = arm { + add_resource sql + add_resource myAlert + } - Expect.isNotNull allOf "allOf not found" + let jsn = template.Template |> Writer.toJson + let jobj = jsn |> Newtonsoft.Json.Linq.JObject.Parse - let targ = - jobj - .SelectToken("resources[?(@.name=='myDbAlert')].properties.targetResourceType") - .ToString() + let allOf = + jobj.SelectToken("resources[?(@.name=='myDbAlert')].properties.criteria.allOf[0]") - Expect.equal targ Farmer.Arm.Sql.databases.Type "Wrong target resource type" - } + Expect.isNotNull allOf "allOf not found" - test "Create a webtest alert when website down" { - let ai = appInsights { name "ai" } + let targ = + jobj + .SelectToken("resources[?(@.name=='myDbAlert')].properties.targetResourceType") + .ToString() - let webtest = - availabilityTest { - name "webTest" - link_to_app_insights ai + Expect.equal targ Farmer.Arm.Sql.databases.Type "Wrong target resource type" + } - locations - [ - AvailabilityTest.TestSiteLocation.CentralUS - AvailabilityTest.TestSiteLocation.NorthEurope - ] + test "Create a webtest alert when website down" { + let ai = appInsights { name "ai" } - web_test ("https://google.com" |> System.Uri |> AvailabilityTest.WebsiteUrl) - } + let webtest = availabilityTest { + name "webTest" + link_to_app_insights ai - let aiId, webId = - (ai :> IBuilder).ResourceId |> Managed, (webtest :> IBuilder).ResourceId |> Managed - - let webAlert = - alert { - name "myWebAlert" - description "Alert if Google is failing 5 mins on both 2 locations" - frequency (System.TimeSpan.FromMinutes(1.0) |> IsoDateTime.OfTimeSpan) - window (System.TimeSpan.FromMinutes(5.0) |> IsoDateTime.OfTimeSpan) - add_linked_resources [ aiId; webId ] - severity AlertSeverity.Warning - webtest_location_availability_criteria (aiId.ResourceId, webId.ResourceId, 2) - } + locations [ + AvailabilityTest.TestSiteLocation.CentralUS + AvailabilityTest.TestSiteLocation.NorthEurope + ] - let template = arm { add_resources [ ai; webtest; webAlert ] } - let jsn = template.Template |> Writer.toJson - let jobj = jsn |> Newtonsoft.Json.Linq.JObject.Parse - - let freq = - jobj - .SelectToken("resources[?(@.name=='myWebAlert')].properties.evaluationFrequency") - .ToString() + web_test ("https://google.com" |> System.Uri |> AvailabilityTest.WebsiteUrl) + } - Expect.equal freq "PT1M" "Wrong frequency" + let aiId, webId = + (ai :> IBuilder).ResourceId |> Managed, (webtest :> IBuilder).ResourceId |> Managed + + let webAlert = alert { + name "myWebAlert" + description "Alert if Google is failing 5 mins on both 2 locations" + frequency (System.TimeSpan.FromMinutes(1.0) |> IsoDateTime.OfTimeSpan) + window (System.TimeSpan.FromMinutes(5.0) |> IsoDateTime.OfTimeSpan) + add_linked_resources [ aiId; webId ] + severity AlertSeverity.Warning + webtest_location_availability_criteria (aiId.ResourceId, webId.ResourceId, 2) } - test "Create a custom metric alert based on Azure ApplicationInsights" { - let alertName = "myCustomAlert" - let ai = appInsights { name "ai" } - - let customAlert = - alert { - name alertName - description "Alert based on MyCustomMetric" - frequency (System.TimeSpan.FromMinutes(1.0) |> IsoDateTime.OfTimeSpan) - window (System.TimeSpan.FromMinutes(5.0) |> IsoDateTime.OfTimeSpan) - add_linked_resource ai - severity AlertSeverity.Warning - - single_resource_multiple_custom_metric_criteria - [ - { - MetricNamespace = None - MetricName = MetricsName "MyCustomMetric" - Threshold = 20 - Comparison = GreaterThan - Aggregation = Average - } - ] + let template = arm { add_resources [ ai; webtest; webAlert ] } + let jsn = template.Template |> Writer.toJson + let jobj = jsn |> Newtonsoft.Json.Linq.JObject.Parse + + let freq = + jobj + .SelectToken("resources[?(@.name=='myWebAlert')].properties.evaluationFrequency") + .ToString() + + Expect.equal freq "PT1M" "Wrong frequency" + } + + test "Create a custom metric alert based on Azure ApplicationInsights" { + let alertName = "myCustomAlert" + let ai = appInsights { name "ai" } + + let customAlert = alert { + name alertName + description "Alert based on MyCustomMetric" + frequency (System.TimeSpan.FromMinutes(1.0) |> IsoDateTime.OfTimeSpan) + window (System.TimeSpan.FromMinutes(5.0) |> IsoDateTime.OfTimeSpan) + add_linked_resource ai + severity AlertSeverity.Warning + + single_resource_multiple_custom_metric_criteria [ + { + MetricNamespace = None + MetricName = MetricsName "MyCustomMetric" + Threshold = 20 + Comparison = GreaterThan + Aggregation = Average } - - let template = arm { add_resources [ ai; customAlert ] } - let jsn = template.Template |> Writer.toJson - let jobj = jsn |> Newtonsoft.Json.Linq.JObject.Parse - - let allOf = - jobj.SelectToken($"resources[?(@.name=='{alertName}')].properties.criteria.allOf[0]") - - Expect.isNotNull allOf "allOf not found" - Expect.equal (allOf.Item("metricName").ToString()) "MyCustomMetric" "Wrong target metric namespace" - - Expect.equal - (allOf.Item("metricNamespace").ToString()) - "Azure.ApplicationInsights" - "Wrong target metric namespace" - - Expect.equal - (allOf.Item("skipMetricValidation").ToObject()) - true - "Wrong value of skipMetricValidation" - - let targ = - jobj - .SelectToken($"resources[?(@.name=='{alertName}')].properties.targetResourceType") - .ToString() - - Expect.equal targ Farmer.Arm.Insights.components.Type "Wrong target resource type" + ] } - test "Create a custom metric alert based on custom namespace" { - let alertName = "myCustomAlert" - - let customAlert = - alert { - name alertName - description "Alert based on MyCustomMetric" - frequency (System.TimeSpan.FromMinutes(1.0) |> IsoDateTime.OfTimeSpan) - window (System.TimeSpan.FromMinutes(5.0) |> IsoDateTime.OfTimeSpan) - severity AlertSeverity.Warning - - single_resource_multiple_custom_metric_criteria - [ - { - MetricNamespace = Some(ResourceType("MyCustomNamespace", "")) - MetricName = MetricsName "MyCustomMetric" - Threshold = 20 - Comparison = GreaterThan - Aggregation = Average - } - ] + let template = arm { add_resources [ ai; customAlert ] } + let jsn = template.Template |> Writer.toJson + let jobj = jsn |> Newtonsoft.Json.Linq.JObject.Parse + + let allOf = + jobj.SelectToken($"resources[?(@.name=='{alertName}')].properties.criteria.allOf[0]") + + Expect.isNotNull allOf "allOf not found" + Expect.equal (allOf.Item("metricName").ToString()) "MyCustomMetric" "Wrong target metric namespace" + + Expect.equal + (allOf.Item("metricNamespace").ToString()) + "Azure.ApplicationInsights" + "Wrong target metric namespace" + + Expect.equal + (allOf.Item("skipMetricValidation").ToObject()) + true + "Wrong value of skipMetricValidation" + + let targ = + jobj + .SelectToken($"resources[?(@.name=='{alertName}')].properties.targetResourceType") + .ToString() + + Expect.equal targ Farmer.Arm.Insights.components.Type "Wrong target resource type" + } + + test "Create a custom metric alert based on custom namespace" { + let alertName = "myCustomAlert" + + let customAlert = alert { + name alertName + description "Alert based on MyCustomMetric" + frequency (System.TimeSpan.FromMinutes(1.0) |> IsoDateTime.OfTimeSpan) + window (System.TimeSpan.FromMinutes(5.0) |> IsoDateTime.OfTimeSpan) + severity AlertSeverity.Warning + + single_resource_multiple_custom_metric_criteria [ + { + MetricNamespace = Some(ResourceType("MyCustomNamespace", "")) + MetricName = MetricsName "MyCustomMetric" + Threshold = 20 + Comparison = GreaterThan + Aggregation = Average } + ] + } - let template = arm { add_resources [ customAlert ] } - let jsn = template.Template |> Writer.toJson - let jobj = jsn |> Newtonsoft.Json.Linq.JObject.Parse + let template = arm { add_resources [ customAlert ] } + let jsn = template.Template |> Writer.toJson + let jobj = jsn |> Newtonsoft.Json.Linq.JObject.Parse - let allOf = - jobj.SelectToken($"resources[?(@.name=='{alertName}')].properties.criteria.allOf[0]") + let allOf = + jobj.SelectToken($"resources[?(@.name=='{alertName}')].properties.criteria.allOf[0]") - Expect.isNotNull allOf "allOf not found" - Expect.equal (allOf.Item("metricName").ToString()) "MyCustomMetric" "Wrong target metric namespace" + Expect.isNotNull allOf "allOf not found" + Expect.equal (allOf.Item("metricName").ToString()) "MyCustomMetric" "Wrong target metric namespace" - Expect.equal - (allOf.Item("metricNamespace").ToString()) - "MyCustomNamespace" - "Wrong target custom metric namespace" - } - ] + Expect.equal + (allOf.Item("metricNamespace").ToString()) + "MyCustomNamespace" + "Wrong target custom metric namespace" + } + ] diff --git a/src/Tests/AllTests.fs b/src/Tests/AllTests.fs index f5eb71ae7..11cdb6ae0 100644 --- a/src/Tests/AllTests.fs +++ b/src/Tests/AllTests.fs @@ -13,91 +13,86 @@ let notEnv a b = [] let allTests = testSequencedGroup "" - <| testList - "All Tests" - [ - testList - "Builders" - [ - AppGateway.tests - AppInsights.tests - AppInsightsAvailability.tests - if notEnv "BUILD_REASON" "PullRequest" then - AzCli.tests - AzureFirewall.tests - Bastion.tests - BingSearch.tests - Cdn.tests - CognitiveServices.tests - CommunicationServices.tests - ContainerApps.tests - ContainerGroup.tests - ContainerRegistry.tests - ContainerService.tests - Cosmos.tests - Databricks.tests - DedicatedHosts.tests - DeploymentScript.tests - DiagnosticSettings.tests - Disk.tests - Dns.tests - DnsResolver.tests - EventGrid.tests - EventHub.tests - ExpressRoute.tests - Functions.tests - IotHub.tests - Gallery.tests - ImageTemplate.tests - JsonRegression.tests - KeyVault.tests - Network.tests - LoadBalancer.tests - LogAnalytics.tests - LogicApps.tests - Maps.tests - NetworkSecurityGroup.tests - OperationsManagement.tests - PostgreSQL.tests - PrivateLink.tests - ResourceGroup.tests - RoleAssignment.tests - ServiceBus.tests - SignalR.tests - Sql.tests - StaticWebApp.tests - Storage.tests - TrafficManager.tests - Types.tests - VirtualHub.tests - VirtualMachine.tests - VirtualNetworkGateway.tests - VirtualWan.tests - WebApp.tests - Dashboards.tests - Alerts.tests - ServicePlan.tests - ] - testList - "Control" - [ - if - (hasEnv "TF_BUILD" "True" && notEnv "BUILD_REASON" "PullRequest") - || hasEnv "FARMER_E2E" "True" - then - AzCli.endToEndTests - Common.tests - Identity.tests - Template.tests - ] + <| testList "All Tests" [ + testList "Builders" [ + AppGateway.tests + AppInsights.tests + AppInsightsAvailability.tests + if notEnv "BUILD_REASON" "PullRequest" then + AzCli.tests + AzureFirewall.tests + Bastion.tests + BingSearch.tests + Cdn.tests + CognitiveServices.tests + CommunicationServices.tests + ContainerApps.tests + ContainerGroup.tests + ContainerRegistry.tests + ContainerService.tests + Cosmos.tests + Databricks.tests + DedicatedHosts.tests + DeploymentScript.tests + DiagnosticSettings.tests + Disk.tests + Dns.tests + DnsResolver.tests + EventGrid.tests + EventHub.tests + ExpressRoute.tests + Functions.tests + IotHub.tests + Gallery.tests + ImageTemplate.tests + JsonRegression.tests + KeyVault.tests + Network.tests + LoadBalancer.tests + LogAnalytics.tests + LogicApps.tests + Maps.tests + NetworkSecurityGroup.tests + OperationsManagement.tests + PostgreSQL.tests + PrivateLink.tests + ResourceGroup.tests + RoleAssignment.tests + ServiceBus.tests + SignalR.tests + Sql.tests + StaticWebApp.tests + Storage.tests + TrafficManager.tests + Types.tests + VirtualHub.tests + VirtualMachine.tests + VirtualNetworkGateway.tests + VirtualWan.tests + WebApp.tests + Dashboards.tests + Alerts.tests + ServicePlan.tests ] + testList "Control" [ + if + (hasEnv "TF_BUILD" "True" && notEnv "BUILD_REASON" "PullRequest") + || hasEnv "FARMER_E2E" "True" + then + AzCli.endToEndTests + Common.tests + Identity.tests + Template.tests + ] + ] [] let main _ = printfn "Running tests!" runTests - { defaultConfig with - verbosity = Logging.Info + { + defaultConfig with + verbosity = Logging.Info } allTests diff --git a/src/Tests/AppGateway.fs b/src/Tests/AppGateway.fs index d04989ad7..39423a18f 100644 --- a/src/Tests/AppGateway.fs +++ b/src/Tests/AppGateway.fs @@ -14,245 +14,229 @@ let client = new NetworkManagementClient(Uri "http://management.azure.com", TokenCredentials "NotNullOrWhiteSpace") let tests = - testList - "Application Gateway Tests" - [ - test "Empty basic app gateway" { - let ag = appGateway { name "ag" } - () - - let resource = - arm { add_resource ag } - |> findAzureResources - client.SerializationSettings - |> List.head - - Expect.equal resource.Name "ag" "Name did not match" - } - - test "Complex App gateway" { - let myNsg = - nsg { - name "agw-nsg" - - add_rules - [ - securityRule { - name "app-gw" - description "GatewayManager" - services [ NetworkService("GatewayManager", Range(65200us, 65535us)) ] - add_source_tag NetworkSecurity.TCP "GatewayManager" - add_destination_any - } - securityRule { - name "inet-gw" - description "Internet to gateway" - services [ "http", 80 ] - add_source_tag NetworkSecurity.TCP "Internet" - add_destination_network "10.28.0.0/24" - } - securityRule { - name "app-servers" - description "Internal app server access" - services [ "http", 80 ] - add_source_network NetworkSecurity.TCP "10.28.0.0/24" - add_destination_network "10.28.1.0/24" - } - ] + testList "Application Gateway Tests" [ + test "Empty basic app gateway" { + let ag = appGateway { name "ag" } + () + + let resource = + arm { add_resource ag } + |> findAzureResources + client.SerializationSettings + |> List.head + + Expect.equal resource.Name "ag" "Name did not match" + } + + test "Complex App gateway" { + let myNsg = nsg { + name "agw-nsg" + + add_rules [ + securityRule { + name "app-gw" + description "GatewayManager" + services [ NetworkService("GatewayManager", Range(65200us, 65535us)) ] + add_source_tag NetworkSecurity.TCP "GatewayManager" + add_destination_any } - - let net = - vnet { - name "agw-vnet" - - build_address_spaces - [ - addressSpace { - space "10.28.0.0/16" - - subnets - [ - subnetSpec { - name "gw" - size 24 - network_security_group myNsg - } - subnetSpec { - name "apps" - size 24 - network_security_group myNsg - add_delegations [ SubnetDelegationService.ContainerGroups ] - } - ] - } - ] + securityRule { + name "inet-gw" + description "Internet to gateway" + services [ "http", 80 ] + add_source_tag NetworkSecurity.TCP "Internet" + add_destination_network "10.28.0.0/24" } - - let msi = createUserAssignedIdentity "agw-msi" - - let backendPoolName = ResourceName "agw-be-pool" - - let myAppGateway = - let gwIp = - gatewayIp { - name "app-gw-ip" - link_to_subnet net.Name net.Subnets.[0].Name - } - - let frontendIp = - frontendIp { - name "app-gw-fe-ip" - public_ip "agp-gw-pip" - } - - let frontendPort = - frontendPort { - name "port-80" - port 80 - } - - let listener = - httpListener { - name "http-listener" - frontend_ip frontendIp - frontend_port frontendPort - backend_pool backendPoolName.Value - } - - let backendPool = - appGatewayBackendAddressPool { - name backendPoolName.Value - add_backend_addresses [ backend_ip_address "10.28.1.4"; backend_ip_address "10.28.1.5" ] - } - - let healthProbe = - appGatewayProbe { - name "agw-probe" - host "localhost" - path "/" - port 80 - protocol Protocol.Http - } - - let backendSettings = - backendHttpSettings { - name "bp-default-web-80-web-80" - port 80 - probe healthProbe - protocol Protocol.Http - request_timeout 10 - } - - let routingRule = - basicRequestRoutingRule { - name "web-front-to-services-back" - http_listener listener - backend_address_pool backendPool - backend_http_settings backendSettings - } - - appGateway { - name "app-gw" - sku_capacity 2 - add_identity msi - add_ip_configs [ gwIp ] - add_frontends [ frontendIp ] - add_frontend_ports [ frontendPort ] - add_http_listeners [ listener ] - add_backend_address_pools [ backendPool ] - add_backend_http_settings_collection [ backendSettings ] - add_request_routing_rules [ routingRule ] - add_probes [ healthProbe ] - depends_on myNsg - depends_on net + securityRule { + name "app-servers" + description "Internal app server access" + services [ "http", 80 ] + add_source_network NetworkSecurity.TCP "10.28.0.0/24" + add_destination_network "10.28.1.0/24" } + ] + } - let deployment = - arm { - location Location.EastUS - add_resources [ msi; net; myNsg; myAppGateway ] + let net = vnet { + name "agw-vnet" + + build_address_spaces [ + addressSpace { + space "10.28.0.0/16" + + subnets [ + subnetSpec { + name "gw" + size 24 + network_security_group myNsg + } + subnetSpec { + name "apps" + size 24 + network_security_group myNsg + add_delegations [ SubnetDelegationService.ContainerGroups ] + } + ] } + ] + } - deployment.Template |> Writer.toJson |> ignore - - let resource = - deployment - |> findAzureResources - client.SerializationSettings - |> List.item 3 - - Expect.equal resource.Name "app-gw" "Name did not match" - Expect.hasLength resource.BackendAddressPools 1 "Expecting 1 backend address pool" - let backendPool = resource.BackendAddressPools.[0] - Expect.equal backendPool.Name "agw-be-pool" "Backend address pool name did not match" - Expect.hasLength backendPool.BackendAddresses 2 "Incorrect number of addresses in backend pool" - - Expect.equal - backendPool.BackendAddresses.[0].IpAddress - "10.28.1.4" - "Backend address pool has incorrect IP in pool - item 1" - - Expect.equal - backendPool.BackendAddresses.[1].IpAddress - "10.28.1.5" - "Backend address pool has incorrect IP in pool - item 2" - - Expect.hasLength resource.BackendHttpSettingsCollection 1 "Expecting 1 backend http setting" - let backendSettings = resource.BackendHttpSettingsCollection.[0] - Expect.equal backendSettings.Name "bp-default-web-80-web-80" "Backend http settings name did not match" - Expect.hasLength resource.FrontendPorts 1 "Expecting 1 frontend port" - let feport = resource.FrontendPorts.[0] - Expect.equal feport.Name "port-80" "Frontend port name did not match" - Expect.equal feport.Port (Nullable 80) "Frontend port value did not match" - Expect.hasLength resource.FrontendIPConfigurations 1 "Expecting 1 frontend IP config" - let feipconf = resource.FrontendIPConfigurations.[0] - Expect.equal feipconf.Name "app-gw-fe-ip" "Frontend IP config name did not match" - Expect.hasLength resource.GatewayIPConfigurations 1 "Expecting 1 gateway IP" - let gwipconf = resource.GatewayIPConfigurations.[0] - Expect.equal gwipconf.Name "app-gw-ip" "Gateway IP subnet ID did not match" - - Expect.equal - gwipconf.Subnet.Id - "[resourceId('Microsoft.Network/virtualNetworks/subnets', 'agw-vnet', 'gw')]" - "Gateway IP subnet ID did not match" - - Expect.hasLength resource.HttpListeners 1 "Expecting 1 http listener" - let httpListener = resource.HttpListeners.[0] - Expect.equal httpListener.Name "http-listener" "Listener name did not match" - - Expect.equal - httpListener.FrontendPort.Id - "[resourceId('Microsoft.Network/applicationGateways/frontendPorts', 'app-gw', 'port-80')]" - "Listener port did not match" - - Expect.equal - httpListener.FrontendIPConfiguration.Id - "[resourceId('Microsoft.Network/applicationGateways/frontendIPConfigurations', 'app-gw', 'app-gw-fe-ip')]" - "Listener ipconfig did not match" - - Expect.hasLength resource.Probes 1 "Expecting 1 health probe" - let probe = resource.Probes.[0] - Expect.equal probe.Name "agw-probe" "Probe name did not match" - Expect.equal probe.Host "localhost" "Probe host did not match" - Expect.equal probe.Port (Nullable 80) "Probe port did not match" - Expect.equal probe.Path "/" "Probe path did not match" - Expect.hasLength resource.RequestRoutingRules 1 "Expecting 1 request routing rule" - let routingRule = resource.RequestRoutingRules.[0] - Expect.equal routingRule.Name "web-front-to-services-back" "Routing rule name did not match" - - Expect.equal - routingRule.HttpListener.Id - "[resourceId('Microsoft.Network/applicationGateways/httpListeners', 'app-gw', 'http-listener')]" - "Routing rule listener id did not match" - - Expect.equal - routingRule.BackendAddressPool.Id - "[resourceId('Microsoft.Network/applicationGateways/backendAddressPools', 'app-gw', 'agw-be-pool')]" - "Routing rule pool did not match" - - Expect.equal - routingRule.BackendHttpSettings.Id - "[resourceId('Microsoft.Network/applicationGateways/backendHttpSettingsCollection', 'app-gw', 'bp-default-web-80-web-80')]" - "Routing rule http settings did not match" + let msi = createUserAssignedIdentity "agw-msi" + + let backendPoolName = ResourceName "agw-be-pool" + + let myAppGateway = + let gwIp = gatewayIp { + name "app-gw-ip" + link_to_subnet net.Name net.Subnets.[0].Name + } + + let frontendIp = frontendIp { + name "app-gw-fe-ip" + public_ip "agp-gw-pip" + } + + let frontendPort = frontendPort { + name "port-80" + port 80 + } + + let listener = httpListener { + name "http-listener" + frontend_ip frontendIp + frontend_port frontendPort + backend_pool backendPoolName.Value + } + + let backendPool = appGatewayBackendAddressPool { + name backendPoolName.Value + add_backend_addresses [ backend_ip_address "10.28.1.4"; backend_ip_address "10.28.1.5" ] + } + + let healthProbe = appGatewayProbe { + name "agw-probe" + host "localhost" + path "/" + port 80 + protocol Protocol.Http + } + + let backendSettings = backendHttpSettings { + name "bp-default-web-80-web-80" + port 80 + probe healthProbe + protocol Protocol.Http + request_timeout 10 + } + + let routingRule = basicRequestRoutingRule { + name "web-front-to-services-back" + http_listener listener + backend_address_pool backendPool + backend_http_settings backendSettings + } + + appGateway { + name "app-gw" + sku_capacity 2 + add_identity msi + add_ip_configs [ gwIp ] + add_frontends [ frontendIp ] + add_frontend_ports [ frontendPort ] + add_http_listeners [ listener ] + add_backend_address_pools [ backendPool ] + add_backend_http_settings_collection [ backendSettings ] + add_request_routing_rules [ routingRule ] + add_probes [ healthProbe ] + depends_on myNsg + depends_on net + } + + let deployment = arm { + location Location.EastUS + add_resources [ msi; net; myNsg; myAppGateway ] } - ] + + deployment.Template |> Writer.toJson |> ignore + + let resource = + deployment + |> findAzureResources + client.SerializationSettings + |> List.item 3 + + Expect.equal resource.Name "app-gw" "Name did not match" + Expect.hasLength resource.BackendAddressPools 1 "Expecting 1 backend address pool" + let backendPool = resource.BackendAddressPools.[0] + Expect.equal backendPool.Name "agw-be-pool" "Backend address pool name did not match" + Expect.hasLength backendPool.BackendAddresses 2 "Incorrect number of addresses in backend pool" + + Expect.equal + backendPool.BackendAddresses.[0].IpAddress + "10.28.1.4" + "Backend address pool has incorrect IP in pool - item 1" + + Expect.equal + backendPool.BackendAddresses.[1].IpAddress + "10.28.1.5" + "Backend address pool has incorrect IP in pool - item 2" + + Expect.hasLength resource.BackendHttpSettingsCollection 1 "Expecting 1 backend http setting" + let backendSettings = resource.BackendHttpSettingsCollection.[0] + Expect.equal backendSettings.Name "bp-default-web-80-web-80" "Backend http settings name did not match" + Expect.hasLength resource.FrontendPorts 1 "Expecting 1 frontend port" + let feport = resource.FrontendPorts.[0] + Expect.equal feport.Name "port-80" "Frontend port name did not match" + Expect.equal feport.Port (Nullable 80) "Frontend port value did not match" + Expect.hasLength resource.FrontendIPConfigurations 1 "Expecting 1 frontend IP config" + let feipconf = resource.FrontendIPConfigurations.[0] + Expect.equal feipconf.Name "app-gw-fe-ip" "Frontend IP config name did not match" + Expect.hasLength resource.GatewayIPConfigurations 1 "Expecting 1 gateway IP" + let gwipconf = resource.GatewayIPConfigurations.[0] + Expect.equal gwipconf.Name "app-gw-ip" "Gateway IP subnet ID did not match" + + Expect.equal + gwipconf.Subnet.Id + "[resourceId('Microsoft.Network/virtualNetworks/subnets', 'agw-vnet', 'gw')]" + "Gateway IP subnet ID did not match" + + Expect.hasLength resource.HttpListeners 1 "Expecting 1 http listener" + let httpListener = resource.HttpListeners.[0] + Expect.equal httpListener.Name "http-listener" "Listener name did not match" + + Expect.equal + httpListener.FrontendPort.Id + "[resourceId('Microsoft.Network/applicationGateways/frontendPorts', 'app-gw', 'port-80')]" + "Listener port did not match" + + Expect.equal + httpListener.FrontendIPConfiguration.Id + "[resourceId('Microsoft.Network/applicationGateways/frontendIPConfigurations', 'app-gw', 'app-gw-fe-ip')]" + "Listener ipconfig did not match" + + Expect.hasLength resource.Probes 1 "Expecting 1 health probe" + let probe = resource.Probes.[0] + Expect.equal probe.Name "agw-probe" "Probe name did not match" + Expect.equal probe.Host "localhost" "Probe host did not match" + Expect.equal probe.Port (Nullable 80) "Probe port did not match" + Expect.equal probe.Path "/" "Probe path did not match" + Expect.hasLength resource.RequestRoutingRules 1 "Expecting 1 request routing rule" + let routingRule = resource.RequestRoutingRules.[0] + Expect.equal routingRule.Name "web-front-to-services-back" "Routing rule name did not match" + + Expect.equal + routingRule.HttpListener.Id + "[resourceId('Microsoft.Network/applicationGateways/httpListeners', 'app-gw', 'http-listener')]" + "Routing rule listener id did not match" + + Expect.equal + routingRule.BackendAddressPool.Id + "[resourceId('Microsoft.Network/applicationGateways/backendAddressPools', 'app-gw', 'agw-be-pool')]" + "Routing rule pool did not match" + + Expect.equal + routingRule.BackendHttpSettings.Id + "[resourceId('Microsoft.Network/applicationGateways/backendHttpSettingsCollection', 'app-gw', 'bp-default-web-80-web-80')]" + "Routing rule http settings did not match" + } + ] diff --git a/src/Tests/AppInsights.fs b/src/Tests/AppInsights.fs index b9b98a8f5..8dc7d8e35 100644 --- a/src/Tests/AppInsights.fs +++ b/src/Tests/AppInsights.fs @@ -7,73 +7,70 @@ open Farmer.Builders.LogAnalytics open Newtonsoft.Json.Linq let tests = - testList - "AppInsights" - [ - test "Creates keys on an AI instance correctly" { - let ai = appInsights { name "foo" } + testList "AppInsights" [ + test "Creates keys on an AI instance correctly" { + let ai = appInsights { name "foo" } - Expect.equal - ai.InstrumentationKey.Owner.Value.ArmExpression.Value - "resourceId('Microsoft.Insights/components', 'foo')" - "Incorrect owner" + Expect.equal + ai.InstrumentationKey.Owner.Value.ArmExpression.Value + "resourceId('Microsoft.Insights/components', 'foo')" + "Incorrect owner" - Expect.equal - ai.InstrumentationKey.Value - ("reference(resourceId('Microsoft.Insights/components', 'foo'), '2014-04-01').InstrumentationKey") - "Incorrect Value" - } + 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 "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") - ) + 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" - } + 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" } + test "Creates LA-enabled workspace" { + let workspace = logAnalytics { name "la" } - let ai = - appInsights { - name "ai" - log_analytics_workspace workspace - } + let ai = appInsights { + name "ai" + log_analytics_workspace workspace + } - let deployment = arm { add_resources [ workspace; ai ] } + let deployment = arm { add_resources [ workspace; ai ] } - let json = deployment.Template |> Writer.toJson |> JObject.Parse - let select query = json.SelectToken(query).ToString() + 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')].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 (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.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" - } - ] + Expect.sequenceEqual + (json.SelectToken("resources[?(@.name=='ai')].dependsOn").Children() + |> Seq.map string + |> Seq.toArray) + [ "[resourceId('Microsoft.OperationalInsights/workspaces', 'la')]" ] + "Incorrect dependencies" + } + ] diff --git a/src/Tests/AvailabilityTests.fs b/src/Tests/AvailabilityTests.fs index dd7829771..f619cd9cf 100644 --- a/src/Tests/AvailabilityTests.fs +++ b/src/Tests/AvailabilityTests.fs @@ -5,37 +5,34 @@ open Farmer open Farmer.Builders let tests = - testList - "AvailabilityTests" - [ - - test "Create an availability test" { - let ai = appInsights { name "ai" } - - let availabilityTest = - availabilityTest { - name "avTest" - link_to_app_insights ai - timeout 60 - frequency 800 - locations [ AvailabilityTest.TestSiteLocation.CentralUS ] - web_test ("https://google.com" |> System.Uri |> AvailabilityTest.WebsiteUrl) - } - - let template = arm { add_resources [ availabilityTest; ai ] } - let jsn = template.Template |> Writer.toJson - let jobj = jsn |> Newtonsoft.Json.Linq.JObject.Parse - - let hasWebTest = - jobj.SelectToken("resources[?(@.name=='avTest')].properties.Configuration.WebTest") - - Expect.isNotNull hasWebTest "WebTest context missing" - - let availabilityLocation = - jobj.SelectToken("resources[?(@.name=='avTest')].properties.Locations[0].Id") - - Expect.equal (availabilityLocation.ToString()) "us-fl-mia-edge" "WebTest location incorrect" - let dependsAi = jobj.SelectToken("resources[?(@.name=='avTest')].dependsOn") - Expect.isNotNull dependsAi "AppInsights dependency missing" + testList "AvailabilityTests" [ + + test "Create an availability test" { + let ai = appInsights { name "ai" } + + let availabilityTest = availabilityTest { + name "avTest" + link_to_app_insights ai + timeout 60 + frequency 800 + locations [ AvailabilityTest.TestSiteLocation.CentralUS ] + web_test ("https://google.com" |> System.Uri |> AvailabilityTest.WebsiteUrl) } - ] + + let template = arm { add_resources [ availabilityTest; ai ] } + let jsn = template.Template |> Writer.toJson + let jobj = jsn |> Newtonsoft.Json.Linq.JObject.Parse + + let hasWebTest = + jobj.SelectToken("resources[?(@.name=='avTest')].properties.Configuration.WebTest") + + Expect.isNotNull hasWebTest "WebTest context missing" + + let availabilityLocation = + jobj.SelectToken("resources[?(@.name=='avTest')].properties.Locations[0].Id") + + Expect.equal (availabilityLocation.ToString()) "us-fl-mia-edge" "WebTest location incorrect" + let dependsAi = jobj.SelectToken("resources[?(@.name=='avTest')].dependsOn") + Expect.isNotNull dependsAi "AppInsights dependency missing" + } + ] diff --git a/src/Tests/AzCli.fs b/src/Tests/AzCli.fs index 6fece9bdc..4e6c965ae 100644 --- a/src/Tests/AzCli.fs +++ b/src/Tests/AzCli.fs @@ -19,38 +19,36 @@ let deployTo resourceGroupName parameters (deployment: IDeploymentSource) = | _, Error e -> raiseFarmer $"Something went wrong during the delete: {e}" let endToEndTests = - testList - "End to end tests" - [ - test "Deploys and deletes a resource group" { - let resourceGroupName = sprintf "farmer-integration-test-delete-%O" (Guid.NewGuid()) - arm { location Location.NorthEurope } |> deployTo resourceGroupName [] - } - - test "If parameters are missing, deployment is immediately rejected" { - let deployment = createSimpleDeployment [ "p1" ] - let result = deployment |> Deploy.tryExecute "sample-rg" [] - Expect.equal result (Error "The following parameters are missing: p1. Please add them.") "" - } - ] + testList "End to end tests" [ + test "Deploys and deletes a resource group" { + let resourceGroupName = sprintf "farmer-integration-test-delete-%O" (Guid.NewGuid()) + arm { location Location.NorthEurope } |> deployTo resourceGroupName [] + } + + test "If parameters are missing, deployment is immediately rejected" { + let deployment = createSimpleDeployment [ "p1" ] + let result = deployment |> Deploy.tryExecute "sample-rg" [] + Expect.equal result (Error "The following parameters are missing: p1. Please add them.") "" + } + ] let tests = - testList - "Azure CLI" - [ - test "Can connect to Az CLI" { - match Deploy.Az.checkVersion Deploy.Az.MinimumVersion with - | Ok _ -> () - | Error msg -> raiseFarmer $"Version check failed: {msg}" - } - - test "Az output is always JSON" { - // account list always defaults to table, regardless of defaults? - Deploy.Az.az "account list --all" - |> Result.map - Serialization.ofJson<{| id: Guid - tenantId: Guid - isDefault: bool |} array> - |> ignore - } - ] + testList "Azure CLI" [ + test "Can connect to Az CLI" { + match Deploy.Az.checkVersion Deploy.Az.MinimumVersion with + | Ok _ -> () + | Error msg -> raiseFarmer $"Version check failed: {msg}" + } + + test "Az output is always JSON" { + // account list always defaults to table, regardless of defaults? + Deploy.Az.az "account list --all" + |> Result.map + Serialization.ofJson<{| + id: Guid + tenantId: Guid + isDefault: bool + |} array> + |> ignore + } + ] diff --git a/src/Tests/AzureFirewall.fs b/src/Tests/AzureFirewall.fs index e5f3aebb4..edea3c919 100644 --- a/src/Tests/AzureFirewall.fs +++ b/src/Tests/AzureFirewall.fs @@ -8,80 +8,72 @@ open Farmer.Builders open Newtonsoft.Json.Linq let tests = - testList - "Azure Firewall" - [ - test "Link to builder" { - let existingFwPolicy = - { new IBuilder with - member _.ResourceId = azureFirewallPolicies.resourceId "existing-firewall-policy" - member _.BuildResources _ = [] - } + testList "Azure Firewall" [ + test "Link to builder" { + let existingFwPolicy = + { new IBuilder with + member _.ResourceId = azureFirewallPolicies.resourceId "existing-firewall-policy" + member _.BuildResources _ = [] + } - let vwan = - vwan { - name "my-vwan" - standard_vwan - } + 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 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 - link_to_firewall_policy existingFwPolicy - } + let fw = azureFirewall { + name "azfw" + sku AzureFirewall.SkuName.AZFW_Hub AzureFirewall.SkuTier.Standard + public_ip_reservation_count 2 + 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" + 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.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.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'" + Expect.isNull zones "Should not have a value for 'zones'" + } + test "Zonal firewall" { + let vwan = vwan { + name "my-vwan" + standard_vwan } - 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 vhub = vhub { + name "my-vhub" + address_prefix (IPAddressCidr.parse "100.73.255.0/24") + link_to_vwan vwan + } - 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." + 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/Bastion.fs b/src/Tests/Bastion.fs index ff1de28bf..f7098801c 100644 --- a/src/Tests/Bastion.fs +++ b/src/Tests/Bastion.fs @@ -14,47 +14,43 @@ let client = new NetworkManagementClient(Uri "http://management.azure.com", TokenCredentials "NotNullOrWhiteSpace") let tests = - testList - "Bastion Host" - [ - test "Bastion host is attached to subnet named AzureBastionSubnet" { - - let resources = - arm { - location Location.EastUS - - add_resources - [ - bastion { - name "my-bastion-host" - vnet "private-network" + testList "Bastion Host" [ + test "Bastion host is attached to subnet named AzureBastionSubnet" { + + let resources = + arm { + location Location.EastUS + + add_resources [ + bastion { + name "my-bastion-host" + vnet "private-network" + } + vnet { + name "private-network" + add_address_spaces [ "10.1.0.0/16" ] + + add_subnets [ + subnet { + name "default" + prefix "10.1.0.0/24" } - vnet { - name "private-network" - add_address_spaces [ "10.1.0.0/16" ] - - add_subnets - [ - subnet { - name "default" - prefix "10.1.0.0/24" - } - subnet { - name "AzureBastionSubnet" - prefix "10.1.250.0/27" - } - ] + subnet { + name "AzureBastionSubnet" + prefix "10.1.250.0/27" } ] - } - |> findAzureResources client.SerializationSettings - |> Array.ofList - - Expect.equal resources.[1].Name "my-bastion-host" "Account name is wrong" - - Expect.equal - resources.[1].IpConfigurations.[0].Subnet.Id - "[resourceId('Microsoft.Network/virtualNetworks/subnets', 'private-network', 'AzureBastionSubnet')]" - "Subnet name must be 'AzureBastionSubnet'" - } - ] + } + ] + } + |> findAzureResources client.SerializationSettings + |> Array.ofList + + Expect.equal resources.[1].Name "my-bastion-host" "Account name is wrong" + + Expect.equal + resources.[1].IpConfigurations.[0].Subnet.Id + "[resourceId('Microsoft.Network/virtualNetworks/subnets', 'private-network', 'AzureBastionSubnet')]" + "Subnet name must be 'AzureBastionSubnet'" + } + ] diff --git a/src/Tests/BingSearch.fs b/src/Tests/BingSearch.fs index c601fcdb5..9a6e62885 100644 --- a/src/Tests/BingSearch.fs +++ b/src/Tests/BingSearch.fs @@ -10,48 +10,47 @@ open TestHelpers let private asJson (arm: IArmResource) = arm.JsonModel - |> convertTo<{| kind: string - properties: {| statisticsEnabled: bool |} |}> + |> convertTo<{| + kind: string + properties: {| statisticsEnabled: bool |} + |} > let tests = - testList - "Bing Search" - [ - test "Basic test" { - let tags = [ "a", "1"; "b", "2" ] + testList "Bing Search" [ + test "Basic test" { + let tags = [ "a", "1"; "b", "2" ] - let swa = - bingSearch { - name "test" - sku S0 - add_tags tags - statistics Enabled - } - - let baseArm = (swa :> IBuilder).BuildResources(Location.WestEurope).[0] - let bsArm = baseArm :?> BingSearch.Accounts - let jsonModel = asJson baseArm - Expect.equal bsArm.Name (ResourceName "test") "Name" - Expect.equal bsArm.Location Location.WestEurope "Location" - Expect.isTrue jsonModel.properties.statisticsEnabled "Statistics enabled in json" - Expect.equal bsArm.Statistics FeatureFlag.Enabled "Statistics enabled" - Expect.equal bsArm.Sku S0 "Sku" - Expect.equal jsonModel.kind "Bing.Search.v7" "kind" - Expect.equal bsArm.Tags (Map tags) "Tags" + let swa = bingSearch { + name "test" + sku S0 + add_tags tags + statistics Enabled } - test "Default options test" { - let swa = bingSearch { name "test" } + let baseArm = (swa :> IBuilder).BuildResources(Location.WestEurope).[0] + let bsArm = baseArm :?> BingSearch.Accounts + let jsonModel = asJson baseArm + Expect.equal bsArm.Name (ResourceName "test") "Name" + Expect.equal bsArm.Location Location.WestEurope "Location" + Expect.isTrue jsonModel.properties.statisticsEnabled "Statistics enabled in json" + Expect.equal bsArm.Statistics FeatureFlag.Enabled "Statistics enabled" + Expect.equal bsArm.Sku S0 "Sku" + Expect.equal jsonModel.kind "Bing.Search.v7" "kind" + Expect.equal bsArm.Tags (Map tags) "Tags" + } - let baseArm = (swa :> IBuilder).BuildResources(Location.WestEurope).[0] - let bsArm = baseArm :?> BingSearch.Accounts - let jsonModel = asJson baseArm - Expect.equal bsArm.Name (ResourceName "test") "Name" - Expect.equal bsArm.Location Location.WestEurope "Location" - Expect.isFalse jsonModel.properties.statisticsEnabled "Statistics not enabled in json" - Expect.equal bsArm.Statistics Disabled "Statistics not enabled" - Expect.equal bsArm.Sku F1 "Sku" - Expect.equal jsonModel.kind "Bing.Search.v7" "kind" - Expect.isEmpty bsArm.Tags "Tags" - } - ] + test "Default options test" { + let swa = bingSearch { name "test" } + + let baseArm = (swa :> IBuilder).BuildResources(Location.WestEurope).[0] + let bsArm = baseArm :?> BingSearch.Accounts + let jsonModel = asJson baseArm + Expect.equal bsArm.Name (ResourceName "test") "Name" + Expect.equal bsArm.Location Location.WestEurope "Location" + Expect.isFalse jsonModel.properties.statisticsEnabled "Statistics not enabled in json" + Expect.equal bsArm.Statistics Disabled "Statistics not enabled" + Expect.equal bsArm.Sku F1 "Sku" + Expect.equal jsonModel.kind "Bing.Search.v7" "kind" + Expect.isEmpty bsArm.Tags "Tags" + } + ] diff --git a/src/Tests/Cdn.fs b/src/Tests/Cdn.fs index a4156b930..8f5fee04a 100644 --- a/src/Tests/Cdn.fs +++ b/src/Tests/Cdn.fs @@ -28,126 +28,118 @@ let asAzureResource (cdn: CdnConfig) = r let tests = - testList - "CDN tests" - [ - test "CDN profile is created correctly" { - let profile = - cdn { - name "test-cdn" - sku Cdn.Sku.Premium_Verizon - } - |> asAzureResource - - Expect.equal profile.Name "test-cdn" "Incorrect name" - Expect.equal profile.Sku.Name "Premium_Verizon" "Incorrect SKU" - } - test "Endpoint is created with correct defaults" { - let endpoint: Endpoint = - cdn { - name "test-cdn" - - add_endpoints - [ - endpoint { - name "test-endpoint" - origin "origin" + testList "CDN tests" [ + test "CDN profile is created correctly" { + let profile = + cdn { + name "test-cdn" + sku Cdn.Sku.Premium_Verizon + } + |> asAzureResource + + Expect.equal profile.Name "test-cdn" "Incorrect name" + Expect.equal profile.Sku.Name "Premium_Verizon" "Incorrect SKU" + } + test "Endpoint is created with correct defaults" { + let endpoint: Endpoint = + cdn { + name "test-cdn" + + add_endpoints [ + endpoint { + name "test-endpoint" + origin "origin" + } + ] + } + |> getResourceAtIndex 1 + + Expect.equal endpoint.Name "test-cdn/test-endpoint" "Incorrect name" + Expect.equal endpoint.Origins.[0].HostName "origin" "Incorrect origin" + Expect.isFalse endpoint.IsCompressionEnabled.Value "Compression should be disabled by default" + Expect.equal endpoint.OptimizationType "GeneralWebDelivery" "Optimisation type should be web by default" + + Expect.equal + endpoint.QueryStringCachingBehavior.Value + QueryStringCachingBehavior.UseQueryString + "QSCB should be UseQueryString by default" + } + + test "Endpoint settings are correctly cascaded" { + let endpoint: Endpoint = + cdn { + name "test-cdn" + + add_endpoints [ + endpoint { + origin "origin" + add_compressed_content [ "a"; "b"; "c" ] + optimise_for GeneralMediaStreaming + query_string_caching_behaviour QueryStringCachingBehaviour.BypassCaching + disable_http + disable_https + } + ] + } + |> getResourceAtIndex 1 + + Expect.equal endpoint.Name "test-cdn/origin-endpoint" "Incorrect endpoint name" + + Expect.equal + (endpoint.ContentTypesToCompress |> Set) + (Set [ "a"; "b"; "c" ]) + "Incorrect content compression types" + + Expect.isTrue endpoint.IsCompressionEnabled.Value "Compression should be enabled when content is supplied" + + Expect.equal endpoint.OptimizationType "GeneralMediaStreaming" "Optimisation type" + + Expect.equal + endpoint.QueryStringCachingBehavior.Value + QueryStringCachingBehavior.BypassCaching + "Query String Caching Behaviour" + + Expect.isFalse endpoint.IsHttpAllowed.Value "HTTP should be disabled" + Expect.isFalse endpoint.IsHttpsAllowed.Value "HTTPS should be disabled" + } + + test "Custom Domain is correctly set" { + let domain: CustomDomain = + cdn { + name "test" + add_endpoints [ endpoint { custom_domain_name "www.compositional-it.com" } ] + } + |> getResourceAtIndex 2 + + Expect.equal domain.HostName "www.compositional-it.com" "Custom Domain name is wrong" + } + + test "Rules are correctly created" { + let endpoint: Endpoint = + cdn { + name "test" + + add_endpoints [ + endpoint { + add_rules [ + cdnRule { + name "test-rule" + order 2 + when_request_body Contains [ "content" ] ToLowercase + when_device_type EqualityOperator.Equals Mobile + modify_response_header Append "headerName" "headerValue" + cache_expiration BypassCache } ] - } - |> getResourceAtIndex 1 - - Expect.equal endpoint.Name "test-cdn/test-endpoint" "Incorrect name" - Expect.equal endpoint.Origins.[0].HostName "origin" "Incorrect origin" - Expect.isFalse endpoint.IsCompressionEnabled.Value "Compression should be disabled by default" - Expect.equal endpoint.OptimizationType "GeneralWebDelivery" "Optimisation type should be web by default" - - Expect.equal - endpoint.QueryStringCachingBehavior.Value - QueryStringCachingBehavior.UseQueryString - "QSCB should be UseQueryString by default" - } - - test "Endpoint settings are correctly cascaded" { - let endpoint: Endpoint = - cdn { - name "test-cdn" - - add_endpoints - [ - endpoint { - origin "origin" - add_compressed_content [ "a"; "b"; "c" ] - optimise_for GeneralMediaStreaming - query_string_caching_behaviour QueryStringCachingBehaviour.BypassCaching - disable_http - disable_https - } - ] - } - |> getResourceAtIndex 1 - - Expect.equal endpoint.Name "test-cdn/origin-endpoint" "Incorrect endpoint name" - - Expect.equal - (endpoint.ContentTypesToCompress |> Set) - (Set [ "a"; "b"; "c" ]) - "Incorrect content compression types" - - Expect.isTrue - endpoint.IsCompressionEnabled.Value - "Compression should be enabled when content is supplied" - - Expect.equal endpoint.OptimizationType "GeneralMediaStreaming" "Optimisation type" - - Expect.equal - endpoint.QueryStringCachingBehavior.Value - QueryStringCachingBehavior.BypassCaching - "Query String Caching Behaviour" - - Expect.isFalse endpoint.IsHttpAllowed.Value "HTTP should be disabled" - Expect.isFalse endpoint.IsHttpsAllowed.Value "HTTPS should be disabled" - } - - test "Custom Domain is correctly set" { - let domain: CustomDomain = - cdn { - name "test" - add_endpoints [ endpoint { custom_domain_name "www.compositional-it.com" } ] - } - |> getResourceAtIndex 2 - - Expect.equal domain.HostName "www.compositional-it.com" "Custom Domain name is wrong" - } - - test "Rules are correctly created" { - let endpoint: Endpoint = - cdn { - name "test" - - add_endpoints - [ - endpoint { - add_rules - [ - cdnRule { - name "test-rule" - order 2 - when_request_body Contains [ "content" ] ToLowercase - when_device_type EqualityOperator.Equals Mobile - modify_response_header Append "headerName" "headerValue" - cache_expiration BypassCache - } - ] - } - ] - } - |> getResourceAtIndex 1 - - let rule = endpoint.DeliveryPolicy.Rules.Item 0 - Expect.equal rule.Name "test-rule" "Incorrect rule name" - Expect.equal rule.Order 2 "Incorrect rule order" - Expect.hasLength rule.Conditions 2 "Incorrect number of conditions" - Expect.hasLength rule.Actions 2 "Incorrect number of actions" - } - ] + } + ] + } + |> getResourceAtIndex 1 + + let rule = endpoint.DeliveryPolicy.Rules.Item 0 + Expect.equal rule.Name "test-rule" "Incorrect rule name" + Expect.equal rule.Order 2 "Incorrect rule order" + Expect.hasLength rule.Conditions 2 "Incorrect number of conditions" + Expect.hasLength rule.Actions 2 "Incorrect number of actions" + } + ] diff --git a/src/Tests/CognitiveServices.fs b/src/Tests/CognitiveServices.fs index 0ee4c1faf..7938e0021 100644 --- a/src/Tests/CognitiveServices.fs +++ b/src/Tests/CognitiveServices.fs @@ -15,53 +15,50 @@ let getResourceAtIndex o = o |> getResourceAtIndex dummyClient.SerializationSettings let tests = - testList - "Cognitive Services" - [ - test "Basic Cognitive Services test" { - let service = - cognitiveServices { - name "test" - api TextAnalytics - sku S0 - add_tags [ "a", "1"; "b", "2" ] - } + testList "Cognitive Services" [ + test "Basic Cognitive Services test" { + let service = cognitiveServices { + name "test" + api TextAnalytics + sku S0 + add_tags [ "a", "1"; "b", "2" ] + } - let model: Models.CognitiveServicesAccount = service |> getResourceAtIndex 0 + let model: Models.CognitiveServicesAccount = service |> getResourceAtIndex 0 - Expect.equal model.Name "test" "Name is wrong" - Expect.equal model.Kind "TextAnalytics" "Kind is wrong" - Expect.equal model.Sku.Name "S0" "Sku is wrong" + Expect.equal model.Name "test" "Name is wrong" + Expect.equal model.Kind "TextAnalytics" "Kind is wrong" + Expect.equal model.Sku.Name "S0" "Sku is wrong" - Expect.sequenceEqual - (model.Tags |> Seq.map (fun x -> x.Key, x.Value)) - [ "a", "1"; "b", "2" ] - "Tags are wrong" - } + Expect.sequenceEqual + (model.Tags |> Seq.map (fun x -> x.Key, x.Value)) + [ "a", "1"; "b", "2" ] + "Tags are wrong" + } - test "Key is correctly calculated on a CS instance" { - let service = cognitiveServices { name "test" } + test "Key is correctly calculated on a CS instance" { + let service = cognitiveServices { name "test" } - Expect.equal - service.Key.Owner.Value.ArmExpression.Value - "resourceId('Microsoft.CognitiveServices/accounts', 'test')" - "Owner is wrong" + Expect.equal + service.Key.Owner.Value.ArmExpression.Value + "resourceId('Microsoft.CognitiveServices/accounts', 'test')" + "Owner is wrong" - Expect.equal - service.Key.Value - "listKeys(resourceId('Microsoft.CognitiveServices/accounts', 'test'), '2017-04-18').key1" - "Key is wrong" - } + Expect.equal + service.Key.Value + "listKeys(resourceId('Microsoft.CognitiveServices/accounts', 'test'), '2017-04-18').key1" + "Key is wrong" + } - test "Key is correctly calculated with a resource group" { - let key = - CognitiveServices.getKey ( - ResourceId.create (Arm.CognitiveServices.accounts, ResourceName "test", "resource group") - ) + test "Key is correctly calculated with a resource group" { + let key = + CognitiveServices.getKey ( + ResourceId.create (Arm.CognitiveServices.accounts, ResourceName "test", "resource group") + ) - Expect.equal - key.Value - "listKeys(resourceId('resource group', 'Microsoft.CognitiveServices/accounts', 'test'), '2017-04-18').key1" - "Key is wrong" - } - ] + Expect.equal + key.Value + "listKeys(resourceId('resource group', 'Microsoft.CognitiveServices/accounts', 'test'), '2017-04-18').key1" + "Key is wrong" + } + ] diff --git a/src/Tests/Common.fs b/src/Tests/Common.fs index 39e66363d..52f6ac454 100644 --- a/src/Tests/Common.fs +++ b/src/Tests/Common.fs @@ -4,160 +4,152 @@ open Expecto open Farmer let tests = - testList - "Common" - [ - test "IPAddressCidr creates correct range" { - let cidr = IPAddressCidr.parse "192.168.0.0/24" - let first, last = cidr |> IPAddressCidr.ipRange - Expect.equal (string first) "192.168.0.0" "First address incorrect" - Expect.equal (string last) "192.168.0.255" "Last address incorrect" - } - - test "Can carve /22 into 4 /24 subnets" { - let cidr = IPAddressCidr.parse "192.168.0.0/22" - - let subnets = - [ 24; 24; 24; 24 ] + testList "Common" [ + test "IPAddressCidr creates correct range" { + let cidr = IPAddressCidr.parse "192.168.0.0/24" + let first, last = cidr |> IPAddressCidr.ipRange + Expect.equal (string first) "192.168.0.0" "First address incorrect" + Expect.equal (string last) "192.168.0.255" "Last address incorrect" + } + + test "Can carve /22 into 4 /24 subnets" { + let cidr = IPAddressCidr.parse "192.168.0.0/22" + + let subnets = + [ 24; 24; 24; 24 ] + |> IPAddressCidr.carveAddressSpace cidr + |> Seq.map IPAddressCidr.format + |> Array.ofSeq + + Expect.equal subnets.Length 4 "Incorrect number of subnets" + Expect.equal subnets.[0] "192.168.0.0/24" "First subnet incorrect" + Expect.equal subnets.[1] "192.168.1.0/24" "Second subnet incorrect" + Expect.equal subnets.[2] "192.168.2.0/24" "Third subnet incorrect" + Expect.equal subnets.[3] "192.168.3.0/24" "Fourth subnet incorrect" + } + + test "Can carve /22 into 7 different subnets preventing overlap" { + let cidr = IPAddressCidr.parse "192.168.0.0/22" + + let subnets = + [ 24; 24; 24; 30; 30; 28; 26 ] + |> IPAddressCidr.carveAddressSpace cidr + |> Seq.map IPAddressCidr.format + |> Array.ofSeq + + Expect.equal subnets.Length 7 "Incorrect number of subnets" + Expect.equal subnets.[0] "192.168.0.0/24" "First subnet incorrect" + Expect.equal subnets.[1] "192.168.1.0/24" "Second subnet incorrect" + Expect.equal subnets.[2] "192.168.2.0/24" "Third subnet incorrect" + Expect.equal subnets.[3] "192.168.3.0/30" "Fourth subnet incorrect" + Expect.equal subnets.[4] "192.168.3.4/30" "Fifth subnet incorrect" + Expect.equal subnets.[5] "192.168.3.16/28" "Sixth subnet incorrect" + Expect.equal subnets.[6] "192.168.3.64/26" "Seventh subnet incorrect" + } + test "Fails to carve /22 into 3 /24 and 1 /23 subnets" { + Expect.throws + (fun _ -> + let cidr = IPAddressCidr.parse "192.168.0.0/22" + + [ 24; 24; 24; 23 ] |> IPAddressCidr.carveAddressSpace cidr - |> Seq.map IPAddressCidr.format - |> Array.ofSeq - - Expect.equal subnets.Length 4 "Incorrect number of subnets" - Expect.equal subnets.[0] "192.168.0.0/24" "First subnet incorrect" - Expect.equal subnets.[1] "192.168.1.0/24" "Second subnet incorrect" - Expect.equal subnets.[2] "192.168.2.0/24" "Third subnet incorrect" - Expect.equal subnets.[3] "192.168.3.0/24" "Fourth subnet incorrect" - } - - test "Can carve /22 into 7 different subnets preventing overlap" { - let cidr = IPAddressCidr.parse "192.168.0.0/22" - - let subnets = - [ 24; 24; 24; 30; 30; 28; 26 ] - |> IPAddressCidr.carveAddressSpace cidr - |> Seq.map IPAddressCidr.format - |> Array.ofSeq - - Expect.equal subnets.Length 7 "Incorrect number of subnets" - Expect.equal subnets.[0] "192.168.0.0/24" "First subnet incorrect" - Expect.equal subnets.[1] "192.168.1.0/24" "Second subnet incorrect" - Expect.equal subnets.[2] "192.168.2.0/24" "Third subnet incorrect" - Expect.equal subnets.[3] "192.168.3.0/30" "Fourth subnet incorrect" - Expect.equal subnets.[4] "192.168.3.4/30" "Fifth subnet incorrect" - Expect.equal subnets.[5] "192.168.3.16/28" "Sixth subnet incorrect" - Expect.equal subnets.[6] "192.168.3.64/26" "Seventh subnet incorrect" - } - test "Fails to carve /22 into 3 /24 and 1 /23 subnets" { - Expect.throws - (fun _ -> - let cidr = IPAddressCidr.parse "192.168.0.0/22" - - [ 24; 24; 24; 23 ] - |> IPAddressCidr.carveAddressSpace cidr - |> List.ofSeq - |> ignore) - "Should have failed to carve /22 into subnets" - } - - test "10.0.5.0/24 is contained within 10.0.0.0/16" { - let innerCidr = IPAddressCidr.parse "10.0.5.0/24" - let outerCidr = IPAddressCidr.parse "10.0.0.0/16" - Expect.isTrue (outerCidr |> IPAddressCidr.contains innerCidr) "" - } - - test "192.168.1.0/24 is not contained within 10.0.0.0/16" { - let innerCidr = IPAddressCidr.parse "192.168.1.0/24" - let outerCidr = IPAddressCidr.parse "10.0.0.0/16" - 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" - - let officialNginxVersion = - Containers.DockerImage.PublicImage("nginx", Some "1.21.4") - - Expect.equal - officialNginxVersion.ImageTag - "nginx:1.21.4" - "Official versioned image generated with incorrect tag" - - let privateRepo = Containers.DockerImage.PrivateImage("my.azurecr.io", "foo", None) - - Expect.equal - privateRepo.ImageTag - "my.azurecr.io/foo:latest" - "Private image generated with incorrect tag" - - let privateRepoVersion = - Containers.DockerImage.PrivateImage("my.azurecr.io", "foo", Version = Some "1.2.3") - - Expect.equal - privateRepoVersion.ImageTag - "my.azurecr.io/foo:1.2.3" - "Private versioned image generated with incorrect tag" - - let privateRepoNamedContainer = - Containers.DockerImage.PrivateImage("my.azurecr.io", "foo/bar", None) - - Expect.equal - privateRepoNamedContainer.ImageTag - "my.azurecr.io/foo/bar:latest" - "Private named container image generated with incorrect tag" - - let privateRepoNamedContainerVersion = - Containers.DockerImage.PrivateImage("my.azurecr.io", "foo/bar", Some "1.2.3") - - Expect.equal - privateRepoNamedContainerVersion.ImageTag - "my.azurecr.io/foo/bar:1.2.3" - "Private named and versioned container image generated with incorrect tag" - } - - test "Docker image tag parsing" { - let officialNginx = Containers.DockerImage.Parse "nginx" - Expect.equal officialNginx.ImageTag "nginx:latest" "Official image generated with incorrect tag" - let officialNginxVersion = Containers.DockerImage.Parse "nginx:1.21.4" - - Expect.equal - officialNginxVersion.ImageTag - "nginx:1.21.4" - "Official versioned image generated with incorrect tag" - - let privateRepo = Containers.DockerImage.Parse "my.azurecr.io/foo" - - Expect.equal - privateRepo.ImageTag - "my.azurecr.io/foo:latest" - "Private image generated with incorrect tag" - - let privateRepoVersion = Containers.DockerImage.Parse "my.azurecr.io/foo:1.2.3" - - Expect.equal - privateRepoVersion.ImageTag - "my.azurecr.io/foo:1.2.3" - "Private versioned image generated with incorrect tag" - - let privateRepoNamedContainer = Containers.DockerImage.Parse "my.azurecr.io/foo/bar" - - Expect.equal - privateRepoNamedContainer.ImageTag - "my.azurecr.io/foo/bar:latest" - "Private named container image generated with incorrect tag" - - let privateRepoNamedContainerVersion = - Containers.DockerImage.Parse "my.azurecr.io/foo/bar:1.2.3" - - Expect.equal - privateRepoNamedContainerVersion.ImageTag - "my.azurecr.io/foo/bar:1.2.3" - "Private named and versioned container image generated with incorrect tag" - } - ] + |> List.ofSeq + |> ignore) + "Should have failed to carve /22 into subnets" + } + + test "10.0.5.0/24 is contained within 10.0.0.0/16" { + let innerCidr = IPAddressCidr.parse "10.0.5.0/24" + let outerCidr = IPAddressCidr.parse "10.0.0.0/16" + Expect.isTrue (outerCidr |> IPAddressCidr.contains innerCidr) "" + } + + test "192.168.1.0/24 is not contained within 10.0.0.0/16" { + let innerCidr = IPAddressCidr.parse "192.168.1.0/24" + let outerCidr = IPAddressCidr.parse "10.0.0.0/16" + 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" + + let officialNginxVersion = + Containers.DockerImage.PublicImage("nginx", Some "1.21.4") + + Expect.equal + officialNginxVersion.ImageTag + "nginx:1.21.4" + "Official versioned image generated with incorrect tag" + + let privateRepo = Containers.DockerImage.PrivateImage("my.azurecr.io", "foo", None) + + Expect.equal privateRepo.ImageTag "my.azurecr.io/foo:latest" "Private image generated with incorrect tag" + + let privateRepoVersion = + Containers.DockerImage.PrivateImage("my.azurecr.io", "foo", Version = Some "1.2.3") + + Expect.equal + privateRepoVersion.ImageTag + "my.azurecr.io/foo:1.2.3" + "Private versioned image generated with incorrect tag" + + let privateRepoNamedContainer = + Containers.DockerImage.PrivateImage("my.azurecr.io", "foo/bar", None) + + Expect.equal + privateRepoNamedContainer.ImageTag + "my.azurecr.io/foo/bar:latest" + "Private named container image generated with incorrect tag" + + let privateRepoNamedContainerVersion = + Containers.DockerImage.PrivateImage("my.azurecr.io", "foo/bar", Some "1.2.3") + + Expect.equal + privateRepoNamedContainerVersion.ImageTag + "my.azurecr.io/foo/bar:1.2.3" + "Private named and versioned container image generated with incorrect tag" + } + + test "Docker image tag parsing" { + let officialNginx = Containers.DockerImage.Parse "nginx" + Expect.equal officialNginx.ImageTag "nginx:latest" "Official image generated with incorrect tag" + let officialNginxVersion = Containers.DockerImage.Parse "nginx:1.21.4" + + Expect.equal + officialNginxVersion.ImageTag + "nginx:1.21.4" + "Official versioned image generated with incorrect tag" + + let privateRepo = Containers.DockerImage.Parse "my.azurecr.io/foo" + + Expect.equal privateRepo.ImageTag "my.azurecr.io/foo:latest" "Private image generated with incorrect tag" + + let privateRepoVersion = Containers.DockerImage.Parse "my.azurecr.io/foo:1.2.3" + + Expect.equal + privateRepoVersion.ImageTag + "my.azurecr.io/foo:1.2.3" + "Private versioned image generated with incorrect tag" + + let privateRepoNamedContainer = Containers.DockerImage.Parse "my.azurecr.io/foo/bar" + + Expect.equal + privateRepoNamedContainer.ImageTag + "my.azurecr.io/foo/bar:latest" + "Private named container image generated with incorrect tag" + + let privateRepoNamedContainerVersion = + Containers.DockerImage.Parse "my.azurecr.io/foo/bar:1.2.3" + + Expect.equal + privateRepoNamedContainerVersion.ImageTag + "my.azurecr.io/foo/bar:1.2.3" + "Private named and versioned container image generated with incorrect tag" + } + ] diff --git a/src/Tests/CommunicationServices.fs b/src/Tests/CommunicationServices.fs index d96239d14..de7ff2bd0 100644 --- a/src/Tests/CommunicationServices.fs +++ b/src/Tests/CommunicationServices.fs @@ -6,33 +6,30 @@ open Farmer.Arm open Farmer.Builders let tests = - testList - "Communication Services" - [ - test "Basic test" { - let tags = [ "a", "1"; "b", "2" ] + testList "Communication Services" [ + test "Basic test" { + let tags = [ "a", "1"; "b", "2" ] - let swa = - communicationService { - name "test" - add_tags tags - data_location DataLocation.Australia - } - - let baseArm = (swa :> IBuilder).BuildResources(Location.WestEurope).[0] - let bsArm = baseArm :?> CommunicationService - Expect.equal bsArm.Name (ResourceName "test") "Name" - Expect.equal bsArm.DataLocation DataLocation.Australia "Data Location" - Expect.equal bsArm.Tags (Map tags) "Tags" + let swa = communicationService { + name "test" + add_tags tags + data_location DataLocation.Australia } - test "Default options test" { - let swa = communicationService { name "test" } + let baseArm = (swa :> IBuilder).BuildResources(Location.WestEurope).[0] + let bsArm = baseArm :?> CommunicationService + Expect.equal bsArm.Name (ResourceName "test") "Name" + Expect.equal bsArm.DataLocation DataLocation.Australia "Data Location" + Expect.equal bsArm.Tags (Map tags) "Tags" + } - let baseArm = (swa :> IBuilder).BuildResources(Location.WestEurope).[0] - let bsArm = baseArm :?> CommunicationService - Expect.equal bsArm.Name (ResourceName "test") "Name" - Expect.equal bsArm.DataLocation DataLocation.UnitedStates "Data Location" - Expect.isEmpty bsArm.Tags "Tags" - } - ] + test "Default options test" { + let swa = communicationService { name "test" } + + let baseArm = (swa :> IBuilder).BuildResources(Location.WestEurope).[0] + let bsArm = baseArm :?> CommunicationService + Expect.equal bsArm.Name (ResourceName "test") "Name" + Expect.equal bsArm.DataLocation DataLocation.UnitedStates "Data Location" + Expect.isEmpty bsArm.Tags "Tags" + } + ] diff --git a/src/Tests/ContainerApps.fs b/src/Tests/ContainerApps.fs index 682ed6b74..4a548c9be 100644 --- a/src/Tests/ContainerApps.fs +++ b/src/Tests/ContainerApps.fs @@ -15,386 +15,358 @@ let storageAccountName = "storagename" let fullContainerAppDeployment = let containerLogs = logAnalytics { name "containerlogs" } - let insights = - appInsights { - name "appinsights" - log_analytics_workspace containerLogs - } + let insights = appInsights { + name "appinsights" + log_analytics_workspace containerLogs + } let containerRegistryDomain = $"{containerRegistryName}.azurecr.io" let acr = containerRegistry { name containerRegistryName } - let storage = - storageAccount { - name storageAccountName - add_file_share "certs" - } + let storage = storageAccount { + name storageAccountName + add_file_share "certs" + } let version = "1.0.0" let managedIdentity = ManagedIdentity.Empty - let containerEnv = - containerEnvironment { - name "kubecontainerenv" - log_analytics_instance containerLogs - app_insights_instance insights + let containerEnv = containerEnvironment { + name "kubecontainerenv" + log_analytics_instance containerLogs + app_insights_instance insights + + add_containers [ + containerApp { + name "http" + add_identity msi + active_revision_mode Single - add_containers - [ - containerApp { + add_registry_credentials [ registry containerRegistryDomain containerRegistryName managedIdentity ] + + add_containers [ + container { name "http" - add_identity msi - active_revision_mode Single - - add_registry_credentials - [ registry containerRegistryDomain containerRegistryName managedIdentity ] - - add_containers - [ - container { - name "http" - private_docker_image containerRegistryDomain "http" version - cpu_cores 0.25 - memory 0.5 - ephemeral_storage 1. - } - ] - - replicas 1 5 - add_env_variable "ServiceBusQueueName" "wishrequests" - add_secret_parameter "servicebusconnectionkey" - ingress_state Enabled - ingress_target_port 80us - ingress_transport Auto - dapr_app_id "http" - add_http_scale_rule "http-rule" { ConcurrentRequests = 100 } - } - containerApp { - name "multienv" - add_simple_container "mcr.microsoft.com/dotnet/samples" "aspnetapp" - ingress_target_port 80us - ingress_transport Auto - add_http_scale_rule "http-scaler" { ConcurrentRequests = 10 } - add_cpu_scale_rule "cpu-scaler" { Utilization = 50 } - add_secret_parameters [ "servicebusconnectionkey" ] - - add_env_variables [ "ServiceBusQueueName", "wishrequests" ] - - add_secret_expressions [ "containerlogs", containerLogs.PrimarySharedKey ] + private_docker_image containerRegistryDomain "http" version + cpu_cores 0.25 + memory 0.5 + ephemeral_storage 1. } - containerApp { + ] + + replicas 1 5 + add_env_variable "ServiceBusQueueName" "wishrequests" + add_secret_parameter "servicebusconnectionkey" + ingress_state Enabled + ingress_target_port 80us + ingress_transport Auto + dapr_app_id "http" + add_http_scale_rule "http-rule" { ConcurrentRequests = 100 } + } + containerApp { + name "multienv" + add_simple_container "mcr.microsoft.com/dotnet/samples" "aspnetapp" + ingress_target_port 80us + ingress_transport Auto + add_http_scale_rule "http-scaler" { ConcurrentRequests = 10 } + add_cpu_scale_rule "cpu-scaler" { Utilization = 50 } + add_secret_parameters [ "servicebusconnectionkey" ] + + add_env_variables [ "ServiceBusQueueName", "wishrequests" ] + + add_secret_expressions [ "containerlogs", containerLogs.PrimarySharedKey ] + } + containerApp { + name "servicebus" + active_revision_mode Single + reference_registry_credentials [ (acr :> IBuilder).ResourceId ] + + add_volumes [ + Volume.emptyDir "empty-v" + Volume.azureFile "certs-v" (ResourceName "certs") storage.Name StorageAccessMode.ReadOnly + ] + + add_containers [ + container { name "servicebus" - active_revision_mode Single - reference_registry_credentials [ (acr :> IBuilder).ResourceId ] - - add_volumes - [ - Volume.emptyDir "empty-v" - Volume.azureFile - "certs-v" - (ResourceName "certs") - storage.Name - StorageAccessMode.ReadOnly - ] - - add_containers - [ - container { - name "servicebus" - private_docker_image containerRegistryDomain "servicebus" version - - add_volume_mounts [ "empty-v", "/tmp"; "certs-v", "/certs" ] - } - ] - - replicas 0 3 - add_env_variable "ServiceBusQueueName" "wishrequests" - add_secret_parameter "servicebusconnectionkey" - - add_servicebus_scale_rule - "sb-keda-scale" - { - QueueName = "wishrequests" - MessageCount = 5 - SecretRef = "servicebusconnectionkey" - } + private_docker_image containerRegistryDomain "servicebus" version + + add_volume_mounts [ "empty-v", "/tmp"; "certs-v", "/certs" ] } ] - } + + replicas 0 3 + add_env_variable "ServiceBusQueueName" "wishrequests" + add_secret_parameter "servicebusconnectionkey" + + add_servicebus_scale_rule "sb-keda-scale" { + QueueName = "wishrequests" + MessageCount = 5 + SecretRef = "servicebusconnectionkey" + } + } + ] + } arm { add_resources [ containerEnv; msi ] } let tests = - testList - "Container Apps" - [ - let jsonTemplate = fullContainerAppDeployment.Template |> Writer.toJson + testList "Container Apps" [ + let jsonTemplate = fullContainerAppDeployment.Template |> Writer.toJson - let jobj = JObject.Parse jsonTemplate + let jobj = JObject.Parse jsonTemplate - test "Container automatically creates a log analytics workspace" { - let env: IBuilder = containerEnvironment { name "testca" } + test "Container automatically creates a log analytics workspace" { + let env: IBuilder = containerEnvironment { name "testca" } - let resources = env.BuildResources Location.NorthEurope + let resources = env.BuildResources Location.NorthEurope - Expect.exists - resources - (fun r -> r.ResourceId.Name.Value = "testca-workspace") - "No Log Analytics workspace was created." - } + Expect.exists + resources + (fun r -> r.ResourceId.Name.Value = "testca-workspace") + "No Log Analytics workspace was created." + } - test "Full container environment parameters" { - Expect.hasLength jobj.["parameters"] 2 "Expecting 2 parameters" + test "Full container environment parameters" { + Expect.hasLength jobj.["parameters"] 2 "Expecting 2 parameters" - Expect.isNotNull - (jobj.SelectToken("parameters.servicebusconnectionkey")) - "Missing 'servicebusconnectionkey' parameter" + Expect.isNotNull + (jobj.SelectToken("parameters.servicebusconnectionkey")) + "Missing 'servicebusconnectionkey' parameter" - Expect.isNotNull - (jobj.SelectToken("parameters['myregistry.azurecr.io-password']")) - "Missing 'myregistry.azurecr.io-password' parameter" - } + Expect.isNotNull + (jobj.SelectToken("parameters['myregistry.azurecr.io-password']")) + "Missing 'myregistry.azurecr.io-password' parameter" + } - test "Seq container environment parameters" { - let containerApp = - fullContainerAppDeployment.Template.Resources - |> List.find (fun r -> r.ResourceId.Name.Value = "multienv") - :?> Farmer.Arm.App.ContainerApp + test "Seq container environment parameters" { + let containerApp = + fullContainerAppDeployment.Template.Resources + |> List.find (fun r -> r.ResourceId.Name.Value = "multienv") + :?> Farmer.Arm.App.ContainerApp - containerApp.EnvironmentVariables.["ServiceBusQueueName"] |> ignore + containerApp.EnvironmentVariables.["ServiceBusQueueName"] |> ignore - containerApp.EnvironmentVariables.["servicebusconnectionkey"] |> ignore + containerApp.EnvironmentVariables.["servicebusconnectionkey"] |> ignore - containerApp.EnvironmentVariables.["containerlogs"] |> ignore - } + containerApp.EnvironmentVariables.["containerlogs"] |> ignore + } - test "Full container managed environments" { - let kubeEnv = jobj.SelectToken("resources[?(@.name=='kubecontainerenv')]") + test "Full container managed environments" { + let kubeEnv = jobj.SelectToken("resources[?(@.name=='kubecontainerenv')]") - Expect.equal - (kubeEnv.["type"] |> string) - "Microsoft.App/managedEnvironments" - "Incorrect type for kuberenetes environment" + Expect.equal + (kubeEnv.["type"] |> string) + "Microsoft.App/managedEnvironments" + "Incorrect type for kuberenetes environment" - Expect.equal - (kubeEnv.["kind"] |> string) - "containerenvironment" - "Incorrect kind for kuberenetes environment" + Expect.equal + (kubeEnv.["kind"] |> string) + "containerenvironment" + "Incorrect kind for kuberenetes environment" - let kubeEnvAppLogConfig = - jobj.SelectToken("resources[?(@.name=='kubecontainerenv')].properties.appLogsConfiguration") + let kubeEnvAppLogConfig = + jobj.SelectToken("resources[?(@.name=='kubecontainerenv')].properties.appLogsConfiguration") - Expect.equal - (kubeEnvAppLogConfig.["destination"] |> string) - "log-analytics" - "Incorrect type for app log config" + Expect.equal + (kubeEnvAppLogConfig.["destination"] |> string) + "log-analytics" + "Incorrect type for app log config" - let kubeEnvLogAnalyticsCustomerId = - jobj.SelectToken( - "resources[?(@.name=='kubecontainerenv')].properties.appLogsConfiguration.logAnalyticsConfiguration" - ) + let kubeEnvLogAnalyticsCustomerId = + jobj.SelectToken( + "resources[?(@.name=='kubecontainerenv')].properties.appLogsConfiguration.logAnalyticsConfiguration" + ) - Expect.equal - (kubeEnvLogAnalyticsCustomerId.["customerId"] |> string) - "[reference(resourceId('Microsoft.OperationalInsights/workspaces', 'containerlogs'), '2020-03-01-preview').customerId]" - "Incorrect log analytics customerId reference" - } + Expect.equal + (kubeEnvLogAnalyticsCustomerId.["customerId"] |> string) + "[reference(resourceId('Microsoft.OperationalInsights/workspaces', 'containerlogs'), '2020-03-01-preview').customerId]" + "Incorrect log analytics customerId reference" + } - test "Full container environment containerApp" { - let httpContainerApp = jobj.SelectToken("resources[?(@.name=='http')]") + test "Full container environment containerApp" { + let httpContainerApp = jobj.SelectToken("resources[?(@.name=='http')]") - Expect.equal - (httpContainerApp.["type"] |> string) - "Microsoft.App/containerApps" - "Incorrect type for containerApps" + Expect.equal + (httpContainerApp.["type"] |> string) + "Microsoft.App/containerApps" + "Incorrect type for containerApps" - Expect.equal (httpContainerApp.["kind"] |> string) "containerapp" "Incorrect kind for containerApps" + Expect.equal (httpContainerApp.["kind"] |> string) "containerapp" "Incorrect kind for containerApps" - let ingress = httpContainerApp.SelectToken("properties.configuration.ingress") + let ingress = httpContainerApp.SelectToken("properties.configuration.ingress") - Expect.isTrue (ingress.SelectToken("external") |> string |> bool.Parse) "Incorrect external ingress" + Expect.isTrue (ingress.SelectToken("external") |> string |> bool.Parse) "Incorrect external ingress" - Expect.equal (ingress.SelectToken("targetPort") |> string |> int) 80 "Incorrect targetPort" - Expect.equal (ingress.SelectToken("transport") |> string) "auto" "Incorrect transport" + Expect.equal (ingress.SelectToken("targetPort") |> string |> int) 80 "Incorrect targetPort" + Expect.equal (ingress.SelectToken("transport") |> string) "auto" "Incorrect transport" - let registries = httpContainerApp.SelectToken("properties.configuration.registries") + let registries = httpContainerApp.SelectToken("properties.configuration.registries") - Expect.hasLength registries 1 "Expected 1 registry" - let firstRegistry = registries |> Seq.head + Expect.hasLength registries 1 "Expected 1 registry" + let firstRegistry = registries |> Seq.head - Expect.equal - (firstRegistry.SelectToken("passwordSecretRef") |> string) - "myregistry" - "Incorrect registry password secretRef" + Expect.equal + (firstRegistry.SelectToken("passwordSecretRef") |> string) + "myregistry" + "Incorrect registry password secretRef" - Expect.equal - (firstRegistry.SelectToken("server") |> string) - "myregistry.azurecr.io" - "Incorrect registry" - // The value here doesn't seem quite right. Is it really supposed to be the name of the repository in the registry? - Expect.equal - (firstRegistry.SelectToken("username") |> string) - "myregistry" - "Incorrect registry username" + Expect.equal (firstRegistry.SelectToken("server") |> string) "myregistry.azurecr.io" "Incorrect registry" + // The value here doesn't seem quite right. Is it really supposed to be the name of the repository in the registry? + Expect.equal (firstRegistry.SelectToken("username") |> string) "myregistry" "Incorrect registry username" - let secrets = httpContainerApp.SelectToken("properties.configuration.secrets") + let secrets = httpContainerApp.SelectToken("properties.configuration.secrets") - Expect.hasLength secrets 2 "Expecting 2 secrets" - Expect.equal (secrets.[0].["name"] |> string) "myregistry" "Incorrect name for registry password secret" + Expect.hasLength secrets 2 "Expecting 2 secrets" + Expect.equal (secrets.[0].["name"] |> string) "myregistry" "Incorrect name for registry password secret" - Expect.equal - (secrets.[0].["value"] |> string) - "[parameters('myregistry.azurecr.io-password')]" - "Incorrect password parameter for registry password secret" + Expect.equal + (secrets.[0].["value"] |> string) + "[parameters('myregistry.azurecr.io-password')]" + "Incorrect password parameter for registry password secret" - Expect.equal - (httpContainerApp.SelectToken("properties.managedEnvironmentId") |> string) - "[resourceId('Microsoft.App/managedEnvironments', 'kubecontainerenv')]" - "Incorrect kube environment Id" + Expect.equal + (httpContainerApp.SelectToken("properties.managedEnvironmentId") |> string) + "[resourceId('Microsoft.App/managedEnvironments', 'kubecontainerenv')]" + "Incorrect kube environment Id" - let containers = httpContainerApp.SelectToken("properties.template.containers") + let containers = httpContainerApp.SelectToken("properties.template.containers") - Expect.hasLength containers 1 "Expected 1 http container" - let httpContainer = containers |> Seq.head + Expect.hasLength containers 1 "Expected 1 http container" + let httpContainer = containers |> Seq.head - Expect.equal - (httpContainer.["image"] |> string) - "myregistry.azurecr.io/http:1.0.0" - "Incorrect container image" + Expect.equal + (httpContainer.["image"] |> string) + "myregistry.azurecr.io/http:1.0.0" + "Incorrect container image" - Expect.equal (httpContainer.["name"] |> string) "http" "Incorrect container name" + Expect.equal (httpContainer.["name"] |> string) "http" "Incorrect container name" - Expect.equal - (httpContainer.SelectToken("resources.cpu") |> float) - 0.25 - "Incorrect container cpu resources" + Expect.equal (httpContainer.SelectToken("resources.cpu") |> float) 0.25 "Incorrect container cpu resources" - Expect.equal - (httpContainer.SelectToken("resources.memory") |> string) - "0.50Gi" - "Incorrect container memory resources" + Expect.equal + (httpContainer.SelectToken("resources.memory") |> string) + "0.50Gi" + "Incorrect container memory resources" - Expect.equal - (httpContainer.SelectToken("resources.ephemeralStorage") |> string) - "1.00Gi" - "Incorrect container ephemeral storage resources" + Expect.equal + (httpContainer.SelectToken("resources.ephemeralStorage") |> string) + "1.00Gi" + "Incorrect container ephemeral storage resources" - let scale = httpContainerApp.SelectToken("properties.template.scale") + let scale = httpContainerApp.SelectToken("properties.template.scale") - Expect.isNotNull scale "properties.scale was null" - Expect.equal (scale.["minReplicas"] |> int) 1 "Incorrect min replicas" - Expect.equal (scale.["maxReplicas"] |> int) 5 "Incorrect max replicas" + Expect.isNotNull scale "properties.scale was null" + Expect.equal (scale.["minReplicas"] |> int) 1 "Incorrect min replicas" + Expect.equal (scale.["maxReplicas"] |> int) 5 "Incorrect max replicas" - let serviceBusContainerApp = jobj.SelectToken("resources[?(@.name=='servicebus')]") + let serviceBusContainerApp = jobj.SelectToken("resources[?(@.name=='servicebus')]") - let volumes = serviceBusContainerApp.SelectToken("properties.template.volumes") + let volumes = serviceBusContainerApp.SelectToken("properties.template.volumes") - Expect.hasLength volumes 2 "Expecting 2 volumes" + Expect.hasLength volumes 2 "Expecting 2 volumes" - let serviceBusContainer = - serviceBusContainerApp.SelectToken("properties.template.containers") |> Seq.head + let serviceBusContainer = + serviceBusContainerApp.SelectToken("properties.template.containers") |> Seq.head - let serviceBusVolumeMounts = serviceBusContainer.SelectToken("volumeMounts") + let serviceBusVolumeMounts = serviceBusContainer.SelectToken("volumeMounts") - Expect.equal - (serviceBusVolumeMounts.[1].["volumeName"] |> string) - "empty-v" - "Incorrect container volume mount" + Expect.equal + (serviceBusVolumeMounts.[1].["volumeName"] |> string) + "empty-v" + "Incorrect container volume mount" - Expect.equal - (serviceBusVolumeMounts.[1].["mountPath"] |> string) - "/tmp" - "Incorrect container volume mount" + Expect.equal (serviceBusVolumeMounts.[1].["mountPath"] |> string) "/tmp" "Incorrect container volume mount" - Expect.equal - (serviceBusVolumeMounts.[0].["volumeName"] |> string) - "certs-v" - "Incorrect container volume mount" + Expect.equal + (serviceBusVolumeMounts.[0].["volumeName"] |> string) + "certs-v" + "Incorrect container volume mount" - Expect.equal - (serviceBusVolumeMounts.[0].["mountPath"] |> string) - "/certs" - "Incorrect container volume mount" - } + Expect.equal + (serviceBusVolumeMounts.[0].["mountPath"] |> string) + "/certs" + "Incorrect container volume mount" + } - test "Makes container app with MSI" { - let containerApp = - fullContainerAppDeployment.Template.Resources - |> List.find (fun r -> r.ResourceId.Name.Value = "http") - :?> Farmer.Arm.App.ContainerApp + test "Makes container app with MSI" { + let containerApp = + fullContainerAppDeployment.Template.Resources + |> List.find (fun r -> r.ResourceId.Name.Value = "http") + :?> Farmer.Arm.App.ContainerApp - Expect.isNonEmpty containerApp.Identity.UserAssigned "Container app did not have identity" + Expect.isNonEmpty containerApp.Identity.UserAssigned "Container app did not have identity" - Expect.equal - containerApp.Identity.UserAssigned.[0] - (UserAssignedIdentity( - ResourceId.create (Arm.ManagedIdentity.userAssignedIdentities, ResourceName "appUser") - )) - "Expected user identity named 'appUser'." - } + Expect.equal + containerApp.Identity.UserAssigned.[0] + (UserAssignedIdentity( + ResourceId.create (Arm.ManagedIdentity.userAssignedIdentities, ResourceName "appUser") + )) + "Expected user identity named 'appUser'." + } - test "Makes container environment with volumes" { - let certsStorage = - fullContainerAppDeployment.Template.Resources - |> List.find (fun r -> r.ResourceId.Name.Value = "certs-v") - :?> Farmer.Arm.App.ManagedEnvironmentStorage + test "Makes container environment with volumes" { + let certsStorage = + fullContainerAppDeployment.Template.Resources + |> List.find (fun r -> r.ResourceId.Name.Value = "certs-v") + :?> Farmer.Arm.App.ManagedEnvironmentStorage - Expect.equal - certsStorage.AzureFile.AccessMode - StorageAccessMode.ReadOnly - "Expected ReadOnly mode for 'certs-v'." + Expect.equal + certsStorage.AzureFile.AccessMode + StorageAccessMode.ReadOnly + "Expected ReadOnly mode for 'certs-v'." - Expect.equal - certsStorage.AzureFile.AccountName.ResourceName.Value - storageAccountName - "Expected 'certs-v' account name." + Expect.equal + certsStorage.AzureFile.AccountName.ResourceName.Value + storageAccountName + "Expected 'certs-v' account name." - Expect.equal certsStorage.AzureFile.ShareName.Value "certs" "Expected 'certs-v' share name." - } + Expect.equal certsStorage.AzureFile.ShareName.Value "certs" "Expected 'certs-v' share name." + } - test "Linked ACR references correct secret" { - let containerApp = - fullContainerAppDeployment.Template.Resources - |> List.find (fun r -> r.ResourceId.Name.Value = "servicebus") - :?> Farmer.Arm.App.ContainerApp - - Expect.isFalse - (containerApp.Secrets - |> Map.containsKey - (ContainerAppValidation.ContainerAppSettingKey.Create $"{containerRegistryName}-username") - .OkValue) - "Container app did not have linked ACR's secret" - } + test "Linked ACR references correct secret" { + let containerApp = + fullContainerAppDeployment.Template.Resources + |> List.find (fun r -> r.ResourceId.Name.Value = "servicebus") + :?> Farmer.Arm.App.ContainerApp + + Expect.isFalse + (containerApp.Secrets + |> Map.containsKey + (ContainerAppValidation.ContainerAppSettingKey.Create $"{containerRegistryName}-username") + .OkValue) + "Container app did not have linked ACR's secret" + } - test "Turns on Dapr" { - let containerApp = - fullContainerAppDeployment.Template.Resources - |> List.find (fun r -> r.ResourceId.Name.Value = "http") - :?> ContainerApp + test "Turns on Dapr" { + let containerApp = + fullContainerAppDeployment.Template.Resources + |> List.find (fun r -> r.ResourceId.Name.Value = "http") + :?> ContainerApp - Expect.isSome containerApp.DaprConfig "Dapr config was not set" - } + Expect.isSome containerApp.DaprConfig "Dapr config was not set" + } - test "Adds App Insight integration" { - let apps = - fullContainerAppDeployment.Template.Resources - |> List.choose (function - | (:? ContainerApp as c) -> Some c - | _ -> None) - - for ca in apps do - Expect.exists - ca.EnvironmentVariables - (fun r -> r.Key = "APPINSIGHTS_INSTRUMENTATIONKEY") - "Missing AI key" - - let managedEnvironment = - fullContainerAppDeployment.Template.Resources - |> List.pick (function - | (:? ManagedEnvironment as c) -> Some c - | _ -> None) - - Expect.isSome managedEnvironment.AppInsightsInstrumentationKey "Dapr AI key not set" - } - ] + test "Adds App Insight integration" { + let apps = + fullContainerAppDeployment.Template.Resources + |> List.choose (function + | (:? ContainerApp as c) -> Some c + | _ -> None) + + for ca in apps do + Expect.exists + ca.EnvironmentVariables + (fun r -> r.Key = "APPINSIGHTS_INSTRUMENTATIONKEY") + "Missing AI key" + + let managedEnvironment = + fullContainerAppDeployment.Template.Resources + |> List.pick (function + | (:? ManagedEnvironment as c) -> Some c + | _ -> None) + + Expect.isSome managedEnvironment.AppInsightsInstrumentationKey "Dapr AI key not set" + } + ] diff --git a/src/Tests/ContainerGroup.fs b/src/Tests/ContainerGroup.fs index 6a599b38f..05aa5c038 100644 --- a/src/Tests/ContainerGroup.fs +++ b/src/Tests/ContainerGroup.fs @@ -13,32 +13,29 @@ open Microsoft.Rest open System open Newtonsoft.Json.Linq -let nginx = - containerInstance { - name "nginx" - image "nginx:1.17.6-alpine" - add_ports PublicPort [ 80us; 443us ] - add_ports InternalPort [ 9090us ] - memory 0.5 - cpu_cores 1 - } - -let fsharpApp = - containerInstance { - name "fsharpApp" - image "myrepo/myapp:1.7.2" - add_ports PublicPort [ 8080us ] - memory 1.5 - cpu_cores 2 - } - -let appWithoutPorts = - containerInstance { - name "appWithoutPorts" - image "myapp:1.7.2" - memory 1.5 - cpu_cores 2 - } +let nginx = containerInstance { + name "nginx" + image "nginx:1.17.6-alpine" + add_ports PublicPort [ 80us; 443us ] + add_ports InternalPort [ 9090us ] + memory 0.5 + cpu_cores 1 +} + +let fsharpApp = containerInstance { + name "fsharpApp" + image "myrepo/myapp:1.7.2" + add_ports PublicPort [ 8080us ] + memory 1.5 + cpu_cores 2 +} + +let appWithoutPorts = containerInstance { + name "appWithoutPorts" + image "myapp:1.7.2" + memory 1.5 + cpu_cores 2 +} /// Client instance needed to get the serializer settings. let dummyClient = @@ -53,559 +50,514 @@ let asAzureResource (group: ContainerGroupConfig) = r let tests = - testList - "Container Group" - [ - test "Single container in a group is correctly created" { - let group = - containerGroup { - name "appWithHttpFrontend" - operating_system Linux - restart_policy AlwaysRestart - add_udp_port 123us - add_instances [ nginx ] - network_profile "test" - } - |> asAzureResource - - Expect.equal group.Name "appWithHttpFrontend" "Group name is not set correctly." - Expect.equal group.OsType "Linux" "OS should be Linux" - Expect.equal group.IpAddress.Ports.[1].PortProperty 123 "Incorrect udp port" - - let containerInstance = group.Containers.[0] - Expect.equal containerInstance.Image "nginx:1.17.6-alpine" "Incorrect image" - Expect.equal containerInstance.Name "nginx" "Incorrect instance name" - Expect.equal containerInstance.Resources.Requests.MemoryInGB 0.5 "Incorrect memory" - Expect.equal containerInstance.Resources.Requests.Cpu 1.0 "Incorrect CPU" - let ports = containerInstance.Ports |> Seq.map (fun p -> p.Port) |> Seq.toList - Expect.equal ports [ 80; 443; 9090 ] "Incorrect ports on container" + testList "Container Group" [ + test "Single container in a group is correctly created" { + let group = + containerGroup { + name "appWithHttpFrontend" + operating_system Linux + restart_policy AlwaysRestart + add_udp_port 123us + add_instances [ nginx ] + network_profile "test" + } + |> asAzureResource + + Expect.equal group.Name "appWithHttpFrontend" "Group name is not set correctly." + Expect.equal group.OsType "Linux" "OS should be Linux" + Expect.equal group.IpAddress.Ports.[1].PortProperty 123 "Incorrect udp port" + + let containerInstance = group.Containers.[0] + Expect.equal containerInstance.Image "nginx:1.17.6-alpine" "Incorrect image" + Expect.equal containerInstance.Name "nginx" "Incorrect instance name" + Expect.equal containerInstance.Resources.Requests.MemoryInGB 0.5 "Incorrect memory" + Expect.equal containerInstance.Resources.Requests.Cpu 1.0 "Incorrect CPU" + let ports = containerInstance.Ports |> Seq.map (fun p -> p.Port) |> Seq.toList + Expect.equal ports [ 80; 443; 9090 ] "Incorrect ports on container" + } + + test "Container group memory increment truncation" { + let container4_5Gb = containerInstance { + name "myapp" + image "myapp:latest" + memory 4.5678 } - test "Container group memory increment truncation" { - let container4_5Gb = - containerInstance { - name "myapp" - image "myapp:latest" - memory 4.5678 - } - - Expect.equal 4.6 container4_5Gb.Memory "Memory rounded incorrectly for 4.5 GB" - - let container8Gb = - containerInstance { - name "myapp" - image "myapp:latest" - memory 8. - } - - Expect.equal 8.0 container8Gb.Memory "Memory rounded incorrectly for 8 GB" - - let container8_2Gb = - containerInstance { - name "myapp" - image "myapp:latest" - memory 8.2 - } - - Expect.equal 8.2 container8_2Gb.Memory "Memory rounded incorrectly for 8.2 GB" - - let container0_2Gb = - containerInstance { - name "myapp" - image "myapp:latest" - memory 0.22 - } + Expect.equal 4.6 container4_5Gb.Memory "Memory rounded incorrectly for 4.5 GB" - Expect.equal 0.2 container0_2Gb.Memory "Memory rounded incorrectly for 0.2 GB" + let container8Gb = containerInstance { + name "myapp" + image "myapp:latest" + memory 8. } - test "Container group with init containers" { - let group = - let emptyDir1 = "emptyDir1" - - containerGroup { - name "appWithInitContainers" - add_volumes [ volume_mount.empty_dir emptyDir1 ] - - add_init_containers - [ - initContainer { - name "init" - image "busybox" - - command_line - [ - "/bin/sh" - "-c" - "sleep 60; echo python wordcount.py http://shakespeare.mit.edu/romeo_juliet/full.html > /mnt/emptydir/command_line.txt" - ] - - add_volume_mount emptyDir1 "/mnt/emptydir" - } - ] - - add_instances - [ - containerInstance { - name "hamlet" - image "mcr.microsoft.com/azuredocs/aci-wordcount" - add_volume_mount emptyDir1 "/mnt/emptydir" - env_vars [ "NumWords", "3"; "MinLength", "5" ] - } - ] - } - |> asAzureResource + Expect.equal 8.0 container8Gb.Memory "Memory rounded incorrectly for 8 GB" - let containerInstance = group.Containers.[0] - - Expect.equal - containerInstance.Image - "mcr.microsoft.com/azuredocs/aci-wordcount:latest" - "Incorrect containerInstance image" - - Expect.equal containerInstance.Name "hamlet" "Incorrect containerInstance name" - let initContainer = group.InitContainers.[0] - Expect.equal initContainer.Image "busybox:latest" "Incorrect initContainer image" - Expect.equal initContainer.Name "init" "Incorrect initContainer name" + let container8_2Gb = containerInstance { + name "myapp" + image "myapp:latest" + memory 8.2 } - test "Group without public ip" { - let group = - containerGroup { - name "myGroup" - operating_system Linux - restart_policy RestartOnFailure - add_instances [ appWithoutPorts ] - } + Expect.equal 8.2 container8_2Gb.Memory "Memory rounded incorrectly for 8.2 GB" - Expect.isNone group.IpAddress "IpAddresses should be none" + let container0_2Gb = containerInstance { + name "myapp" + image "myapp:latest" + memory 0.22 } - test "Container with command line arguments" { - let containerInstance = - containerInstance { - name "appWithCommand" - image "myapp:1.7.2" - memory 1.5 - cpu_cores 2 - command_line [ "echo"; "hello world" ] - } - - Expect.equal - containerInstance.Command - [ "echo"; "hello world" ] - "Incorrect container command line arguments" - } + Expect.equal 0.2 container0_2Gb.Memory "Memory rounded incorrectly for 0.2 GB" + } - test "Multiple containers are correctly added" { - let group = containerGroup { add_instances [ nginx; fsharpApp ] } |> asAzureResource + test "Container group with init containers" { + let group = + let emptyDir1 = "emptyDir1" - Expect.hasLength group.Containers 2 "Should be two containers" - Expect.equal group.Containers.[0].Name "nginx" "Incorrect container name" + containerGroup { + name "appWithInitContainers" + add_volumes [ volume_mount.empty_dir emptyDir1 ] - Expect.equal - group.Containers.[0].Image - "nginx:1.17.6-alpine" - "Incorrect image tag generated on first container instance" + add_init_containers [ + initContainer { + name "init" + image "busybox" - Expect.equal group.Containers.[1].Name "fsharpapp" "Incorrect container name" + command_line [ + "/bin/sh" + "-c" + "sleep 60; echo python wordcount.py http://shakespeare.mit.edu/romeo_juliet/full.html > /mnt/emptydir/command_line.txt" + ] - Expect.equal - group.Containers.[1].Image - "myrepo/myapp:1.7.2" - "Incorrect image tag generated on second container instance" + add_volume_mount emptyDir1 "/mnt/emptydir" + } + ] - Expect.equal group.Containers.[1].Resources.Requests.MemoryInGB 1.5 "Incorrect memory" - Expect.equal group.Containers.[1].Resources.Requests.Cpu 2.0 "Incorrect CPU count" + add_instances [ + containerInstance { + name "hamlet" + image "mcr.microsoft.com/azuredocs/aci-wordcount" + add_volume_mount emptyDir1 "/mnt/emptydir" + env_vars [ "NumWords", "3"; "MinLength", "5" ] + } + ] + } + |> asAzureResource + + let containerInstance = group.Containers.[0] + + Expect.equal + containerInstance.Image + "mcr.microsoft.com/azuredocs/aci-wordcount:latest" + "Incorrect containerInstance image" + + Expect.equal containerInstance.Name "hamlet" "Incorrect containerInstance name" + let initContainer = group.InitContainers.[0] + Expect.equal initContainer.Image "busybox:latest" "Incorrect initContainer image" + Expect.equal initContainer.Name "init" "Incorrect initContainer name" + } + + test "Group without public ip" { + let group = containerGroup { + name "myGroup" + operating_system Linux + restart_policy RestartOnFailure + add_instances [ appWithoutPorts ] } - test "Implicitly creates public ports for group based on instances" { - let group = containerGroup { add_instances [ nginx ] } |> asAzureResource + Expect.isNone group.IpAddress "IpAddresses should be none" + } - let ports = group.IpAddress.Ports |> Seq.map (fun p -> p.PortProperty) |> Seq.toList - Expect.equal ports ([ 80; 443 ]) "Incorrect implicitly created public ports" - Expect.hasLength group.Containers.[0].Ports 3 "Incorrect number of private port" + test "Container with command line arguments" { + let containerInstance = containerInstance { + name "appWithCommand" + image "myapp:1.7.2" + memory 1.5 + cpu_cores 2 + command_line [ "echo"; "hello world" ] } - test "Does not create two ports with the same number across public and private" { - let group = - containerGroup { - add_instances - [ - containerInstance { - name "foo" - image "testrepo" - add_ports PublicPort [ 123us ] - add_ports InternalPort [ 123us ] - } - ] - } - |> asAzureResource - - Expect.equal group.IpAddress null "Should not be any public ports" - Expect.equal group.Containers.[0].Ports.[0].Port 123 "Incorrect private port" - Expect.hasLength group.Containers.[0].Ports 1 "Should only be one port" + Expect.equal + containerInstance.Command + [ "echo"; "hello world" ] + "Incorrect container command line arguments" + } + + test "Multiple containers are correctly added" { + let group = containerGroup { add_instances [ nginx; fsharpApp ] } |> asAzureResource + + Expect.hasLength group.Containers 2 "Should be two containers" + Expect.equal group.Containers.[0].Name "nginx" "Incorrect container name" + + Expect.equal + group.Containers.[0].Image + "nginx:1.17.6-alpine" + "Incorrect image tag generated on first container instance" + + Expect.equal group.Containers.[1].Name "fsharpapp" "Incorrect container name" + + Expect.equal + group.Containers.[1].Image + "myrepo/myapp:1.7.2" + "Incorrect image tag generated on second container instance" + + Expect.equal group.Containers.[1].Resources.Requests.MemoryInGB 1.5 "Incorrect memory" + Expect.equal group.Containers.[1].Resources.Requests.Cpu 2.0 "Incorrect CPU count" + } + + test "Implicitly creates public ports for group based on instances" { + let group = containerGroup { add_instances [ nginx ] } |> asAzureResource + + let ports = group.IpAddress.Ports |> Seq.map (fun p -> p.PortProperty) |> Seq.toList + Expect.equal ports ([ 80; 443 ]) "Incorrect implicitly created public ports" + Expect.hasLength group.Containers.[0].Ports 3 "Incorrect number of private port" + } + + test "Does not create two ports with the same number across public and private" { + let group = + containerGroup { + add_instances [ + containerInstance { + name "foo" + image "testrepo" + add_ports PublicPort [ 123us ] + add_ports InternalPort [ 123us ] + } + ] + } + |> asAzureResource + + Expect.equal group.IpAddress null "Should not be any public ports" + Expect.equal group.Containers.[0].Ports.[0].Port 123 "Incorrect private port" + Expect.hasLength group.Containers.[0].Ports 1 "Should only be one port" + } + + test "Adds container group with volumes mounted on each container." { + let helloShared1 = containerInstance { + name "hello-shared-dir1" + image "mcr.microsoft.com/azuredocs/aci-helloworld:latest" + add_ports PublicPort [ 80us ] + add_volume_mount "shared-socket" "/var/lib/shared/hello" + add_volume_mount "source-code" "/src/farmer" + add_volume_mount "secret-files" "/config/secrets" } - test "Adds container group with volumes mounted on each container." { - let helloShared1 = - containerInstance { - name "hello-shared-dir1" - image "mcr.microsoft.com/azuredocs/aci-helloworld:latest" - add_ports PublicPort [ 80us ] - add_volume_mount "shared-socket" "/var/lib/shared/hello" - add_volume_mount "source-code" "/src/farmer" - add_volume_mount "secret-files" "/config/secrets" - } - - let helloShared2 = - containerInstance { - name "hello-shared-dir2" - add_ports PublicPort [ 81us ] - env_vars [ "testing", "environment variables" ] - image "mcr.microsoft.com/azuredocs/aci-helloworld:latest" - add_volume_mount "shared-socket" "/var/lib/shared/hello" - add_volume_mount "azure-file" "/var/lib/files" - } - - let group = - containerGroup { - name "containersWithFiles" - add_instances [ helloShared1; helloShared2 ] - - add_volumes - [ - volume_mount.azureFile "azure-file" "fileShare1" "storageaccount1" - volume_mount.secret_string "secret-files" "secret1" "abcdefg" - volume_mount.empty_dir "shared-socket" - volume_mount.git_repo "source-code" (Uri "https://github.com/CompositionalIT/farmer") - ] - } - |> asAzureResource - - Expect.equal group.Name "containersWithFiles" "Incorrect name on container group" - - Expect.equal - group.Containers.[0].VolumeMounts.Count - 3 - "Incorrect number of volume mounts on container 1" - - Expect.equal - group.Containers.[1].VolumeMounts.Count - 2 - "Incorrect number of volume mounts on container 1" - - Expect.hasLength group.Volumes 4 "Incorrect number of volumes in group" - Expect.isNotNull group.Volumes.[0].AzureFile "Azure file volume should not be null" - Expect.isNotNull group.Volumes.[1].Secret "Secret volume should not be null" - Expect.isNotNull group.Volumes.[2].EmptyDir "Empty directory volume should not be null" - Expect.isNotNull group.Volumes.[3].GitRepo "Git repo volume should not be null" + let helloShared2 = containerInstance { + name "hello-shared-dir2" + add_ports PublicPort [ 81us ] + env_vars [ "testing", "environment variables" ] + image "mcr.microsoft.com/azuredocs/aci-helloworld:latest" + add_volume_mount "shared-socket" "/var/lib/shared/hello" + add_volume_mount "azure-file" "/var/lib/files" } - test "Container group with private registry" { - let managedIdentity = ManagedIdentity.Empty - - let group = - containerGroup { - add_instances [ nginx ] - add_registry_credentials [ registry "my-registry.azurecr.io" "user" managedIdentity ] - } - |> asAzureResource - - Expect.hasLength group.ImageRegistryCredentials 1 "Expected one image registry credential" - let credentials = group.ImageRegistryCredentials.[0] - Expect.equal credentials.Server "my-registry.azurecr.io" "Incorrect container image registry server" - Expect.equal credentials.Username "user" "Incorrect container image registry user" + let group = + containerGroup { + name "containersWithFiles" + add_instances [ helloShared1; helloShared2 ] - Expect.equal - credentials.Password - "[parameters('my-registry.azurecr.io-password')]" - "Container image registry password should be secure parameter" + add_volumes [ + volume_mount.azureFile "azure-file" "fileShare1" "storageaccount1" + volume_mount.secret_string "secret-files" "secret1" "abcdefg" + volume_mount.empty_dir "shared-socket" + volume_mount.git_repo "source-code" (Uri "https://github.com/CompositionalIT/farmer") + ] + } + |> asAzureResource + + Expect.equal group.Name "containersWithFiles" "Incorrect name on container group" + + Expect.equal group.Containers.[0].VolumeMounts.Count 3 "Incorrect number of volume mounts on container 1" + + Expect.equal group.Containers.[1].VolumeMounts.Count 2 "Incorrect number of volume mounts on container 1" + + Expect.hasLength group.Volumes 4 "Incorrect number of volumes in group" + Expect.isNotNull group.Volumes.[0].AzureFile "Azure file volume should not be null" + Expect.isNotNull group.Volumes.[1].Secret "Secret volume should not be null" + Expect.isNotNull group.Volumes.[2].EmptyDir "Empty directory volume should not be null" + Expect.isNotNull group.Volumes.[3].GitRepo "Git repo volume should not be null" + } + + test "Container group with private registry" { + let managedIdentity = ManagedIdentity.Empty + + let group = + containerGroup { + add_instances [ nginx ] + add_registry_credentials [ registry "my-registry.azurecr.io" "user" managedIdentity ] + } + |> asAzureResource + + Expect.hasLength group.ImageRegistryCredentials 1 "Expected one image registry credential" + let credentials = group.ImageRegistryCredentials.[0] + Expect.equal credentials.Server "my-registry.azurecr.io" "Incorrect container image registry server" + Expect.equal credentials.Username "user" "Incorrect container image registry user" + + Expect.equal + credentials.Password + "[parameters('my-registry.azurecr.io-password')]" + "Container image registry password should be secure parameter" + } + + test "Container group with managed identity to private registry" { + let userAssignedIdentity = + ResourceId.create (Arm.ManagedIdentity.userAssignedIdentities, ResourceName "user", "resourceGroup") + |> UserAssignedIdentity + + let managedIdentity = { + ManagedIdentity.Empty with + UserAssigned = [ userAssignedIdentity ] } - test "Container group with managed identity to private registry" { - let userAssignedIdentity = - ResourceId.create (Arm.ManagedIdentity.userAssignedIdentities, ResourceName "user", "resourceGroup") - |> UserAssignedIdentity - - let managedIdentity = - { ManagedIdentity.Empty with - UserAssigned = [ userAssignedIdentity ] - } + let group = + containerGroup { + add_instances [ nginx ] - let group = - containerGroup { - add_instances [ nginx ] - - add_identity ( - ResourceId.create ( - Arm.ManagedIdentity.userAssignedIdentities, - ResourceName "user", - "resourceGroup" - ) - |> UserAssignedIdentity + add_identity ( + ResourceId.create ( + Arm.ManagedIdentity.userAssignedIdentities, + ResourceName "user", + "resourceGroup" ) + |> UserAssignedIdentity + ) - add_managed_identity_registry_credentials - [ registry "my-registry.azurecr.io" "user" managedIdentity ] - } - |> asAzureResource - - Expect.hasLength - group.ImageRegistryCredentials - 1 - "Expected one image managed identity registry credential" + add_managed_identity_registry_credentials [ + registry "my-registry.azurecr.io" "user" managedIdentity + ] + } + |> asAzureResource - let credentials = group.ImageRegistryCredentials.[0] - Expect.equal credentials.Server "my-registry.azurecr.io" "Incorrect container image registry server" - Expect.equal credentials.Username String.Empty "Container image registry user should be null" + Expect.hasLength group.ImageRegistryCredentials 1 "Expected one image managed identity registry credential" - Expect.equal - credentials.Identity - (managedIdentity.Dependencies.Head.ArmExpression.Eval()) - "Incorrect container image registry identity" + let credentials = group.ImageRegistryCredentials.[0] + Expect.equal credentials.Server "my-registry.azurecr.io" "Incorrect container image registry server" + Expect.equal credentials.Username String.Empty "Container image registry user should be null" - Expect.equal credentials.Password null "Container image registry password should be null" - } + Expect.equal + credentials.Identity + (managedIdentity.Dependencies.Head.ArmExpression.Eval()) + "Incorrect container image registry identity" - test "Container group with an link_to_identity to private registry" { - let resourceId = - ResourceId.create (ManagedIdentity.userAssignedIdentities, ResourceName "user", "resourceGroup") + Expect.equal credentials.Password null "Container image registry password should be null" + } - let managedIdentity = - { ManagedIdentity.Empty with - UserAssigned = [ (LinkedUserAssignedIdentity resourceId) ] - } + test "Container group with an link_to_identity to private registry" { + let resourceId = + ResourceId.create (ManagedIdentity.userAssignedIdentities, ResourceName "user", "resourceGroup") - let containerGroupConfig = - containerGroup { - add_instances [ nginx ] - link_to_identity resourceId + let managedIdentity = { + ManagedIdentity.Empty with + UserAssigned = [ (LinkedUserAssignedIdentity resourceId) ] + } - add_managed_identity_registry_credentials - [ registry "my-registry.azurecr.io" "user" managedIdentity ] - } + let containerGroupConfig = containerGroup { + add_instances [ nginx ] + link_to_identity resourceId - let group = containerGroupConfig |> asAzureResource + add_managed_identity_registry_credentials [ registry "my-registry.azurecr.io" "user" managedIdentity ] + } - Expect.hasLength - group.ImageRegistryCredentials - 1 - "Expected one image managed identity registry credential" + let group = containerGroupConfig |> asAzureResource - let credentials = group.ImageRegistryCredentials.[0] - Expect.equal credentials.Server "my-registry.azurecr.io" "Incorrect container image registry server" - Expect.equal credentials.Username String.Empty "Container image registry user should be null" + Expect.hasLength group.ImageRegistryCredentials 1 "Expected one image managed identity registry credential" - Expect.equal - credentials.Identity - (managedIdentity.UserAssigned.Head.ResourceId.ArmExpression.Eval()) - "Incorrect container image registry identity" + let credentials = group.ImageRegistryCredentials.[0] + Expect.equal credentials.Server "my-registry.azurecr.io" "Incorrect container image registry server" + Expect.equal credentials.Username String.Empty "Container image registry user should be null" - Expect.notEqual credentials.Identity null "Identity should not be null" - Expect.notEqual credentials.Identity String.Empty "Identity should not be an empty string" + Expect.equal + credentials.Identity + (managedIdentity.UserAssigned.Head.ResourceId.ArmExpression.Eval()) + "Incorrect container image registry identity" - Expect.equal - containerGroupConfig.Identity.Dependencies.Length - 0 - "Container Group Config Identity Dependencies should be 0" + Expect.notEqual credentials.Identity null "Identity should not be null" + Expect.notEqual credentials.Identity String.Empty "Identity should not be an empty string" - Expect.equal credentials.Password null "Container image registry password should be null" - } + Expect.equal + containerGroupConfig.Identity.Dependencies.Length + 0 + "Container Group Config Identity Dependencies should be 0" - test "Container group with reference to private registry" { - let group = - containerGroup { - add_instances [ nginx ] + Expect.equal credentials.Password null "Container image registry password should be null" + } - reference_registry_credentials - [ - // Reference a container registry in a different resource group. - ResourceId.create ( - Arm.ContainerRegistry.registries, - ResourceName "my-registry", - "my-reg-group" - ) - ] - } - |> asAzureResource + test "Container group with reference to private registry" { + let group = + containerGroup { + add_instances [ nginx ] - Expect.hasLength group.ImageRegistryCredentials 1 "Expected one image registry credential" - let credentials = group.ImageRegistryCredentials.[0] + reference_registry_credentials [ + // Reference a container registry in a different resource group. + ResourceId.create (Arm.ContainerRegistry.registries, ResourceName "my-registry", "my-reg-group") + ] + } + |> asAzureResource + + Expect.hasLength group.ImageRegistryCredentials 1 "Expected one image registry credential" + let credentials = group.ImageRegistryCredentials.[0] + + Expect.equal + credentials.Server + "[reference(resourceId('my-reg-group', 'Microsoft.ContainerRegistry/registries', 'my-registry'), '2019-05-01').loginServer]" + "Image registry server should come from 'reference'" + + Expect.equal + credentials.Username + "[listCredentials(resourceId('my-reg-group', 'Microsoft.ContainerRegistry/registries', 'my-registry'), '2019-05-01').username]" + "mage registry user should come from 'listCredentials'" + + Expect.equal + credentials.Password + "[listCredentials(resourceId('my-reg-group', 'Microsoft.ContainerRegistry/registries', 'my-registry'), '2019-05-01').passwords[0].value]" + "Image registry password should come from listCredentials" + } + + test "Container group with system assigned identity" { + let group = + containerGroup { + name "myapp" + add_instances [ nginx ] + system_identity + } + |> asAzureResource + + Expect.isTrue group.Identity.Type.HasValue "Expecting an assigned identity." + + Expect.equal + group.Identity.Type.Value + ResourceIdentityType.SystemAssigned + "Expecting a system assigned identity" + } + + test "Container group with user assigned identity" { + let group = + containerGroup { + name "myapp" + add_instances [ nginx ] + + add_identity ( + ResourceId.create ( + Arm.ManagedIdentity.userAssignedIdentities, + ResourceName "user", + "resourceGroup" + ) + |> UserAssignedIdentity + ) + } + |> asAzureResource - Expect.equal - credentials.Server - "[reference(resourceId('my-reg-group', 'Microsoft.ContainerRegistry/registries', 'my-registry'), '2019-05-01').loginServer]" - "Image registry server should come from 'reference'" + Expect.hasLength group.Identity.UserAssignedIdentities 1 "No user assigned identity." + } - Expect.equal - credentials.Username - "[listCredentials(resourceId('my-reg-group', 'Microsoft.ContainerRegistry/registries', 'my-registry'), '2019-05-01').username]" - "mage registry user should come from 'listCredentials'" + test "Make container group with MSI" { + let msi = createUserAssignedIdentity "aciUser" - Expect.equal - credentials.Password - "[listCredentials(resourceId('my-reg-group', 'Microsoft.ContainerRegistry/registries', 'my-registry'), '2019-05-01').passwords[0].value]" - "Image registry password should come from listCredentials" + let group = containerGroup { + name "myapp-with-msi" + add_instances [ nginx ] + add_identity msi } - test "Container group with system assigned identity" { - let group = - containerGroup { - name "myapp" - add_instances [ nginx ] - system_identity - } - |> asAzureResource - - Expect.isTrue group.Identity.Type.HasValue "Expecting an assigned identity." - - Expect.equal - group.Identity.Type.Value - ResourceIdentityType.SystemAssigned - "Expecting a system assigned identity" + let template = arm { + location Location.EastUS + add_resource msi + add_resource group } - test "Container group with user assigned identity" { - let group = - containerGroup { - name "myapp" - add_instances [ nginx ] - - add_identity ( - ResourceId.create ( - Arm.ManagedIdentity.userAssignedIdentities, - ResourceName "user", - "resourceGroup" - ) - |> UserAssignedIdentity - ) + let containerGroup = + template.Template.Resources + |> List.find (fun r -> r.ResourceId.Name.Value = "myapp-with-msi") + :?> Farmer.Arm.ContainerInstance.ContainerGroup + + Expect.isNonEmpty containerGroup.Identity.UserAssigned "Container group did not have identity" + + Expect.equal + containerGroup.Identity.UserAssigned.[0] + (UserAssignedIdentity( + ResourceId.create (Arm.ManagedIdentity.userAssignedIdentities, ResourceName "aciUser") + )) + "Expected user identity named 'aciUser'." + } + test "Secure environment variables are generated correctly" { + let cg = containerGroup { + name "myapp" + + add_instances [ + containerInstance { + name "nginx" + image "nginx:1.17.6-alpine" + env_vars [ EnvVar.createSecure "foo" "secret-foo" ] } - |> asAzureResource - - Expect.hasLength group.Identity.UserAssignedIdentities 1 "No user assigned identity." + ] } - test "Make container group with MSI" { - let msi = createUserAssignedIdentity "aciUser" + let deployment = arm { add_resource cg } - let group = - containerGroup { - name "myapp-with-msi" - add_instances [ nginx ] - add_identity msi - } + Expect.hasLength deployment.Template.Parameters 1 "Should have a secure parameter for environment variable" - let template = - arm { - location Location.EastUS - add_resource msi - add_resource group - } - - let containerGroup = - template.Template.Resources - |> List.find (fun r -> r.ResourceId.Name.Value = "myapp-with-msi") - :?> Farmer.Arm.ContainerInstance.ContainerGroup + Expect.equal + (deployment.Template.Parameters.Head.ArmExpression.Eval()) + "[parameters('secret-foo')]" + "Generated incorrect secure parameter." + } + test "Secure environment variables are generated for init containers" { + let cg = containerGroup { + name "myapp" - Expect.isNonEmpty containerGroup.Identity.UserAssigned "Container group did not have identity" - - Expect.equal - containerGroup.Identity.UserAssigned.[0] - (UserAssignedIdentity( - ResourceId.create (Arm.ManagedIdentity.userAssignedIdentities, ResourceName "aciUser") - )) - "Expected user identity named 'aciUser'." - } - test "Secure environment variables are generated correctly" { - let cg = - containerGroup { - name "myapp" - - add_instances - [ - containerInstance { - name "nginx" - image "nginx:1.17.6-alpine" - env_vars [ EnvVar.createSecure "foo" "secret-foo" ] - } - ] + add_init_containers [ + initContainer { + name "nginx" + image "nginx:1.17.6-alpine" + env_vars [ EnvVar.createSecure "foo" "secret-init" ] } - - let deployment = arm { add_resource cg } - - Expect.hasLength - deployment.Template.Parameters - 1 - "Should have a secure parameter for environment variable" - - Expect.equal - (deployment.Template.Parameters.Head.ArmExpression.Eval()) - "[parameters('secret-foo')]" - "Generated incorrect secure parameter." + ] } - test "Secure environment variables are generated for init containers" { - let cg = - containerGroup { - name "myapp" - - add_init_containers - [ - initContainer { - name "nginx" - image "nginx:1.17.6-alpine" - env_vars [ EnvVar.createSecure "foo" "secret-init" ] - } - ] - } - let deployment = arm { add_resource cg } + let deployment = arm { add_resource cg } - Expect.hasLength - deployment.Template.Parameters - 1 - "Should have a secure parameter for initContainer environment variable" + Expect.hasLength + deployment.Template.Parameters + 1 + "Should have a secure parameter for initContainer environment variable" - Expect.equal - (deployment.Template.Parameters.Head.ArmExpression.Eval()) - "[parameters('secret-init')]" - "Generated incorrect secure parameter." - } - test "Secure parameters for secret volume is generated correctly" { - let cg = - containerGroup { - name "myapp" + Expect.equal + (deployment.Template.Parameters.Head.ArmExpression.Eval()) + "[parameters('secret-init')]" + "Generated incorrect secure parameter." + } + test "Secure parameters for secret volume is generated correctly" { + let cg = containerGroup { + name "myapp" - add_instances - [ - containerInstance { - name "nginx" - image "nginx:1.17.6-alpine" - add_volume_mount "secrets" "/config/secrets" - } - ] - - add_volumes [ volume_mount.secret_parameter "secrets" "foo" "secret-foo" ] - } - - let deployment = - arm { - location Location.EastUS - add_resource cg + add_instances [ + containerInstance { + name "nginx" + image "nginx:1.17.6-alpine" + add_volume_mount "secrets" "/config/secrets" } + ] - Expect.hasLength deployment.Template.Parameters 1 "Should have a secure parameter for secret volume" + add_volumes [ volume_mount.secret_parameter "secrets" "foo" "secret-foo" ] + } - Expect.equal - (deployment.Template.Parameters.Head.ArmExpression.Eval()) - "[parameters('secret-foo')]" - "Generated incorrect secure parameter." + let deployment = arm { + location Location.EastUS + add_resource cg } - /// Test creates a storage account and container group where the storage account connection string - /// is passed as an ARM expression in a secure environment variable. - test "Secure environment variables created from ARM expressions" { - let script = - """ + + Expect.hasLength deployment.Template.Parameters 1 "Should have a secure parameter for secret volume" + + Expect.equal + (deployment.Template.Parameters.Head.ArmExpression.Eval()) + "[parameters('secret-foo')]" + "Generated incorrect secure parameter." + } + /// Test creates a storage account and container group where the storage account connection string + /// is passed as an ARM expression in a secure environment variable. + test "Secure environment variables created from ARM expressions" { + let script = + """ #r "nuget: Azure.Storage.Blobs" open System @@ -625,771 +577,718 @@ async { } |> Async.RunSynchronously """ - let storage = storageAccount { name "containerdata1234" } + let storage = storageAccount { name "containerdata1234" } - let app = - containerGroup { - name "myapp" - depends_on storage - - add_instances - [ - containerInstance { - name "app" - image "mcr.microsoft.com/dotnet/sdk:5.0" - add_volume_mount "script" "/app/src" - command_line ("dotnet fsi /app/src/main.fsx".Split null |> List.ofArray) - - env_vars - [ EnvVar.createSecureExpression "AZURE_STORAGE_CONNECTION_STRING" storage.Key ] - } - ] + let app = containerGroup { + name "myapp" + depends_on storage - add_volumes [ volume_mount.secret_string "script" "main.fsx" script ] - } + add_instances [ + containerInstance { + name "app" + image "mcr.microsoft.com/dotnet/sdk:5.0" + add_volume_mount "script" "/app/src" + command_line ("dotnet fsi /app/src/main.fsx".Split null |> List.ofArray) - let deployment = - arm { - location Location.EastUS - add_resources [ storage; app ] + env_vars [ EnvVar.createSecureExpression "AZURE_STORAGE_CONNECTION_STRING" storage.Key ] } + ] - let json = deployment.Template |> Writer.toJson - let jobj = JObject.Parse json - let parameters = jobj.["parameters"] - Expect.hasLength parameters 0 "Expected no parameters emitted with a SecureEnvExpression" - - let envVars = - jobj.SelectToken( - "$.resources[?(@.name=='myapp')].properties.containers[?(@.name=='app')].properties.environmentVariables" - ) - :?> JArray - - Expect.hasLength envVars 1 "Expected to have an environment variable on the 'app' container" - let firstEnvVar = envVars.[0] - Expect.equal (firstEnvVar.["name"] |> string) "AZURE_STORAGE_CONNECTION_STRING" "Incorrect env var name" - - Expect.equal - (firstEnvVar.["secureValue"] |> string) - "[concat('DefaultEndpointsProtocol=https;AccountName=containerdata1234;AccountKey=', listKeys(resourceId('Microsoft.Storage/storageAccounts', 'containerdata1234'), '2017-10-01').keys[0].value)]" - "Incorrect env var expression value" + add_volumes [ volume_mount.secret_string "script" "main.fsx" script ] } - test "Container with liveness and readiness probes" { - - let cg = - containerGroup { - name "myapp" - add_instances - [ - containerInstance { - name "nginx" - image "nginx:1.17.6-alpine" - - probes - [ - liveness { - http "https://whatever.com:8080/healthcheck" - period_seconds 30 // Wait 30 seconds between each liveness check - failure_threshold 10 // After 10 tries, consider this unhealthy - } - readiness { - http "https://whatever.com:8080/healthcheck" - initial_delay_seconds 30 // Wait 30 seconds after the container is started before a readiness check - failure_threshold 5 // Let it retry 5 times, giving another 50 seconds to try to start - } - ] - } - ] - } - |> asAzureResource - - let livenessProbe = cg.Containers.[0].LivenessProbe - Expect.isNotNull livenessProbe "Resulting container should have a liveness probe" - Expect.equal livenessProbe.HttpGet.Path "/healthcheck" "Incorrect path on liveness http probe" - Expect.equal livenessProbe.HttpGet.Port 8080 "Incorrect port on liveness http probe" - Expect.equal livenessProbe.HttpGet.Scheme "https" "Incorrect scheme on liveness http probe" - Expect.equal livenessProbe.PeriodSeconds.Value 30 "Incorrect period on liveness probe" - Expect.equal livenessProbe.FailureThreshold.Value 10 "Incorrect failure threshold on liveness probe" - let readinessProbe = cg.Containers.[0].ReadinessProbe - Expect.isNotNull readinessProbe "Resulting container should have a readiness probe" - Expect.equal readinessProbe.HttpGet.Path "/healthcheck" "Incorrect path on readiness http probe" - Expect.equal readinessProbe.HttpGet.Port 8080 "Incorrect port on readiness http probe" - Expect.equal readinessProbe.HttpGet.Scheme "https" "Incorrect scheme on readiness http probe" - - Expect.equal - readinessProbe.InitialDelaySeconds.Value - 30 - "Incorrect initial delay threshold on readiness probe" - - Expect.equal readinessProbe.FailureThreshold.Value 5 "Incorrect failure threshold on readiness probe" + let deployment = arm { + location Location.EastUS + add_resources [ storage; app ] } - test "Container group with vnet and subnet has subnetIds and expected dependsOn" { - let template = - arm { - add_resources - [ - vnet { - name "containernet" - add_address_spaces [ "10.30.32.0/20" ] - - add_subnets - [ - subnet { - name "ContainerSubnet" - prefix "10.30.41.0/24" - add_delegations [ SubnetDelegationService.ContainerGroups ] - } - ] + + let json = deployment.Template |> Writer.toJson + let jobj = JObject.Parse json + let parameters = jobj.["parameters"] + Expect.hasLength parameters 0 "Expected no parameters emitted with a SecureEnvExpression" + + let envVars = + jobj.SelectToken( + "$.resources[?(@.name=='myapp')].properties.containers[?(@.name=='app')].properties.environmentVariables" + ) + :?> JArray + + Expect.hasLength envVars 1 "Expected to have an environment variable on the 'app' container" + let firstEnvVar = envVars.[0] + Expect.equal (firstEnvVar.["name"] |> string) "AZURE_STORAGE_CONNECTION_STRING" "Incorrect env var name" + + Expect.equal + (firstEnvVar.["secureValue"] |> string) + "[concat('DefaultEndpointsProtocol=https;AccountName=containerdata1234;AccountKey=', listKeys(resourceId('Microsoft.Storage/storageAccounts', 'containerdata1234'), '2017-10-01').keys[0].value)]" + "Incorrect env var expression value" + } + test "Container with liveness and readiness probes" { + + let cg = + containerGroup { + name "myapp" + + add_instances [ + containerInstance { + name "nginx" + image "nginx:1.17.6-alpine" + + probes [ + liveness { + http "https://whatever.com:8080/healthcheck" + period_seconds 30 // Wait 30 seconds between each liveness check + failure_threshold 10 // After 10 tries, consider this unhealthy } - containerGroup { - name "appWithHttpFrontend" - operating_system Linux - restart_policy AlwaysRestart - add_instances [ nginx ] - vnet "containernet" - subnet "ContainerSubnet" + readiness { + http "https://whatever.com:8080/healthcheck" + initial_delay_seconds 30 // Wait 30 seconds after the container is started before a readiness check + failure_threshold 5 // Let it retry 5 times, giving another 50 seconds to try to start } ] + } + ] + } + |> asAzureResource + + let livenessProbe = cg.Containers.[0].LivenessProbe + Expect.isNotNull livenessProbe "Resulting container should have a liveness probe" + Expect.equal livenessProbe.HttpGet.Path "/healthcheck" "Incorrect path on liveness http probe" + Expect.equal livenessProbe.HttpGet.Port 8080 "Incorrect port on liveness http probe" + Expect.equal livenessProbe.HttpGet.Scheme "https" "Incorrect scheme on liveness http probe" + Expect.equal livenessProbe.PeriodSeconds.Value 30 "Incorrect period on liveness probe" + Expect.equal livenessProbe.FailureThreshold.Value 10 "Incorrect failure threshold on liveness probe" + let readinessProbe = cg.Containers.[0].ReadinessProbe + Expect.isNotNull readinessProbe "Resulting container should have a readiness probe" + Expect.equal readinessProbe.HttpGet.Path "/healthcheck" "Incorrect path on readiness http probe" + Expect.equal readinessProbe.HttpGet.Port 8080 "Incorrect port on readiness http probe" + Expect.equal readinessProbe.HttpGet.Scheme "https" "Incorrect scheme on readiness http probe" + + Expect.equal + readinessProbe.InitialDelaySeconds.Value + 30 + "Incorrect initial delay threshold on readiness probe" + + Expect.equal readinessProbe.FailureThreshold.Value 5 "Incorrect failure threshold on readiness probe" + } + test "Container group with vnet and subnet has subnetIds and expected dependsOn" { + let template = arm { + add_resources [ + vnet { + name "containernet" + add_address_spaces [ "10.30.32.0/20" ] + + add_subnets [ + subnet { + name "ContainerSubnet" + prefix "10.30.41.0/24" + add_delegations [ SubnetDelegationService.ContainerGroups ] + } + ] } + containerGroup { + name "appWithHttpFrontend" + operating_system Linux + restart_policy AlwaysRestart + add_instances [ nginx ] + vnet "containernet" + subnet "ContainerSubnet" + } + ] + } - let json = template.Template |> Writer.toJson - let jobj = JObject.Parse json - - let containerGroupJson = - jobj.SelectToken("resources[?(@.name=='appWithHttpFrontend')]") - - let apiVersion = containerGroupJson.["apiVersion"] |> string - let apiDate = DateOnly.Parse apiVersion - - Expect.isGreaterThanOrEqual - apiDate - (DateOnly.Parse "2021-07-01") - "Expecting minimum version of 2021-07-01 for 'subnetIds' support" - - let subnetIds = containerGroupJson.SelectToken("properties.subnetIds") :?> JArray - Expect.hasLength subnetIds 1 "Incorrect number of subnetIds" + let json = template.Template |> Writer.toJson + let jobj = JObject.Parse json + + let containerGroupJson = + jobj.SelectToken("resources[?(@.name=='appWithHttpFrontend')]") + + let apiVersion = containerGroupJson.["apiVersion"] |> string + let apiDate = DateOnly.Parse apiVersion + + Expect.isGreaterThanOrEqual + apiDate + (DateOnly.Parse "2021-07-01") + "Expecting minimum version of 2021-07-01 for 'subnetIds' support" + + let subnetIds = containerGroupJson.SelectToken("properties.subnetIds") :?> JArray + Expect.hasLength subnetIds 1 "Incorrect number of subnetIds" + + let expectedSubnetId = + "[resourceId('Microsoft.Network/virtualNetworks/subnets', 'containernet', 'ContainerSubnet')]" + + let firstSubnetId = string subnetIds.First.["id"] + Expect.equal firstSubnetId expectedSubnetId "Subnet ID not in 'subnetIds'" + let dependsOn = containerGroupJson.SelectToken("dependsOn") :?> JArray + + let expectedContainerNetDeps = + "[resourceId('Microsoft.Network/virtualNetworks', 'containernet')]" + + Expect.hasLength dependsOn 1 "containerGroup has wrong number of dependencies" + let actualContainerNetDeps = string dependsOn.First + Expect.equal actualContainerNetDeps expectedContainerNetDeps "Dependencies didn't match" + } + test "Container groups with subnetIds and netprofile uses correct API versions" { + let template = arm { + add_resources [ + vnet { + name "containernet" + add_address_spaces [ "10.30.32.0/20" ] + + add_subnets [ + subnet { + name "ContainerSubnet" + prefix "10.30.41.0/24" + add_delegations [ SubnetDelegationService.ContainerGroups ] + } + ] + } + networkProfile { + name "netprofile" + vnet "containernet" + subnet "ContainerSubnet" + } + containerGroup { + name "appWithNetProfile" + operating_system Linux + restart_policy AlwaysRestart - let expectedSubnetId = - "[resourceId('Microsoft.Network/virtualNetworks/subnets', 'containernet', 'ContainerSubnet')]" + add_instances [ + containerInstance { + name "nginx" + image "nginx:1.21.6-alpine" + } + ] - let firstSubnetId = string subnetIds.First.["id"] - Expect.equal firstSubnetId expectedSubnetId "Subnet ID not in 'subnetIds'" - let dependsOn = containerGroupJson.SelectToken("dependsOn") :?> JArray + network_profile "netprofile" + } + containerGroup { + name "appWithSubnetIds" + operating_system Linux + restart_policy AlwaysRestart - let expectedContainerNetDeps = - "[resourceId('Microsoft.Network/virtualNetworks', 'containernet')]" + add_instances [ + containerInstance { + name "nginx" + image "nginx:1.21.6-alpine" + } + ] - Expect.hasLength dependsOn 1 "containerGroup has wrong number of dependencies" - let actualContainerNetDeps = string dependsOn.First - Expect.equal actualContainerNetDeps expectedContainerNetDeps "Dependencies didn't match" + vnet "containernet" + subnet "ContainerSubnet" + } + ] } - test "Container groups with subnetIds and netprofile uses correct API versions" { - let template = - arm { - add_resources - [ - vnet { - name "containernet" - add_address_spaces [ "10.30.32.0/20" ] - - add_subnets - [ - subnet { - name "ContainerSubnet" - prefix "10.30.41.0/24" - add_delegations [ SubnetDelegationService.ContainerGroups ] - } - ] - } - networkProfile { - name "netprofile" - vnet "containernet" - subnet "ContainerSubnet" - } - containerGroup { - name "appWithNetProfile" - operating_system Linux - restart_policy AlwaysRestart - - add_instances - [ - containerInstance { - name "nginx" - image "nginx:1.21.6-alpine" - } - ] - - network_profile "netprofile" - } - containerGroup { - name "appWithSubnetIds" - operating_system Linux - restart_policy AlwaysRestart - - add_instances - [ - containerInstance { - name "nginx" - image "nginx:1.21.6-alpine" - } - ] - - vnet "containernet" - subnet "ContainerSubnet" - } - ] + + let jobj = template.Template |> Writer.toJson |> JObject.Parse + + let containerGroupNetProfile = + jobj.SelectToken("resources[?(@.name=='appWithNetProfile')]") + + let netProfileApiVersion = + containerGroupNetProfile.["apiVersion"] |> string |> DateOnly.Parse + + Expect.isLessThanOrEqual + netProfileApiVersion + (DateOnly.Parse "2021-03-01") + "Expecting maximum version of 2021-03-01 for 'networkProfile' support" + + let containerGroupSubnetIds = + jobj.SelectToken("resources[?(@.name=='appWithSubnetIds')]") + + let subnetIdsApiVersion = + containerGroupSubnetIds.["apiVersion"] |> string |> DateOnly.Parse + + Expect.isGreaterThanOrEqual + subnetIdsApiVersion + (DateOnly.Parse "2021-07-01") + "Expecting minimum version of 2021-07-01 for 'subnetIds' support" + } + test "Container network profile with vnet has expected dependsOn" { + let template = arm { + add_resources [ + vnet { + name "containernet" + add_address_spaces [ "10.30.32.0/20" ] + + add_subnets [ + subnet { + name "ContainerSubnet" + prefix "10.30.41.0/24" + add_delegations [ SubnetDelegationService.ContainerGroups ] + } + ] + } + networkProfile { + name "netprofile" + vnet "containernet" + subnet "ContainerSubnet" } + containerGroup { + name "appWithHttpFrontend" + operating_system Linux + restart_policy AlwaysRestart + add_instances [ nginx ] + network_profile "netprofile" + } + ] + } - let jobj = template.Template |> Writer.toJson |> JObject.Parse + let jobj = template.Template |> Writer.toJson |> JObject.Parse - let containerGroupNetProfile = - jobj.SelectToken("resources[?(@.name=='appWithNetProfile')]") + let containerGroupJson = + jobj.SelectToken("resources[?(@.name=='appWithHttpFrontend')]") - let netProfileApiVersion = - containerGroupNetProfile.["apiVersion"] |> string |> DateOnly.Parse + let apiVersion = containerGroupJson.["apiVersion"] |> string + let apiDate = DateOnly.Parse apiVersion - Expect.isLessThanOrEqual - netProfileApiVersion - (DateOnly.Parse "2021-03-01") - "Expecting maximum version of 2021-03-01 for 'networkProfile' support" + Expect.isLessThanOrEqual + apiDate + (DateOnly.Parse "2021-03-01") + "Expecting maximum version of 2021-03-01 for 'networkProfile' support" - let containerGroupSubnetIds = - jobj.SelectToken("resources[?(@.name=='appWithSubnetIds')]") + let expectedContainerNetDeps = + "[resourceId('Microsoft.Network/virtualNetworks', 'containernet')]" - let subnetIdsApiVersion = - containerGroupSubnetIds.["apiVersion"] |> string |> DateOnly.Parse + let dependsOn = jobj.SelectToken("resources[?(@.name=='netprofile')].dependsOn") + Expect.hasLength dependsOn 1 "netprofile has wrong number of dependencies" - Expect.isGreaterThanOrEqual - subnetIdsApiVersion - (DateOnly.Parse "2021-07-01") - "Expecting minimum version of 2021-07-01 for 'subnetIds' support" - } - test "Container network profile with vnet has expected dependsOn" { - let template = - arm { - add_resources - [ - vnet { - name "containernet" - add_address_spaces [ "10.30.32.0/20" ] - - add_subnets - [ - subnet { - name "ContainerSubnet" - prefix "10.30.41.0/24" - add_delegations [ SubnetDelegationService.ContainerGroups ] - } - ] - } - networkProfile { - name "netprofile" - vnet "containernet" - subnet "ContainerSubnet" - } - containerGroup { - name "appWithHttpFrontend" - operating_system Linux - restart_policy AlwaysRestart - add_instances [ nginx ] - network_profile "netprofile" - } - ] + let actualContainerNetDeps = + (dependsOn :?> Newtonsoft.Json.Linq.JArray).First.ToString() + + Expect.equal actualContainerNetDeps expectedContainerNetDeps "Dependencies didn't match" + } + test "Container network profile with linked vnet has empty dependsOn" { + let template = arm { + add_resources [ + networkProfile { + name "netprofile" + link_to_vnet "containernet" + subnet "ContainerSubnet" } + containerGroup { + name "appWithHttpFrontend" + operating_system Linux + restart_policy AlwaysRestart + add_instances [ nginx ] + network_profile "netprofile" + } + ] + } - let jobj = template.Template |> Writer.toJson |> JObject.Parse + let json = template.Template |> Writer.toJson + let jobj = Newtonsoft.Json.Linq.JObject.Parse(json) + let dependsOn = jobj.SelectToken("resources[?(@.name=='netprofile')].dependsOn") + Expect.hasLength dependsOn 0 "network profile had dependencies when existing vnet was linked" + } + test "Container network profile with linked vnet in another resource group has empty dependsOn" { + let template = arm { + add_resources [ + networkProfile { + name "netprofile" + + link_to_vnet ( + ResourceId.create ( + virtualNetworks, + (ResourceName "containerNet"), + group = "other-res-group" + ) + ) - let containerGroupJson = - jobj.SelectToken("resources[?(@.name=='appWithHttpFrontend')]") + subnet "ContainerSubnet" + } + ] + } - let apiVersion = containerGroupJson.["apiVersion"] |> string - let apiDate = DateOnly.Parse apiVersion + let json = template.Template |> Writer.toJson + let jobj = Newtonsoft.Json.Linq.JObject.Parse(json) + let subnetId = jobj.SelectToken("..subnet.id") |> string + + let expectedSubnetId = + "[resourceId('other-res-group', 'Microsoft.Network/virtualNetworks/subnets', 'containerNet', 'ContainerSubnet')]" + + Expect.equal subnetId expectedSubnetId "Generated incorrect subnet ID." + } + test "Container network profile allows naming of ip configs" { + let template = arm { + add_resources [ + vnet { + name "containernet" + add_address_spaces [ "10.30.32.0/20" ] + + add_subnets [ + subnet { + name "ContainerSubnet" + prefix "10.30.41.0/24" + add_delegations [ SubnetDelegationService.ContainerGroups ] + } + ] + } + networkProfile { + name "netprofile" + vnet "containernet" + ip_config "ipconfigProfile" "ContainerSubnet" + } + ] + } - Expect.isLessThanOrEqual - apiDate - (DateOnly.Parse "2021-03-01") - "Expecting maximum version of 2021-03-01 for 'networkProfile' support" + let json = template.Template |> Writer.toJson + let jobj = Newtonsoft.Json.Linq.JObject.Parse(json) - let expectedContainerNetDeps = - "[resourceId('Microsoft.Network/virtualNetworks', 'containernet')]" + let ipConfigName = + jobj.SelectToken( + "resources[?(@.name=='netprofile')].properties.containerNetworkInterfaceConfigurations[0].properties.ipConfigurations[0].name" + ) - let dependsOn = jobj.SelectToken("resources[?(@.name=='netprofile')].dependsOn") - Expect.hasLength dependsOn 1 "netprofile has wrong number of dependencies" + Expect.equal (string ipConfigName) "ipconfigProfile" "netprofile ipConfiguration has wrong name" + } - let actualContainerNetDeps = - (dependsOn :?> Newtonsoft.Json.Linq.JArray).First.ToString() + test "Support for additional dependencies" { + let storage = storageAccount { name "containerstorage" } - Expect.equal actualContainerNetDeps expectedContainerNetDeps "Dependencies didn't match" + let myGroup = containerGroup { + name "myContainerGroup" + depends_on storage } - test "Container network profile with linked vnet has empty dependsOn" { - let template = - arm { - add_resources - [ - networkProfile { - name "netprofile" - link_to_vnet "containernet" - subnet "ContainerSubnet" - } - containerGroup { - name "appWithHttpFrontend" - operating_system Linux - restart_policy AlwaysRestart - add_instances [ nginx ] - network_profile "netprofile" - } - ] - } - let json = template.Template |> Writer.toJson - let jobj = Newtonsoft.Json.Linq.JObject.Parse(json) - let dependsOn = jobj.SelectToken("resources[?(@.name=='netprofile')].dependsOn") - Expect.hasLength dependsOn 0 "network profile had dependencies when existing vnet was linked" - } - test "Container network profile with linked vnet in another resource group has empty dependsOn" { - let template = - arm { - add_resources - [ - networkProfile { - name "netprofile" - - link_to_vnet ( - ResourceId.create ( - virtualNetworks, - (ResourceName "containerNet"), - group = "other-res-group" - ) - ) - - subnet "ContainerSubnet" + let template = arm { add_resources [ storage; myGroup ] } + let json = template.Template |> Writer.toJson + let jobj = json |> Newtonsoft.Json.Linq.JObject.Parse + + let dependencies = + jobj.SelectToken "resources[?(@.name=='myContainerGroup')].dependsOn" + + Expect.sequenceEqual + dependencies + [ + JValue "[resourceId('Microsoft.Storage/storageAccounts', 'containerstorage')]" + ] + "Did not have correct dependencies" + } + + test "Adds GPU to container instance" { + let group = + containerGroup { + add_instances [ + containerInstance { + name "foo" + image "myrepo/gpucontainers" + + gpu ( + containerInstanceGpu { + count 1 + sku Gpu.V100 } - ] - } - - let json = template.Template |> Writer.toJson - let jobj = Newtonsoft.Json.Linq.JObject.Parse(json) - let subnetId = jobj.SelectToken("..subnet.id") |> string + ) + } + ] + } + |> asAzureResource + + let container = group.Containers |> Seq.head + let gpu = container.Resources.Requests.Gpu + Expect.equal gpu.Count 1 "Wrong amount of GPUs" + Expect.equal gpu.Sku "V100" "Wrong SKU" + Expect.equal container.Image "myrepo/gpucontainers:latest" "Incorrect image tag" + } + + test "Container group created in a specific zone" { + let deployment = arm { + add_resources [ + containerGroup { + name "zonal-container-group" - let expectedSubnetId = - "[resourceId('other-res-group', 'Microsoft.Network/virtualNetworks/subnets', 'containerNet', 'ContainerSubnet')]" + add_instances [ + containerInstance { + name "httpserver" + image "nginx" + } + ] - Expect.equal subnetId expectedSubnetId "Generated incorrect subnet ID." - } - test "Container network profile allows naming of ip configs" { - let template = - arm { - add_resources - [ - vnet { - name "containernet" - add_address_spaces [ "10.30.32.0/20" ] - - add_subnets - [ - subnet { - name "ContainerSubnet" - prefix "10.30.41.0/24" - add_delegations [ SubnetDelegationService.ContainerGroups ] - } - ] - } - networkProfile { - name "netprofile" - vnet "containernet" - ip_config "ipconfigProfile" "ContainerSubnet" - } - ] + availability_zone "2" } - - let json = template.Template |> Writer.toJson - let jobj = Newtonsoft.Json.Linq.JObject.Parse(json) - - let ipConfigName = - jobj.SelectToken( - "resources[?(@.name=='netprofile')].properties.containerNetworkInterfaceConfigurations[0].properties.ipConfigurations[0].name" - ) - - Expect.equal (string ipConfigName) "ipconfigProfile" "netprofile ipConfiguration has wrong name" + ] } - test "Support for additional dependencies" { - let storage = storageAccount { name "containerstorage" } + let jobj = deployment.Template |> Writer.toJson |> JObject.Parse - let myGroup = - containerGroup { - name "myContainerGroup" - depends_on storage - } + let containerGroupJson = + jobj.SelectToken("resources[?(@.name=='zonal-container-group')]") - let template = arm { add_resources [ storage; myGroup ] } - let json = template.Template |> Writer.toJson - let jobj = json |> Newtonsoft.Json.Linq.JObject.Parse + let zones = containerGroupJson.SelectToken "zones" + let apiVersion = containerGroupJson.["apiVersion"] |> string + let apiDate = DateOnly.Parse apiVersion - let dependencies = - jobj.SelectToken "resources[?(@.name=='myContainerGroup')].dependsOn" + Expect.isGreaterThanOrEqual + apiDate + (DateOnly.Parse "2021-09-01") + "Expecting minimum version of 2021-09-01 for 'zones' support" - Expect.sequenceEqual - dependencies - [ - JValue "[resourceId('Microsoft.Storage/storageAccounts', 'containerstorage')]" - ] - "Did not have correct dependencies" - } + Expect.hasLength zones 1 "Incorrect number of zones" + Expect.sequenceEqual zones [ JValue "2" ] "Incorrect value for zone" + } - test "Adds GPU to container instance" { - let group = - containerGroup { - add_instances - [ - containerInstance { - name "foo" - image "myrepo/gpucontainers" - - gpu ( - containerInstanceGpu { - count 1 - sku Gpu.V100 - } - ) - } - ] - } - |> asAzureResource + test "Enable container logging workspace" { + let deployment = + let workspace = logAnalytics { name "containergrouplogs1234" } - let container = group.Containers |> Seq.head - let gpu = container.Resources.Requests.Gpu - Expect.equal gpu.Count 1 "Wrong amount of GPUs" - Expect.equal gpu.Sku "V100" "Wrong SKU" - Expect.equal container.Image "myrepo/gpucontainers:latest" "Incorrect image tag" - } + arm { + add_resources [ + workspace + containerGroup { + name "container-group-with-insights" - test "Container group created in a specific zone" { - let deployment = - arm { - add_resources - [ - containerGroup { - name "zonal-container-group" - - add_instances - [ - containerInstance { - name "httpserver" - image "nginx" - } - ] - - availability_zone "2" + add_instances [ + containerInstance { + name "httpserver" + image "nginx" } ] - } - let jobj = deployment.Template |> Writer.toJson |> JObject.Parse + diagnostics_workspace LogType.ContainerInstanceLogs workspace + } + ] + } - let containerGroupJson = - jobj.SelectToken("resources[?(@.name=='zonal-container-group')]") + let jobj = deployment.Template |> Writer.toJson |> JObject.Parse - let zones = containerGroupJson.SelectToken "zones" - let apiVersion = containerGroupJson.["apiVersion"] |> string - let apiDate = DateOnly.Parse apiVersion + let logAnalytics = + jobj.SelectToken + "resources[?(@.name=='container-group-with-insights')].properties.diagnostics.logAnalytics" - Expect.isGreaterThanOrEqual - apiDate - (DateOnly.Parse "2021-09-01") - "Expecting minimum version of 2021-09-01 for 'zones' support" + let workspaceId = logAnalytics.SelectToken "workspaceId" + let workspaceKey = logAnalytics.SelectToken "workspaceKey" + let logType = logAnalytics.SelectToken "logType" - Expect.hasLength zones 1 "Incorrect number of zones" - Expect.sequenceEqual zones [ JValue "2" ] "Incorrect value for zone" - } + Expect.equal + (string workspaceId) + "[reference(resourceId('Microsoft.OperationalInsights/workspaces', 'containergrouplogs1234'), '2020-03-01-preview').customerId]" + "Incorrect value for workspaceId" - test "Enable container logging workspace" { - let deployment = - let workspace = logAnalytics { name "containergrouplogs1234" } - - arm { - add_resources - [ - workspace - containerGroup { - name "container-group-with-insights" - - add_instances - [ - containerInstance { - name "httpserver" - image "nginx" - } - ] - - diagnostics_workspace LogType.ContainerInstanceLogs workspace - } - ] - } + Expect.equal + (string workspaceKey) + "[listkeys(resourceId('Microsoft.OperationalInsights/workspaces', 'containergrouplogs1234'), '2020-03-01-preview').primarySharedKey]" + "Incorrect value for workspaceKey" - let jobj = deployment.Template |> Writer.toJson |> JObject.Parse + Expect.equal (string logType) "ContainerInstanceLogs" "Incorrect value for workspaceId" - let logAnalytics = - jobj.SelectToken - "resources[?(@.name=='container-group-with-insights')].properties.diagnostics.logAnalytics" + let cgDependencies = + jobj.SelectToken "resources[?(@.name=='container-group-with-insights')].dependsOn" - let workspaceId = logAnalytics.SelectToken "workspaceId" - let workspaceKey = logAnalytics.SelectToken "workspaceKey" - let logType = logAnalytics.SelectToken "logType" + Expect.hasLength cgDependencies 1 "Incorrect number of dependencies for diagnostics workspace" + } - Expect.equal - (string workspaceId) - "[reference(resourceId('Microsoft.OperationalInsights/workspaces', 'containergrouplogs1234'), '2020-03-01-preview').customerId]" - "Incorrect value for workspaceId" + test "Enable linking to container logging workspace" { + let deployment = + let workspaceId = LogAnalytics.workspaces.resourceId "my-log-analytics-workspace" - Expect.equal - (string workspaceKey) - "[listkeys(resourceId('Microsoft.OperationalInsights/workspaces', 'containergrouplogs1234'), '2020-03-01-preview').primarySharedKey]" - "Incorrect value for workspaceKey" - - Expect.equal (string logType) "ContainerInstanceLogs" "Incorrect value for workspaceId" - - let cgDependencies = - jobj.SelectToken "resources[?(@.name=='container-group-with-insights')].dependsOn" - - Expect.hasLength cgDependencies 1 "Incorrect number of dependencies for diagnostics workspace" - } + arm { + add_resources [ + containerGroup { + name "container-group-with-insights" - test "Enable linking to container logging workspace" { - let deployment = - let workspaceId = LogAnalytics.workspaces.resourceId "my-log-analytics-workspace" - - arm { - add_resources - [ - containerGroup { - name "container-group-with-insights" - - add_instances - [ - containerInstance { - name "httpserver" - image "nginx" - } - ] - - link_to_diagnostics_workspace LogType.ContainerInstanceLogs workspaceId + add_instances [ + containerInstance { + name "httpserver" + image "nginx" } ] - } - let jobj = deployment.Template |> Writer.toJson |> JObject.Parse + link_to_diagnostics_workspace LogType.ContainerInstanceLogs workspaceId + } + ] + } - let logAnalytics = - jobj.SelectToken - "resources[?(@.name=='container-group-with-insights')].properties.diagnostics.logAnalytics" + let jobj = deployment.Template |> Writer.toJson |> JObject.Parse - let workspaceId = logAnalytics.SelectToken "workspaceId" - let workspaceKey = logAnalytics.SelectToken "workspaceKey" - let logType = logAnalytics.SelectToken "logType" + let logAnalytics = + jobj.SelectToken + "resources[?(@.name=='container-group-with-insights')].properties.diagnostics.logAnalytics" - Expect.equal - (string workspaceId) - "[reference(resourceId('Microsoft.OperationalInsights/workspaces', 'my-log-analytics-workspace'), '2020-03-01-preview').customerId]" - "Incorrect value for workspaceId" + let workspaceId = logAnalytics.SelectToken "workspaceId" + let workspaceKey = logAnalytics.SelectToken "workspaceKey" + let logType = logAnalytics.SelectToken "logType" - Expect.equal - (string workspaceKey) - "[listkeys(resourceId('Microsoft.OperationalInsights/workspaces', 'my-log-analytics-workspace'), '2020-03-01-preview').primarySharedKey]" - "Incorrect value for workspaceKey" + Expect.equal + (string workspaceId) + "[reference(resourceId('Microsoft.OperationalInsights/workspaces', 'my-log-analytics-workspace'), '2020-03-01-preview').customerId]" + "Incorrect value for workspaceId" - Expect.equal (string logType) "ContainerInstanceLogs" "Incorrect value for workspaceId" + Expect.equal + (string workspaceKey) + "[listkeys(resourceId('Microsoft.OperationalInsights/workspaces', 'my-log-analytics-workspace'), '2020-03-01-preview').primarySharedKey]" + "Incorrect value for workspaceKey" - let cgDependencies = - jobj.SelectToken "resources[?(@.name=='container-group-with-insights')].dependsOn" + Expect.equal (string logType) "ContainerInstanceLogs" "Incorrect value for workspaceId" - Expect.isEmpty cgDependencies "Should have no dependencies when linking to a workspace." - } - - test "Enable passing key to container logging workspace" { - let fakeWorkspaceId = Guid.NewGuid() |> string - let fakeWorkspaceKey = Guid.NewGuid() |> string - - let deployment = - arm { - add_resources - [ - containerGroup { - name "container-group-with-insights" - - add_instances - [ - containerInstance { - name "httpserver" - image "nginx" - } - ] - - diagnostics_workspace_key - LogType.ContainerInstanceLogs - fakeWorkspaceId - fakeWorkspaceKey - } - ] - } + let cgDependencies = + jobj.SelectToken "resources[?(@.name=='container-group-with-insights')].dependsOn" - let jobj = deployment.Template |> Writer.toJson |> JObject.Parse + Expect.isEmpty cgDependencies "Should have no dependencies when linking to a workspace." + } - let logAnalytics = - jobj.SelectToken - "resources[?(@.name=='container-group-with-insights')].properties.diagnostics.logAnalytics" + test "Enable passing key to container logging workspace" { + let fakeWorkspaceId = Guid.NewGuid() |> string + let fakeWorkspaceKey = Guid.NewGuid() |> string - let workspaceId = logAnalytics.SelectToken "workspaceId" - let workspaceKey = logAnalytics.SelectToken "workspaceKey" - Expect.equal (string workspaceId) fakeWorkspaceId "Incorrect value for workspaceId" - Expect.equal (string workspaceKey) fakeWorkspaceKey "Incorrect value for workspaceKey" + let deployment = arm { + add_resources [ + containerGroup { + name "container-group-with-insights" - let cgDependencies = - jobj.SelectToken "resources[?(@.name=='container-group-with-insights')].dependsOn" + add_instances [ + containerInstance { + name "httpserver" + image "nginx" + } + ] - Expect.isEmpty cgDependencies "Should have no dependencies when linking to a workspace." + diagnostics_workspace_key LogType.ContainerInstanceLogs fakeWorkspaceId fakeWorkspaceKey + } + ] } - test "Specify DNS nameservers and search domains" { - let deployment = - arm { - add_resources - [ - vnet { - name "mynetwork" - add_address_spaces [ "10.30.32.0/20" ] - - add_subnets - [ - subnet { - name "containers" - prefix "10.30.41.0/24" - add_delegations [ SubnetDelegationService.ContainerGroups ] - } - ] - } - networkProfile { - name "netprofile" - vnet "mynetwork" - subnet "containers" - } - containerGroup { - name "container-group-with-custom-dns" - dns_nameservers [ "8.8.8.8"; "1.1.1.1" ] - dns_search_domains [ "example.com"; "example.local" ] - - add_instances - [ - containerInstance { - name "httpserver" - image "nginx:1.17.6-alpine" - } - ] - - network_profile "netprofile" - } - ] + let jobj = deployment.Template |> Writer.toJson |> JObject.Parse + + let logAnalytics = + jobj.SelectToken + "resources[?(@.name=='container-group-with-insights')].properties.diagnostics.logAnalytics" + + let workspaceId = logAnalytics.SelectToken "workspaceId" + let workspaceKey = logAnalytics.SelectToken "workspaceKey" + Expect.equal (string workspaceId) fakeWorkspaceId "Incorrect value for workspaceId" + Expect.equal (string workspaceKey) fakeWorkspaceKey "Incorrect value for workspaceKey" + + let cgDependencies = + jobj.SelectToken "resources[?(@.name=='container-group-with-insights')].dependsOn" + + Expect.isEmpty cgDependencies "Should have no dependencies when linking to a workspace." + } + + test "Specify DNS nameservers and search domains" { + let deployment = arm { + add_resources [ + vnet { + name "mynetwork" + add_address_spaces [ "10.30.32.0/20" ] + + add_subnets [ + subnet { + name "containers" + prefix "10.30.41.0/24" + add_delegations [ SubnetDelegationService.ContainerGroups ] + } + ] + } + networkProfile { + name "netprofile" + vnet "mynetwork" + subnet "containers" } + containerGroup { + name "container-group-with-custom-dns" + dns_nameservers [ "8.8.8.8"; "1.1.1.1" ] + dns_search_domains [ "example.com"; "example.local" ] + + add_instances [ + containerInstance { + name "httpserver" + image "nginx:1.17.6-alpine" + } + ] + + network_profile "netprofile" + } + ] + } - let jobj = deployment.Template |> Writer.toJson |> JObject.Parse + let jobj = deployment.Template |> Writer.toJson |> JObject.Parse - let dnsConfig = - jobj.SelectToken "resources[?(@.name=='container-group-with-custom-dns')].properties.dnsConfig" + let dnsConfig = + jobj.SelectToken "resources[?(@.name=='container-group-with-custom-dns')].properties.dnsConfig" - let nameservers = dnsConfig.SelectToken "nameServers" - let searchDomains = dnsConfig.SelectToken "searchDomains" - Expect.sequenceEqual nameservers [ JValue "8.8.8.8"; JValue "1.1.1.1" ] "Incorrect nameservers." - Expect.equal searchDomains (JValue "example.com example.local") "Incorrect search domains." - } + let nameservers = dnsConfig.SelectToken "nameServers" + let searchDomains = dnsConfig.SelectToken "searchDomains" + Expect.sequenceEqual nameservers [ JValue "8.8.8.8"; JValue "1.1.1.1" ] "Incorrect nameservers." + Expect.equal searchDomains (JValue "example.com example.local") "Incorrect search domains." + } - test "Create container group created with a link_to_identity" { - let resourceId = - ResourceId.create (ManagedIdentity.userAssignedIdentities, ResourceName "user", "resourceGroup") + test "Create container group created with a link_to_identity" { + let resourceId = + ResourceId.create (ManagedIdentity.userAssignedIdentities, ResourceName "user", "resourceGroup") - let managedIdentity: Identity.ManagedIdentity = - { ManagedIdentity.Empty with - UserAssigned = [ (LinkedUserAssignedIdentity resourceId) ] - } + let managedIdentity: Identity.ManagedIdentity = { + ManagedIdentity.Empty with + UserAssigned = [ (LinkedUserAssignedIdentity resourceId) ] + } - let containerGroup = - containerGroup { - name "container-group-with-link-to-identity" - link_to_identity resourceId + let containerGroup = containerGroup { + name "container-group-with-link-to-identity" + link_to_identity resourceId - add_managed_identity_registry_credentials - [ registry "my-registry.azurecr.io" "user" managedIdentity ] + add_managed_identity_registry_credentials [ registry "my-registry.azurecr.io" "user" managedIdentity ] - add_instances - [ - containerInstance { - name "httpserver" - image "nginx:1.17.6-alpine" - } - ] + add_instances [ + containerInstance { + name "httpserver" + image "nginx:1.17.6-alpine" } + ] + } - let deployment = - arm { - add_resources - [ - containerGroup + let deployment = arm { + add_resources [ + containerGroup - ] - } + ] + } - let jobj = deployment.Template |> Writer.toJson |> JObject.Parse + let jobj = deployment.Template |> Writer.toJson |> JObject.Parse - let containerGroupJson = - jobj.SelectToken("resources[?(@.name=='container-group-with-link-to-identity')]") + let containerGroupJson = + jobj.SelectToken("resources[?(@.name=='container-group-with-link-to-identity')]") - let dependsOn = containerGroupJson.SelectToken("dependsOn") :?> JArray - Expect.equal dependsOn.Count 0 "Container group dependsOn list shall be empty" - } + let dependsOn = containerGroupJson.SelectToken("dependsOn") :?> JArray + Expect.equal dependsOn.Count 0 "Container group dependsOn list shall be empty" + } - test "Create container group created with a add_identity" { - let resourceId = - ResourceId.create (ManagedIdentity.userAssignedIdentities, ResourceName "user", "resourceGroup") + test "Create container group created with a add_identity" { + let resourceId = + ResourceId.create (ManagedIdentity.userAssignedIdentities, ResourceName "user", "resourceGroup") - let userAssignedIdentity = resourceId |> UserAssignedIdentity + let userAssignedIdentity = resourceId |> UserAssignedIdentity - let managedIdentity: Identity.ManagedIdentity = - { ManagedIdentity.Empty with - UserAssigned = [ userAssignedIdentity ] - } + let managedIdentity: Identity.ManagedIdentity = { + ManagedIdentity.Empty with + UserAssigned = [ userAssignedIdentity ] + } - let containerGroup = - containerGroup { - name "container-group-with-add-identity" - add_identity userAssignedIdentity + let containerGroup = containerGroup { + name "container-group-with-add-identity" + add_identity userAssignedIdentity - add_managed_identity_registry_credentials - [ registry "my-registry.azurecr.io" "user" managedIdentity ] + add_managed_identity_registry_credentials [ registry "my-registry.azurecr.io" "user" managedIdentity ] - add_instances - [ - containerInstance { - name "httpserver" - image "nginx:1.17.6-alpine" - } - ] + add_instances [ + containerInstance { + name "httpserver" + image "nginx:1.17.6-alpine" } + ] + } - let deployment = - arm { - add_resources - [ - containerGroup + let deployment = arm { + add_resources [ + containerGroup - ] - } + ] + } - let jobj = deployment.Template |> Writer.toJson |> JObject.Parse + let jobj = deployment.Template |> Writer.toJson |> JObject.Parse - let containerGroupJson = - jobj.SelectToken("resources[?(@.name=='container-group-with-add-identity')]") + let containerGroupJson = + jobj.SelectToken("resources[?(@.name=='container-group-with-add-identity')]") - let dependsOn = containerGroupJson.SelectToken("dependsOn") :?> JArray - Expect.equal dependsOn.Count 1 "Container group dependsOn list shouldn't be empty" - } - ] + let dependsOn = containerGroupJson.SelectToken("dependsOn") :?> JArray + Expect.equal dependsOn.Count 1 "Container group dependsOn list shouldn't be empty" + } + ] diff --git a/src/Tests/ContainerRegistry.fs b/src/Tests/ContainerRegistry.fs index 6896f22ad..a24e4fad1 100644 --- a/src/Tests/ContainerRegistry.fs +++ b/src/Tests/ContainerRegistry.fs @@ -6,22 +6,23 @@ open Farmer.ContainerRegistry open Farmer.Builders open System.Collections.Generic -type RegistryJson = - { - resources: {| name: string - ``type``: string - apiVersion: string - sku: {| name: string |} - location: string - properties: IDictionary |} array - } +type RegistryJson = { + resources: + {| + name: string + ``type``: string + apiVersion: string + sku: {| name: string |} + location: string + properties: IDictionary + |} array +} let toTemplate loc (d: ContainerRegistryConfig) = - let a = - arm { - location loc - add_resource d - } + let a = arm { + location loc + add_resource d + } a.Template @@ -63,80 +64,67 @@ let shouldHaveAdminUserDisabled (r: RegistryJson) = r let tests = - testList - "Container Registry" - [ - test "Basic resource settings are written to template resource" { - containerRegistry { - name "validContainerRegistryName" - sku Premium - } - |> whenWritten - |> shouldHaveType "Microsoft.ContainerRegistry/registries" - |> shouldHaveApiVersion "2019-05-01" - |> shouldHaveName "validContainerRegistryName" - |> shouldHaveSku Premium - |> shouldHaveALocation - |> shouldHaveAdminUserDisabled - |> ignore + testList "Container Registry" [ + test "Basic resource settings are written to template resource" { + containerRegistry { + name "validContainerRegistryName" + sku Premium + } + |> whenWritten + |> shouldHaveType "Microsoft.ContainerRegistry/registries" + |> shouldHaveApiVersion "2019-05-01" + |> shouldHaveName "validContainerRegistryName" + |> shouldHaveSku Premium + |> shouldHaveALocation + |> shouldHaveAdminUserDisabled + |> ignore + } + + test "When enable_admin_user is set it is written to resource properties" { + containerRegistry { + name "validContainerRegistryName" + enable_admin_user } + |> whenWritten + |> shouldHaveAdminUserEnabled + |> ignore + } - test "When enable_admin_user is set it is written to resource properties" { - containerRegistry { - name "validContainerRegistryName" - enable_admin_user + testList "Container Registry Name Validation tests" [ + let invalidNameCases = [ + "Empty Account", "", "cannot be empty", "Name too short" + "Min Length", "abc", "min length is 5, but here is 3. The invalid value is 'abc'", "Name too short" + "Max Length", + "abcdefghij1234567890abcde12345678901234567890abcdef", + "max length is 50, but here is 51. The invalid value is 'abcdefghij1234567890abcde12345678901234567890abcdef'", + "Name too long" + "Non alphanumeric", + "abcde!", + "can only contain alphanumeric characters. The invalid value is 'abcde!'", + "Value contains non-alphanumeric characters" + ] + + for testName, containerRegisterName, error, why in invalidNameCases do + test testName { + Expect.equal + (ContainerRegistryValidation.ContainerRegistryName.Create containerRegisterName) + (Error("Container Registry Name " + error)) + why } - |> whenWritten - |> shouldHaveAdminUserEnabled - |> ignore - } - testList - "Container Registry Name Validation tests" - [ - let invalidNameCases = - [ - "Empty Account", "", "cannot be empty", "Name too short" - "Min Length", - "abc", - "min length is 5, but here is 3. The invalid value is 'abc'", - "Name too short" - "Max Length", - "abcdefghij1234567890abcde12345678901234567890abcdef", - "max length is 50, but here is 51. The invalid value is 'abcdefghij1234567890abcde12345678901234567890abcdef'", - "Name too long" - "Non alphanumeric", - "abcde!", - "can only contain alphanumeric characters. The invalid value is 'abcde!'", - "Value contains non-alphanumeric characters" - ] - - for testName, containerRegisterName, error, why in invalidNameCases do - test testName { - Expect.equal - (ContainerRegistryValidation.ContainerRegistryName.Create containerRegisterName) - (Error("Container Registry Name " + error)) - why - } - - let validNameCases = - [ - "Valid Name 1", "abcde", "Should have created a valid Container Registry name" - "Valid Name 2", "abc123", "Should have created a valid Container Registry name" - ] - - for testName, containerRegisterName, why in validNameCases do - test testName { - Expect.equal - (ContainerRegistryValidation - .ContainerRegistryName - .Create( - containerRegisterName - ) - .OkValue - .ResourceName) - (ResourceName containerRegisterName) - why - } - ] + let validNameCases = [ + "Valid Name 1", "abcde", "Should have created a valid Container Registry name" + "Valid Name 2", "abc123", "Should have created a valid Container Registry name" + ] + + for testName, containerRegisterName, why in validNameCases do + test testName { + Expect.equal + (ContainerRegistryValidation.ContainerRegistryName + .Create(containerRegisterName) + .OkValue.ResourceName) + (ResourceName containerRegisterName) + why + } ] + ] diff --git a/src/Tests/ContainerService.fs b/src/Tests/ContainerService.fs index bdbd049d1..2e32721e0 100644 --- a/src/Tests/ContainerService.fs +++ b/src/Tests/ContainerService.fs @@ -14,367 +14,338 @@ let dummyClient = new ContainerServiceClient(Uri "http://management.azure.com", TokenCredentials "NotNullOrWhiteSpace") let tests = - testList - "AKS" - [ - /// The simplest AKS cluster would be one that uses a system assigned managed identity (MSI), - /// uses that MSI for accessing other resources, and then takes the defaults for node pool - /// size (3 nodes) and DNS prefix (generated based on cluster name). - test "Basic AKS cluster with MSI" { - let myAks = - aks { - name "aks-cluster" - service_principal_use_msi - } - - let template = arm { add_resource myAks } - - let aks = - template - |> findAzureResources dummyClient.SerializationSettings - |> Seq.head - - Expect.equal aks.Name "aks-cluster" "" - Expect.hasLength aks.AgentPoolProfiles 1 "" - Expect.equal aks.AgentPoolProfiles.[0].Name "nodepool1" "" - Expect.equal aks.AgentPoolProfiles.[0].Count 3 "" - let json = template.Template |> Writer.toJson - let jobj = Newtonsoft.Json.Linq.JObject.Parse(json) - - let identity = - jobj.SelectToken("resources[?(@.name=='aks-cluster')].identity.type") |> string - - Expect.equal identity "SystemAssigned" "Basic cluster using MSI should have a SystemAssigned identity." + testList "AKS" [ + /// The simplest AKS cluster would be one that uses a system assigned managed identity (MSI), + /// uses that MSI for accessing other resources, and then takes the defaults for node pool + /// size (3 nodes) and DNS prefix (generated based on cluster name). + test "Basic AKS cluster with MSI" { + let myAks = aks { + name "aks-cluster" + service_principal_use_msi } - test "Basic AKS cluster with client ID" { - let myAks = - aks { - name "aks-cluster" - service_principal_client_id "some-spn-client-id" - } - - let template = arm { add_resource myAks } - - let aks = - template - |> findAzureResources dummyClient.SerializationSettings - |> Seq.head - - Expect.equal aks.Name "aks-cluster" "" - Expect.hasLength aks.AgentPoolProfiles 1 "" - Expect.equal aks.AgentPoolProfiles.[0].Name "nodepool1" "" - Expect.equal aks.AgentPoolProfiles.[0].Count 3 "" - let json = template.Template |> Writer.toJson - let jobj = Newtonsoft.Json.Linq.JObject.Parse(json) - let identity = - jobj.SelectToken("resources[?(@.name=='aks-cluster')].identity.type") |> string - - Expect.equal identity "None" "Basic cluster with client ID should have no identity assigned." - } - test "Basic AKS cluster needs SP" { - Expect.throws - (fun _ -> - let myAks = aks { name "aks-cluster" } - let template = arm { add_resource myAks } - template |> Writer.quickWrite "aks-cluster-should-fail") - "Error should be raised if there are no service principal settings." + let template = arm { add_resource myAks } + + let aks = + template + |> findAzureResources dummyClient.SerializationSettings + |> Seq.head + + Expect.equal aks.Name "aks-cluster" "" + Expect.hasLength aks.AgentPoolProfiles 1 "" + Expect.equal aks.AgentPoolProfiles.[0].Name "nodepool1" "" + Expect.equal aks.AgentPoolProfiles.[0].Count 3 "" + let json = template.Template |> Writer.toJson + let jobj = Newtonsoft.Json.Linq.JObject.Parse(json) + + let identity = + jobj.SelectToken("resources[?(@.name=='aks-cluster')].identity.type") |> string + + Expect.equal identity "SystemAssigned" "Basic cluster using MSI should have a SystemAssigned identity." + } + test "Basic AKS cluster with client ID" { + let myAks = aks { + name "aks-cluster" + service_principal_client_id "some-spn-client-id" } - test "Simple AKS cluster" { - let myAks = - aks { - name "k8s-cluster" - dns_prefix "testaks" - - add_agent_pools - [ - agentPool { - name "linuxPool" - count 3 - } - ] - linux_profile "aksuser" "public-key-here" - service_principal_client_id "some-spn-client-id" + let template = arm { add_resource myAks } + + let aks = + template + |> findAzureResources dummyClient.SerializationSettings + |> Seq.head + + Expect.equal aks.Name "aks-cluster" "" + Expect.hasLength aks.AgentPoolProfiles 1 "" + Expect.equal aks.AgentPoolProfiles.[0].Name "nodepool1" "" + Expect.equal aks.AgentPoolProfiles.[0].Count 3 "" + let json = template.Template |> Writer.toJson + let jobj = Newtonsoft.Json.Linq.JObject.Parse(json) + + let identity = + jobj.SelectToken("resources[?(@.name=='aks-cluster')].identity.type") |> string + + Expect.equal identity "None" "Basic cluster with client ID should have no identity assigned." + } + test "Basic AKS cluster needs SP" { + Expect.throws + (fun _ -> + let myAks = aks { name "aks-cluster" } + let template = arm { add_resource myAks } + template |> Writer.quickWrite "aks-cluster-should-fail") + "Error should be raised if there are no service principal settings." + } + test "Simple AKS cluster" { + let myAks = aks { + name "k8s-cluster" + dns_prefix "testaks" + + add_agent_pools [ + agentPool { + name "linuxPool" + count 3 } + ] - let aks = - arm { add_resource myAks } - |> findAzureResources dummyClient.SerializationSettings - |> Seq.head - - Expect.equal aks.Name "k8s-cluster" "" - Expect.hasLength aks.AgentPoolProfiles 1 "" - Expect.equal aks.AgentPoolProfiles.[0].Name "linuxpool" "" - Expect.equal aks.AgentPoolProfiles.[0].Count 3 "" - Expect.equal aks.AgentPoolProfiles.[0].VmSize "Standard_DS2_v2" "" - Expect.equal aks.LinuxProfile.AdminUsername "aksuser" "" - Expect.equal aks.LinuxProfile.Ssh.PublicKeys.Count 1 "" - Expect.equal aks.LinuxProfile.Ssh.PublicKeys.[0].KeyData "public-key-here" "" - Expect.equal aks.ServicePrincipalProfile.ClientId "some-spn-client-id" "" - Expect.equal aks.ServicePrincipalProfile.Secret "[parameters('client-secret-for-k8s-cluster')]" "" + linux_profile "aksuser" "public-key-here" + service_principal_client_id "some-spn-client-id" } - test "AKS cluster using MSI" { - let myAks = - aks { - name "k8s-cluster" - dns_prefix "testaks" - - add_agent_pools - [ - agentPool { - name "linuxPool" - count 3 - } - ] - service_principal_use_msi + let aks = + arm { add_resource myAks } + |> findAzureResources dummyClient.SerializationSettings + |> Seq.head + + Expect.equal aks.Name "k8s-cluster" "" + Expect.hasLength aks.AgentPoolProfiles 1 "" + Expect.equal aks.AgentPoolProfiles.[0].Name "linuxpool" "" + Expect.equal aks.AgentPoolProfiles.[0].Count 3 "" + Expect.equal aks.AgentPoolProfiles.[0].VmSize "Standard_DS2_v2" "" + Expect.equal aks.LinuxProfile.AdminUsername "aksuser" "" + Expect.equal aks.LinuxProfile.Ssh.PublicKeys.Count 1 "" + Expect.equal aks.LinuxProfile.Ssh.PublicKeys.[0].KeyData "public-key-here" "" + Expect.equal aks.ServicePrincipalProfile.ClientId "some-spn-client-id" "" + Expect.equal aks.ServicePrincipalProfile.Secret "[parameters('client-secret-for-k8s-cluster')]" "" + } + test "AKS cluster using MSI" { + let myAks = aks { + name "k8s-cluster" + dns_prefix "testaks" + + add_agent_pools [ + agentPool { + name "linuxPool" + count 3 } + ] - let aks = - arm { add_resource myAks } - |> findAzureResources dummyClient.SerializationSettings - |> Seq.head - - Expect.equal - aks.ServicePrincipalProfile.ClientId - "msi" - "ClientId should be 'msi' for service principal." - } - test "Calculates network profile DNS server" { - let netProfile = azureCniNetworkProfile { service_cidr "10.250.0.0/16" } - let serviceCidr = Expect.wantSome netProfile.ServiceCidr "Service CIDR not set" - Expect.equal (serviceCidr |> IPAddressCidr.format) "10.250.0.0/16" "Service CIDR set incorrectly." - Expect.isSome netProfile.DnsServiceIP "DNS service IP should have a value" - - Expect.equal - (netProfile.DnsServiceIP.Value.ToString()) - "10.250.0.2" - "DNS service IP should be .2 in service_cidr" + service_principal_use_msi } - test "AKS cluster on Private VNet" { - let myAks = - aks { - name "private-k8s-cluster" - dns_prefix "testprivateaks" - - add_agent_pools - [ - agentPool { - name "linuxPool" - count 3 - vnet "my-vnet" - subnet "containernet" - } - ] - - network_profile (azureCniNetworkProfile { service_cidr "10.250.0.0/16" }) - linux_profile "aksuser" "public-key-here" - service_principal_client_id "some-spn-client-id" - } - let aks = - arm { add_resource myAks } - |> findAzureResources dummyClient.SerializationSettings - |> Seq.head + let aks = + arm { add_resource myAks } + |> findAzureResources dummyClient.SerializationSettings + |> Seq.head + + Expect.equal aks.ServicePrincipalProfile.ClientId "msi" "ClientId should be 'msi' for service principal." + } + test "Calculates network profile DNS server" { + let netProfile = azureCniNetworkProfile { service_cidr "10.250.0.0/16" } + let serviceCidr = Expect.wantSome netProfile.ServiceCidr "Service CIDR not set" + Expect.equal (serviceCidr |> IPAddressCidr.format) "10.250.0.0/16" "Service CIDR set incorrectly." + Expect.isSome netProfile.DnsServiceIP "DNS service IP should have a value" + + Expect.equal + (netProfile.DnsServiceIP.Value.ToString()) + "10.250.0.2" + "DNS service IP should be .2 in service_cidr" + } + test "AKS cluster on Private VNet" { + let myAks = aks { + name "private-k8s-cluster" + dns_prefix "testprivateaks" + + add_agent_pools [ + agentPool { + name "linuxPool" + count 3 + vnet "my-vnet" + subnet "containernet" + } + ] - Expect.hasLength aks.AgentPoolProfiles 1 "" - Expect.equal aks.AgentPoolProfiles.[0].Name "linuxpool" "" + network_profile (azureCniNetworkProfile { service_cidr "10.250.0.0/16" }) + linux_profile "aksuser" "public-key-here" + service_principal_client_id "some-spn-client-id" } - test "AKS with private API must use a standard load balancer." { - Expect.throws - (fun () -> - let _ = - aks { - name "k8s-cluster" - service_principal_client_id "some-spn-client-id" - dns_prefix "testaks" - - add_agent_pools - [ - agentPool { - name "linuxPool" - count 3 - } - ] - - network_profile (kubenetNetworkProfile { load_balancer_sku LoadBalancer.Sku.Basic }) - enable_private_cluster true - } - ()) - "Should throw validation exception when trying to use a private cluster on a basic LB" - } - test "AKS API accessible to limited IP range." { - let myAks = - aks { + let aks = + arm { add_resource myAks } + |> findAzureResources dummyClient.SerializationSettings + |> Seq.head + + Expect.hasLength aks.AgentPoolProfiles 1 "" + Expect.equal aks.AgentPoolProfiles.[0].Name "linuxpool" "" + } + test "AKS with private API must use a standard load balancer." { + Expect.throws + (fun () -> + let _ = aks { name "k8s-cluster" service_principal_client_id "some-spn-client-id" dns_prefix "testaks" - add_agent_pools - [ - agentPool { - name "linuxPool" - count 3 - } - ] + add_agent_pools [ + agentPool { + name "linuxPool" + count 3 + } + ] - add_api_server_authorized_ip_ranges [ "88.77.66.0/24" ] + network_profile (kubenetNetworkProfile { load_balancer_sku LoadBalancer.Sku.Basic }) + enable_private_cluster true } - let template = arm { add_resource myAks } - let json = template.Template |> Writer.toJson - let jobj = Newtonsoft.Json.Linq.JObject.Parse(json) + ()) + "Should throw validation exception when trying to use a private cluster on a basic LB" + } + test "AKS API accessible to limited IP range." { + let myAks = aks { + name "k8s-cluster" + service_principal_client_id "some-spn-client-id" + dns_prefix "testaks" + + add_agent_pools [ + agentPool { + name "linuxPool" + count 3 + } + ] - let authIpRanges = - jobj.SelectToken( - "resources[?(@.name=='k8s-cluster')].properties.apiServerAccessProfile.authorizedIPRanges" - ) + add_api_server_authorized_ip_ranges [ "88.77.66.0/24" ] + } - Expect.hasLength authIpRanges 1 "" + let template = arm { add_resource myAks } + let json = template.Template |> Writer.toJson + let jobj = Newtonsoft.Json.Linq.JObject.Parse(json) + + let authIpRanges = + jobj.SelectToken( + "resources[?(@.name=='k8s-cluster')].properties.apiServerAccessProfile.authorizedIPRanges" + ) + + Expect.hasLength authIpRanges 1 "" + + Expect.equal (authIpRanges.[0].ToString()) "88.77.66.0/24" "Got incorrect value for authorized IP ranges." + } + test "AKS with MSI and Kubelet identity" { + let kubeletMsi = createUserAssignedIdentity "kubeletIdentity" + let clusterMsi = createUserAssignedIdentity "clusterIdentity" + + let assignMsiRoleNameExpr = + ArmExpression.create ( + $"guid(concat(resourceGroup().id, '{clusterMsi.ResourceId.Name.Value}', '{Roles.ManagedIdentityOperator.Id}'))" + ) + + let assignMsiRole = { + Name = assignMsiRoleNameExpr.Eval() |> ResourceName + RoleDefinitionId = Roles.ManagedIdentityOperator + PrincipalId = clusterMsi.PrincipalId + PrincipalType = PrincipalType.ServicePrincipal + Scope = ResourceGroup + Dependencies = Set [ clusterMsi.ResourceId ] + } - Expect.equal - (authIpRanges.[0].ToString()) - "88.77.66.0/24" - "Got incorrect value for authorized IP ranges." + let myAcr = containerRegistry { name "farmercontainerregistry1234" } + let myAcrResId = (myAcr :> IBuilder).ResourceId + + let acrPullRoleNameExpr = + ArmExpression.create ( + $"guid(concat(resourceGroup().id, '{kubeletMsi.ResourceId.Name.Value}', '{Roles.AcrPull.Id}'))" + ) + + let acrPullRole = { + Name = acrPullRoleNameExpr.Eval() |> ResourceName + RoleDefinitionId = Roles.AcrPull + PrincipalId = kubeletMsi.PrincipalId + PrincipalType = PrincipalType.ServicePrincipal + Scope = AssignmentScope.SpecificResource myAcrResId + Dependencies = Set [ kubeletMsi.ResourceId ] } - test "AKS with MSI and Kubelet identity" { - let kubeletMsi = createUserAssignedIdentity "kubeletIdentity" - let clusterMsi = createUserAssignedIdentity "clusterIdentity" - - let assignMsiRoleNameExpr = - ArmExpression.create ( - $"guid(concat(resourceGroup().id, '{clusterMsi.ResourceId.Name.Value}', '{Roles.ManagedIdentityOperator.Id}'))" - ) - - let assignMsiRole = - { - Name = assignMsiRoleNameExpr.Eval() |> ResourceName - RoleDefinitionId = Roles.ManagedIdentityOperator - PrincipalId = clusterMsi.PrincipalId - PrincipalType = PrincipalType.ServicePrincipal - Scope = ResourceGroup - Dependencies = Set [ clusterMsi.ResourceId ] - } - let myAcr = containerRegistry { name "farmercontainerregistry1234" } - let myAcrResId = (myAcr :> IBuilder).ResourceId - - let acrPullRoleNameExpr = - ArmExpression.create ( - $"guid(concat(resourceGroup().id, '{kubeletMsi.ResourceId.Name.Value}', '{Roles.AcrPull.Id}'))" - ) - - let acrPullRole = - { - Name = acrPullRoleNameExpr.Eval() |> ResourceName - RoleDefinitionId = Roles.AcrPull - PrincipalId = kubeletMsi.PrincipalId - PrincipalType = PrincipalType.ServicePrincipal - Scope = AssignmentScope.SpecificResource myAcrResId - Dependencies = Set [ kubeletMsi.ResourceId ] - } + let myAks = aks { + name "aks-cluster" + dns_prefix "aks-cluster-223d2976" + add_identity clusterMsi + service_principal_use_msi + kubelet_identity kubeletMsi + depends_on clusterMsi + depends_on myAcr + depends_on_expression assignMsiRoleNameExpr + depends_on_expression acrPullRoleNameExpr + } - let myAks = - aks { - name "aks-cluster" - dns_prefix "aks-cluster-223d2976" - add_identity clusterMsi - service_principal_use_msi - kubelet_identity kubeletMsi - depends_on clusterMsi - depends_on myAcr - depends_on_expression assignMsiRoleNameExpr - depends_on_expression acrPullRoleNameExpr - } + let template = arm { + location Location.EastUS + add_resource kubeletMsi + add_resource clusterMsi + add_resource myAcr + add_resource myAks + add_resource assignMsiRole + add_resource acrPullRole + } - let template = - arm { - location Location.EastUS - add_resource kubeletMsi - add_resource clusterMsi - add_resource myAcr - add_resource myAks - add_resource assignMsiRole - add_resource acrPullRole + let json = template.Template |> Writer.toJson + let jobj = Newtonsoft.Json.Linq.JObject.Parse(json) + + let identity = + jobj.SelectToken("resources[?(@.name=='aks-cluster')].identity.type") |> string + + Expect.equal identity "UserAssigned" "Should have a UserAssigned identity." + + let kubeletIdentityClientId = + jobj.SelectToken( + "resources[?(@.name=='aks-cluster')].properties.identityProfile.kubeletIdentity.clientId" + ) + |> string + + Expect.equal + kubeletIdentityClientId + "[reference(resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', 'kubeletIdentity'), '2018-11-30').clientId]" + "Incorrect kubelet identity reference." + } + test "Basic AKS cluster with addons" { + let myAppGateway = appGateway { name "app-gw" } + let appGatewayMsi = createUserAssignedIdentity "app-gw-msi" + + let myAks = aks { + name "aks-cluster" + service_principal_use_msi + + addons [ + AciConnectorLinux Enabled + HttpApplicationRouting Enabled + KubeDashboard Enabled + IngressApplicationGateway { + Status = Enabled + ApplicationGatewayId = (myAppGateway :> IBuilder).ResourceId + Identity = Some appGatewayMsi.UserAssignedIdentity } - - let json = template.Template |> Writer.toJson - let jobj = Newtonsoft.Json.Linq.JObject.Parse(json) - - let identity = - jobj.SelectToken("resources[?(@.name=='aks-cluster')].identity.type") |> string - - Expect.equal identity "UserAssigned" "Should have a UserAssigned identity." - - let kubeletIdentityClientId = - jobj.SelectToken( - "resources[?(@.name=='aks-cluster')].properties.identityProfile.kubeletIdentity.clientId" - ) - |> string - - Expect.equal - kubeletIdentityClientId - "[reference(resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', 'kubeletIdentity'), '2018-11-30').clientId]" - "Incorrect kubelet identity reference." + ] } - test "Basic AKS cluster with addons" { - let myAppGateway = appGateway { name "app-gw" } - let appGatewayMsi = createUserAssignedIdentity "app-gw-msi" - - let myAks = - aks { - name "aks-cluster" - service_principal_use_msi - - addons - [ - AciConnectorLinux Enabled - HttpApplicationRouting Enabled - KubeDashboard Enabled - IngressApplicationGateway - { - Status = Enabled - ApplicationGatewayId = (myAppGateway :> IBuilder).ResourceId - Identity = Some appGatewayMsi.UserAssignedIdentity - } - ] - } - let template = arm { add_resource myAks } - let json = template.Template |> Writer.toJson - let jobj = Newtonsoft.Json.Linq.JObject.Parse(json) + let template = arm { add_resource myAks } + let json = template.Template |> Writer.toJson + let jobj = Newtonsoft.Json.Linq.JObject.Parse(json) - let expectedAciConn = - """{ + let expectedAciConn = + """{ "enabled": true }""" - let aciConnector = - jobj.SelectToken("resources[?(@.name=='aks-cluster')].properties.addonProfiles.aciConnectorLinux") - |> string + let aciConnector = + jobj.SelectToken("resources[?(@.name=='aks-cluster')].properties.addonProfiles.aciConnectorLinux") + |> string - Expect.equal aciConnector expectedAciConn "Unexpected value for addonProfiles.aciConnectorLinux." + Expect.equal aciConnector expectedAciConn "Unexpected value for addonProfiles.aciConnectorLinux." - let expectedHttpAppRouting = - """{ + let expectedHttpAppRouting = + """{ "enabled": true }""" - let httpAppRouting = - jobj.SelectToken( - "resources[?(@.name=='aks-cluster')].properties.addonProfiles.httpApplicationRouting" - ) - |> string + let httpAppRouting = + jobj.SelectToken("resources[?(@.name=='aks-cluster')].properties.addonProfiles.httpApplicationRouting") + |> string - Expect.equal - httpAppRouting - expectedHttpAppRouting - "Unexpected value for addonProfiles.httpApplicationRouting." + Expect.equal + httpAppRouting + expectedHttpAppRouting + "Unexpected value for addonProfiles.httpApplicationRouting." - let expectedAppGateway = - """{ + let expectedAppGateway = + """{ "config": { "applicationGatewayId": "[resourceId('Microsoft.Network/applicationGateways', 'app-gw')]" }, @@ -386,15 +357,15 @@ let tests = } }""" - let appGatewayIngress = - jobj.SelectToken( - "resources[?(@.name=='aks-cluster')].properties.addonProfiles.ingressApplicationGateway" - ) - |> string - - Expect.equal - appGatewayIngress - expectedAppGateway - "Unexpected value for addonProfiles.ingressApplicationGateway." - } - ] + let appGatewayIngress = + jobj.SelectToken( + "resources[?(@.name=='aks-cluster')].properties.addonProfiles.ingressApplicationGateway" + ) + |> string + + Expect.equal + appGatewayIngress + expectedAppGateway + "Unexpected value for addonProfiles.ingressApplicationGateway." + } + ] diff --git a/src/Tests/Cosmos.fs b/src/Tests/Cosmos.fs index 0ae2c42c0..9cbd2fcbf 100644 --- a/src/Tests/Cosmos.fs +++ b/src/Tests/Cosmos.fs @@ -7,239 +7,217 @@ open Farmer.Storage open Farmer.Arm.DocumentDb let tests = - testList - "Cosmos" - [ - test "Cosmos container should ignore duplicate unique keys" { - - let container = - cosmosContainer { - name "people" - partition_key [ "/id" ] CosmosDb.Hash - add_unique_key [ "/FirstName" ] - add_unique_key [ "/LastName" ] - add_unique_key [ "/LastName" ] - } - - Expect.equal container.UniqueKeys.Count 2 "There should be 2 unique keys." - Expect.contains container.UniqueKeys [ "/FirstName" ] "UniqueKeys should contain /FirstName" - Expect.contains container.UniqueKeys [ "/LastName" ] "UniqueKeys should contain /LastName" + testList "Cosmos" [ + test "Cosmos container should ignore duplicate unique keys" { + + let container = cosmosContainer { + name "people" + partition_key [ "/id" ] CosmosDb.Hash + add_unique_key [ "/FirstName" ] + add_unique_key [ "/LastName" ] + add_unique_key [ "/LastName" ] } - test "Serverless template should include 'EnableServerless' and should not contains 'throughput'" { - let t = - arm { - add_resource ( - cosmosDb { - name "foo" - throughput CosmosDb.Serverless - } - ) - } - let json = t.Template |> Writer.toJson - - Expect.isTrue - (json.Contains("EnableServerless")) - "Serverless template should contain 'EnableServerless'." - - Expect.isFalse (json.Contains("throughput")) "Serverless template should not contain 'throughput'." - } - test "Serverless template should include one locations.location with filled locationName" { - let t = - arm { - add_resource ( - cosmosDb { - name "foo" - throughput CosmosDb.Serverless - } - ) + Expect.equal container.UniqueKeys.Count 2 "There should be 2 unique keys." + Expect.contains container.UniqueKeys [ "/FirstName" ] "UniqueKeys should contain /FirstName" + Expect.contains container.UniqueKeys [ "/LastName" ] "UniqueKeys should contain /LastName" + } + test "Serverless template should include 'EnableServerless' and should not contains 'throughput'" { + let t = arm { + add_resource ( + cosmosDb { + name "foo" + throughput CosmosDb.Serverless } + ) + } - let jobj = t.Template |> Writer.toJson |> Newtonsoft.Json.Linq.JObject.Parse - - let locationJOjb = - jobj.SelectToken( - "$.resources[?(@.type=='Microsoft.DocumentDb/databaseAccounts')].properties.locations[0]" - ) + let json = t.Template |> Writer.toJson - Expect.isNotEmpty (locationJOjb |> string) "location should be filled" + Expect.isTrue (json.Contains("EnableServerless")) "Serverless template should contain 'EnableServerless'." - let locationName = locationJOjb.SelectToken("locationName") |> string - Expect.isNotEmpty locationName "location should be filled" - } - test "Provisioned template should include 'throughput' and should not contain 'EnableServerless'" { - let t = - arm { - add_resource ( - cosmosDb { - name "foo" - throughput 400 - } - ) + Expect.isFalse (json.Contains("throughput")) "Serverless template should not contain 'throughput'." + } + test "Serverless template should include one locations.location with filled locationName" { + let t = arm { + add_resource ( + cosmosDb { + name "foo" + throughput CosmosDb.Serverless } + ) + } - let json = t.Template |> Writer.toJson + let jobj = t.Template |> Writer.toJson |> Newtonsoft.Json.Linq.JObject.Parse - Expect.isTrue - (json.Contains("\"throughput\": \"400\"")) - "Shared throughput template should contain 'throughput'." + let locationJOjb = + jobj.SelectToken( + "$.resources[?(@.type=='Microsoft.DocumentDb/databaseAccounts')].properties.locations[0]" + ) - Expect.isFalse - (json.Contains("EnableServerless")) - "Shared throughput template should not contain 'EnableServerless'." - } - test "DB properties are correctly evaluated" { - let db = cosmosDb { name "test" } - - Expect.equal - (db.Endpoint.Eval()) - "[reference(resourceId('Microsoft.DocumentDb/databaseAccounts', 'test-account'), '2021-04-15').documentEndpoint]" - "Endpoint is incorrect" - - Expect.equal - (db.PrimaryKey.Eval()) - "[listKeys(resourceId('Microsoft.DocumentDb/databaseAccounts', 'test-account'), providers('Microsoft.DocumentDb','databaseAccounts').apiVersions[0]).primaryMasterKey]" - "Primary Key is incorrect" - - Expect.equal - (db.SecondaryKey.Eval()) - "[listKeys(resourceId('Microsoft.DocumentDb/databaseAccounts', 'test-account'), providers('Microsoft.DocumentDb','databaseAccounts').apiVersions[0]).secondaryMasterKey]" - "Secondary Key is incorrect" - - Expect.equal - (db.PrimaryReadonlyKey.Eval()) - "[listKeys(resourceId('Microsoft.DocumentDb/databaseAccounts', 'test-account'), providers('Microsoft.DocumentDb','databaseAccounts').apiVersions[0]).primaryreadonlyMasterKey]" - "Primary Readonly Key is incorrect" - - Expect.equal - (db.SecondaryReadonlyKey.Eval()) - "[listKeys(resourceId('Microsoft.DocumentDb/databaseAccounts', 'test-account'), providers('Microsoft.DocumentDb','databaseAccounts').apiVersions[0]).secondaryreadonlyMasterKey]" - "Secondary Readonly Key is incorrect" - - Expect.equal - (db.PrimaryConnectionString.Eval()) - "[listConnectionStrings(resourceId('Microsoft.DocumentDb/databaseAccounts', 'test-account'), providers('Microsoft.DocumentDb','databaseAccounts').apiVersions[0]).connectionStrings[0].connectionString]" - "Primary Connection String is incorrect" - - Expect.equal - (db.SecondaryConnectionString.Eval()) - "[listConnectionStrings(resourceId('Microsoft.DocumentDb/databaseAccounts', 'test-account'), providers('Microsoft.DocumentDb','databaseAccounts').apiVersions[0]).connectionStrings[1].connectionString]" - "Secondary Connection String is incorrect" - } + Expect.isNotEmpty (locationJOjb |> string) "location should be filled" - testList - "db type" - [ - test "default" { - let db = - cosmosDb { - name "test" - account_name "account" - } - - Expect.equal db.Kind Document "" + let locationName = locationJOjb.SelectToken("locationName") |> string + Expect.isNotEmpty locationName "location should be filled" + } + test "Provisioned template should include 'throughput' and should not contain 'EnableServerless'" { + let t = arm { + add_resource ( + cosmosDb { + name "foo" + throughput 400 } + ) + } - test "sql" { - let db = - cosmosDb { - name "test" - kind Document - } - - Expect.equal db.Kind Document "" - } + let json = t.Template |> Writer.toJson + + Expect.isTrue + (json.Contains("\"throughput\": \"400\"")) + "Shared throughput template should contain 'throughput'." + + Expect.isFalse + (json.Contains("EnableServerless")) + "Shared throughput template should not contain 'EnableServerless'." + } + test "DB properties are correctly evaluated" { + let db = cosmosDb { name "test" } + + Expect.equal + (db.Endpoint.Eval()) + "[reference(resourceId('Microsoft.DocumentDb/databaseAccounts', 'test-account'), '2021-04-15').documentEndpoint]" + "Endpoint is incorrect" + + Expect.equal + (db.PrimaryKey.Eval()) + "[listKeys(resourceId('Microsoft.DocumentDb/databaseAccounts', 'test-account'), providers('Microsoft.DocumentDb','databaseAccounts').apiVersions[0]).primaryMasterKey]" + "Primary Key is incorrect" + + Expect.equal + (db.SecondaryKey.Eval()) + "[listKeys(resourceId('Microsoft.DocumentDb/databaseAccounts', 'test-account'), providers('Microsoft.DocumentDb','databaseAccounts').apiVersions[0]).secondaryMasterKey]" + "Secondary Key is incorrect" + + Expect.equal + (db.PrimaryReadonlyKey.Eval()) + "[listKeys(resourceId('Microsoft.DocumentDb/databaseAccounts', 'test-account'), providers('Microsoft.DocumentDb','databaseAccounts').apiVersions[0]).primaryreadonlyMasterKey]" + "Primary Readonly Key is incorrect" + + Expect.equal + (db.SecondaryReadonlyKey.Eval()) + "[listKeys(resourceId('Microsoft.DocumentDb/databaseAccounts', 'test-account'), providers('Microsoft.DocumentDb','databaseAccounts').apiVersions[0]).secondaryreadonlyMasterKey]" + "Secondary Readonly Key is incorrect" + + Expect.equal + (db.PrimaryConnectionString.Eval()) + "[listConnectionStrings(resourceId('Microsoft.DocumentDb/databaseAccounts', 'test-account'), providers('Microsoft.DocumentDb','databaseAccounts').apiVersions[0]).connectionStrings[0].connectionString]" + "Primary Connection String is incorrect" + + Expect.equal + (db.SecondaryConnectionString.Eval()) + "[listConnectionStrings(resourceId('Microsoft.DocumentDb/databaseAccounts', 'test-account'), providers('Microsoft.DocumentDb','databaseAccounts').apiVersions[0]).connectionStrings[1].connectionString]" + "Secondary Connection String is incorrect" + } + + testList "db type" [ + test "default" { + let db = cosmosDb { + name "test" + account_name "account" + } + + Expect.equal db.Kind Document "" + } - test "mongoDB" { - let db = - cosmosDb { - name "test" - kind Mongo - } + test "sql" { + let db = cosmosDb { + name "test" + kind Document + } - Expect.equal db.Kind Mongo "" - } - ] + Expect.equal db.Kind Document "" + } - test "Correctly serializes to JSON" { - let t = arm { add_resource (cosmosDb { name "test" }) } + test "mongoDB" { + let db = cosmosDb { + name "test" + kind Mongo + } - t.Template |> Writer.toJson |> ignore - } - test "Creates connection string and keys with resource groups" { - let conn = - CosmosDb - .getConnectionString( - ResourceId.create (Arm.DocumentDb.databaseAccounts, ResourceName "db", "group"), - PrimaryConnectionString - ) - .Eval() - - let key = - CosmosDb - .getKey( - ResourceId.create (Arm.DocumentDb.databaseAccounts, ResourceName "db", "group"), - PrimaryKey, - ReadWrite - ) - .Eval() - - Expect.equal - key - "[listKeys(resourceId('group', 'Microsoft.DocumentDb/databaseAccounts', 'db'), providers('Microsoft.DocumentDb','databaseAccounts').apiVersions[0]).primaryMasterKey]" - "Primary Key is incorrect" - - Expect.equal - conn - "[listConnectionStrings(resourceId('group', 'Microsoft.DocumentDb/databaseAccounts', 'db'), providers('Microsoft.DocumentDb','databaseAccounts').apiVersions[0]).connectionStrings[0].connectionString]" - "Primary Connection String is incorrect" + Expect.equal db.Kind Mongo "" } - testList - "Account Name Validation tests" - [ - let invalidAccountNameCases = - [ - "Empty Account", "", "cannot be empty", "Name too short" - "Min Length", - "zz", - "min length is 3, but here is 2. The invalid value is 'zz'", - "Name too short" - "Max Length", - "abcdefghij1234567890abcde12345678901234567890", - "max length is 44, but here is 45. The invalid value is 'abcdefghij1234567890abcde12345678901234567890'", - "Name too long" - "Lowercase Only", - "zzzT", - "can only contain lowercase letters. The invalid value is 'zzzT'", - "Upper case character allowed" - "Alphanumeric or dash", - "zzz!", - "can only contain alphanumeric characters or the dash (-). The invalid value is 'zzz!'", - "Non alpha numeric (except dash) character allowed" - ] - - for testName, accountName, error, why in invalidAccountNameCases -> - test testName { - Expect.equal - (CosmosDbValidation.CosmosDbName.Create accountName) - (Error("CosmosDb account names " + error)) - why - } - - let validAccountNameCases = - [ - "Valid Name 1", - "abcdefghij1234567890abcd", - "Should have created a valid CosmosDb account name" - "Valid Name 2", "a-b-c-d-e-12-3-4", "Should have created a valid CosmosDb account name" - ] - - for testName, accountName, why in validAccountNameCases -> - test testName { - Expect.equal - (StorageResourceName.Create(accountName).OkValue.ResourceName) - (ResourceName accountName) - why - } - ] ] + + test "Correctly serializes to JSON" { + let t = arm { add_resource (cosmosDb { name "test" }) } + + t.Template |> Writer.toJson |> ignore + } + test "Creates connection string and keys with resource groups" { + let conn = + CosmosDb + .getConnectionString( + ResourceId.create (Arm.DocumentDb.databaseAccounts, ResourceName "db", "group"), + PrimaryConnectionString + ) + .Eval() + + let key = + CosmosDb + .getKey( + ResourceId.create (Arm.DocumentDb.databaseAccounts, ResourceName "db", "group"), + PrimaryKey, + ReadWrite + ) + .Eval() + + Expect.equal + key + "[listKeys(resourceId('group', 'Microsoft.DocumentDb/databaseAccounts', 'db'), providers('Microsoft.DocumentDb','databaseAccounts').apiVersions[0]).primaryMasterKey]" + "Primary Key is incorrect" + + Expect.equal + conn + "[listConnectionStrings(resourceId('group', 'Microsoft.DocumentDb/databaseAccounts', 'db'), providers('Microsoft.DocumentDb','databaseAccounts').apiVersions[0]).connectionStrings[0].connectionString]" + "Primary Connection String is incorrect" + } + testList "Account Name Validation tests" [ + let invalidAccountNameCases = [ + "Empty Account", "", "cannot be empty", "Name too short" + "Min Length", "zz", "min length is 3, but here is 2. The invalid value is 'zz'", "Name too short" + "Max Length", + "abcdefghij1234567890abcde12345678901234567890", + "max length is 44, but here is 45. The invalid value is 'abcdefghij1234567890abcde12345678901234567890'", + "Name too long" + "Lowercase Only", + "zzzT", + "can only contain lowercase letters. The invalid value is 'zzzT'", + "Upper case character allowed" + "Alphanumeric or dash", + "zzz!", + "can only contain alphanumeric characters or the dash (-). The invalid value is 'zzz!'", + "Non alpha numeric (except dash) character allowed" + ] + + for testName, accountName, error, why in invalidAccountNameCases -> + test testName { + Expect.equal + (CosmosDbValidation.CosmosDbName.Create accountName) + (Error("CosmosDb account names " + error)) + why + } + + let validAccountNameCases = [ + "Valid Name 1", "abcdefghij1234567890abcd", "Should have created a valid CosmosDb account name" + "Valid Name 2", "a-b-c-d-e-12-3-4", "Should have created a valid CosmosDb account name" + ] + + for testName, accountName, why in validAccountNameCases -> + test testName { + Expect.equal + (StorageResourceName.Create(accountName).OkValue.ResourceName) + (ResourceName accountName) + why + } + ] + ] diff --git a/src/Tests/Dashboards.fs b/src/Tests/Dashboards.fs index caf7d86bc..6177f137a 100644 --- a/src/Tests/Dashboards.fs +++ b/src/Tests/Dashboards.fs @@ -6,437 +6,467 @@ open Farmer.Insights open Farmer.Builders let tests = - testList - "Dashboards" - [ - test "Create a simple dashboard" { - // https://docs.microsoft.com/en-us/azure/azure-portal/azure-portal-dashboards-structure - - let vm = - vm { - name "foo" - username "foo" + testList "Dashboards" [ + test "Create a simple dashboard" { + // https://docs.microsoft.com/en-us/azure/azure-portal/azure-portal-dashboards-structure + + let vm = vm { + name "foo" + username "foo" + } + + let vmId = (vm :> IBuilder).ResourceId + + let dash = dashboard { + name "myDashboard" + title "Monitoring" + depends_on vm + + add_markdown_part ( + { + x = 0 + y = 0 + rowSpan = 2 + colSpan = 3 + }, + { + title = "" + subtitle = "" + content = + "## Azure Virtual Machines Overview\r\nNew team members should watch this video to get familiar with Azure Virtual Machines." } + ) - let vmId = (vm :> IBuilder).ResourceId - - let dash = - dashboard { - name "myDashboard" - title "Monitoring" - depends_on vm - - add_markdown_part ( - { - x = 0 - y = 0 - rowSpan = 2 - colSpan = 3 - }, - { - title = "" - subtitle = "" - content = - "## Azure Virtual Machines Overview\r\nNew team members should watch this video to get familiar with Azure Virtual Machines." - } - ) - - add_markdown_part ( - { - x = 3 - y = 0 - rowSpan = 4 - colSpan = 8 - }, - { - title = "Test VM Dashboard" - subtitle = "Contoso" - content = - "This is the team dashboard for the test VM we use on our team. Here are some useful links:\r\n\r\n1. [Getting started](https://www.contoso.com/tsgs)\r\n1. [Troubleshooting guide](https://www.contoso.com/tsgs)\r\n1. [Architecture docs](https://www.contoso.com/tsgs)" - } - ) - - add_video_part ( - { - x = 3 - y = 0 - rowSpan = 4 - colSpan = 8 - }, - { - title = "" - subtitle = "" - url = - "https://www.youtube.com/watch?v=YcylDIiKaSU&list=PLLasX02E8BPCsnETz0XAMfpLR1LIBqpgs&index=4" - } - ) - - add_metrics_chart ( - { - x = 0 - y = 4 - rowSpan = 3 - colSpan = 11 - }, - { - interval = System.TimeSpan(1, 0, 0) |> IsoDateTime.OfTimeSpan - metrics = [ MetricsName.PercentageCPU ] - resourceId = vmId - } - ) - - add_virtual_machine_icon ( - { - x = 9 - y = 7 - rowSpan = 2 - colSpan = 2 - }, - vmId - ) + add_markdown_part ( + { + x = 3 + y = 0 + rowSpan = 4 + colSpan = 8 + }, + { + title = "Test VM Dashboard" + subtitle = "Contoso" + content = + "This is the team dashboard for the test VM we use on our team. Here are some useful links:\r\n\r\n1. [Getting started](https://www.contoso.com/tsgs)\r\n1. [Troubleshooting guide](https://www.contoso.com/tsgs)\r\n1. [Architecture docs](https://www.contoso.com/tsgs)" } + ) - let template = arm { add_resources [ vm; dash ] } - let jsn = template.Template |> Writer.toJson - let jobj = jsn |> Newtonsoft.Json.Linq.JObject.Parse + add_video_part ( + { + x = 3 + y = 0 + rowSpan = 4 + colSpan = 8 + }, + { + title = "" + subtitle = "" + url = + "https://www.youtube.com/watch?v=YcylDIiKaSU&list=PLLasX02E8BPCsnETz0XAMfpLR1LIBqpgs&index=4" + } + ) - let title = - jobj - .SelectToken("resources[?(@.name=='myDashboard')].tags.hidden-title") - .ToString() + add_metrics_chart ( + { + x = 0 + y = 4 + rowSpan = 3 + colSpan = 11 + }, + { + interval = System.TimeSpan(1, 0, 0) |> IsoDateTime.OfTimeSpan + metrics = [ MetricsName.PercentageCPU ] + resourceId = vmId + } + ) - Expect.equal title "Monitoring" "Incorrect title" + add_virtual_machine_icon ( + { + x = 9 + y = 7 + rowSpan = 2 + colSpan = 2 + }, + vmId + ) + } - let lenses = - jobj.SelectToken("resources[?(@.name=='myDashboard')].properties.lenses") + let template = arm { add_resources [ vm; dash ] } + let jsn = template.Template |> Writer.toJson + let jobj = jsn |> Newtonsoft.Json.Linq.JObject.Parse - Expect.isNotNull lenses "Lenses missing" - } + let title = + jobj + .SelectToken("resources[?(@.name=='myDashboard')].tags.hidden-title") + .ToString() - test "Create a complex dashboard with monitor parts" { + Expect.equal title "Monitoring" "Incorrect title" - /// Azure ARM-template dasboard that creates a clock of selected timezone - /// This is example of add_custom_lens. - let clockPart (tz: System.TimeZoneInfo) : Farmer.Arm.Dashboard.LensMetadata = - { - ``type`` = "Extension/HubsExtension/PartType/ClockPart" - settings = - {| - content = - {| - settings = - {| - timezoneId = tz.Id - timeFormat = "HH:mm" - version = 1 - |} - |} - |} - |> box - inputs = [] - filters = None - asset = Unchecked.defaultof - isAdapter = System.Nullable() - defaultMenuItemId = null - } + let lenses = + jobj.SelectToken("resources[?(@.name=='myDashboard')].properties.lenses") - /// Azure ARM-template dasboard graph of Virtual Machine CPU usage - let virtualMachineCPU (vmResourceId: Farmer.ResourceId) : Farmer.Arm.Dashboard.MonitorChartParameters = - let vmName = vmResourceId.Name - let insightsName = "Percentage CPU" // See: Farmer.Insights.MetricsName.PercentageCPU - let title = $"{insightsName} - {vmName.Value}" - let timeSpan = "PT1M" // See: Farmer.IsoDateTime.OfTimeSpan System.TimeSpan(0,1,0) + Expect.isNotNull lenses "Lenses missing" + } - { - chartSettings = - {| - title = title - titleKind = 2 - visualization = - {| - chartType = 3 - disablePinning = true - legendVisualization = - {| - isVisible = true - position = 2 - hideSubtitle = false - |} - axisVisualization = - {| - y = {| isVisible = true; axisType = 1 |} - x = {| isVisible = true; axisType = 2 |} - |} + test "Create a complex dashboard with monitor parts" { + + /// Azure ARM-template dasboard that creates a clock of selected timezone + /// This is example of add_custom_lens. + let clockPart (tz: System.TimeZoneInfo) : Farmer.Arm.Dashboard.LensMetadata = { + ``type`` = "Extension/HubsExtension/PartType/ClockPart" + settings = + {| + content = {| + settings = {| + timezoneId = tz.Id + timeFormat = "HH:mm" + version = 1 + |} + |} + |} + |> box + inputs = [] + filters = None + asset = Unchecked.defaultof + isAdapter = System.Nullable() + defaultMenuItemId = null + } + + /// Azure ARM-template dasboard graph of Virtual Machine CPU usage + let virtualMachineCPU (vmResourceId: Farmer.ResourceId) : Farmer.Arm.Dashboard.MonitorChartParameters = + let vmName = vmResourceId.Name + let insightsName = "Percentage CPU" // See: Farmer.Insights.MetricsName.PercentageCPU + let title = $"{insightsName} - {vmName.Value}" + let timeSpan = "PT1M" // See: Farmer.IsoDateTime.OfTimeSpan System.TimeSpan(0,1,0) + + { + chartSettings = + {| + title = title + titleKind = 2 + visualization = {| + chartType = 3 + disablePinning = true + legendVisualization = {| + isVisible = true + position = 2 + hideSubtitle = false + |} + axisVisualization = {| + y = {| isVisible = true; axisType = 1 |} + x = {| isVisible = true; axisType = 2 |} + |} + |} + metrics = [ + {| + resourceMetadata = {| id = vmResourceId.Eval() |} + name = insightsName + aggregationType = 4 + ``namespace`` = Farmer.Arm.Compute.virtualMachines.Type + metricVisualization = {| + displayName = insightsName + color = "#47BDF5" + resourceDisplayName = vmName.Value |} - metrics = - [ - {| - resourceMetadata = {| id = vmResourceId.Eval() |} - name = insightsName - aggregationType = 4 - ``namespace`` = Farmer.Arm.Compute.virtualMachines.Type - metricVisualization = - {| - displayName = insightsName - color = "#47BDF5" - resourceDisplayName = vmName.Value - |} - |} - ] + |} + ] + |} + :> obj + chartInputs = [ + {| + title = title + visualization = {| chartType = 3 |} + timeContext = {| + relative = {| duration = 3600000 |} + options = {| + useDashboardTimeRange = false + grain = 1 + appliedISOGrain = timeSpan + |} |} - :> obj - chartInputs = - [ + metrics = [ {| - title = title - visualization = {| chartType = 3 |} - timeContext = - {| - relative = {| duration = 3600000 |} - options = - {| - useDashboardTimeRange = false - grain = 1 - appliedISOGrain = timeSpan - |} - |} - metrics = - [ - {| - name = title - resourceMetadata = {| resourceId = vmResourceId.Eval() |} - ``type`` = "host" - aggregationType = 3 (* or 1, do you want Max or Avg ?*) - |} - ] - itemDataModel = - {| - id = System.Guid.NewGuid() - appliedISOGrain = timeSpan - chartHeight = 1 - priorPeriod = false - horizontalBars = true - showOther = false - palette = "multiColor" - jsonDefinitionId = "" - version = {| major = 1; minor = 0; build = 0 |} - filters = - {| - filterType = 0 - id = System.Guid.NewGuid() - OperandFilters = [] - LogicalOperator = 0 - |} - yAxisOptions = {| options = 1 |} - title = insightsName - titleKind = "Auto" - visualization = {| chartType = 3 |} - metrics = - [ - {| - metricAggregation = 4 - color = "#47BDF5" // "#7E58FF" - unit = 5 - useSIConversions = true - displaySIUnit = true - id = - {| - name = {| id = title; displayName = title |} - kind = {| id = "host" |} - ``namespace`` = - {| - name = Farmer.Arm.Compute.virtualMachines.Type - |} - resourceDefinition = - {| - resourceId = vmResourceId.Eval() - id = vmResourceId.Eval() - name = vmName.Value - |} - |} - |} - ] - |} + name = title + resourceMetadata = {| resourceId = vmResourceId.Eval() |} + ``type`` = "host" + aggregationType = 3 (* or 1, do you want Max or Avg ?*) |} ] - filters = None - } - - /// Azure ARM-template dasboard graph of SQL Server DTU usage - let databaseUtilization - (databaseResourceId: Farmer.ResourceId) - : Farmer.Arm.Dashboard.MonitorChartParameters = - let title = $"Resource utilization database - {databaseResourceId.Name.Value}" - let insightsName = "dtu_consumption_percent" // See: Farmer.Insights.MetricsName.SQL_DB_DTU - let insightsNameClear = "DTU percentage" - let timeSpan = "PT1M" - - { - filters = - {| - MsPortalFx_TimeRange = + itemDataModel = {| + id = System.Guid.NewGuid() + appliedISOGrain = timeSpan + chartHeight = 1 + priorPeriod = false + horizontalBars = true + showOther = false + palette = "multiColor" + jsonDefinitionId = "" + version = {| major = 1; minor = 0; build = 0 |} + filters = {| + filterType = 0 + id = System.Guid.NewGuid() + OperandFilters = [] + LogicalOperator = 0 + |} + yAxisOptions = {| options = 1 |} + title = insightsName + titleKind = "Auto" + visualization = {| chartType = 3 |} + metrics = [ {| - model = - {| - format = "local" - granularity = "auto" - relative = "60m" + metricAggregation = 4 + color = "#47BDF5" // "#7E58FF" + unit = 5 + useSIConversions = true + displaySIUnit = true + id = {| + name = {| id = title; displayName = title |} + kind = {| id = "host" |} + ``namespace`` = {| + name = Farmer.Arm.Compute.virtualMachines.Type + |} + resourceDefinition = {| + resourceId = vmResourceId.Eval() + id = vmResourceId.Eval() + name = vmName.Value |} + |} |} + ] |} - :> obj - chartSettings = - {| - title = title - openBladeOnClick = {| openBlade = true |} - visualization = - {| - chartType = 2 - legendVisualization = null - disablePinning = true - axisVisualization = {| y = {| isVisible = true |} |} + |} + ] + filters = None + } + + /// Azure ARM-template dasboard graph of SQL Server DTU usage + let databaseUtilization + (databaseResourceId: Farmer.ResourceId) + : Farmer.Arm.Dashboard.MonitorChartParameters = + let title = $"Resource utilization database - {databaseResourceId.Name.Value}" + let insightsName = "dtu_consumption_percent" // See: Farmer.Insights.MetricsName.SQL_DB_DTU + let insightsNameClear = "DTU percentage" + let timeSpan = "PT1M" + + { + filters = + {| + MsPortalFx_TimeRange = {| + model = {| + format = "local" + granularity = "auto" + relative = "60m" + |} + |} + |} + :> obj + chartSettings = + {| + title = title + openBladeOnClick = {| openBlade = true |} + visualization = {| + chartType = 2 + legendVisualization = null + disablePinning = true + axisVisualization = {| y = {| isVisible = true |} |} + |} + metrics = [ + {| + resourceMetadata = {| id = databaseResourceId.Eval() |} + name = insightsName + aggregationType = 3 + metricVisualization = {| + displayName = insightsNameClear + color = "#47BDF5" + resourceDisplayName = null |} - metrics = - [ - {| - resourceMetadata = {| id = databaseResourceId.Eval() |} - name = insightsName - aggregationType = 3 - metricVisualization = - {| - displayName = insightsNameClear - color = "#47BDF5" - resourceDisplayName = null - |} - |} - ] + |} + ] + |} + :> obj + chartInputs = [ + {| + title = title + ariaLabel = null + filterCollection = null + grouping = null + visualization = {| + axisVisualization = null + legendVisualization = null + chartType = null |} - :> obj - chartInputs = - [ + resolvedBladeToOpenOnClick = {| openBlade = true |} + timespan = {| + relative = {| duration = 3600000 |} + |} + timeContext = {| + options = {| + useDashboardTimeRange = false + grain = 1 + |} + relative = {| duration = 3600000 |} + |} + metrics = [ {| - title = title - ariaLabel = null - filterCollection = null - grouping = null - visualization = - {| - axisVisualization = null - legendVisualization = null - chartType = null - |} - resolvedBladeToOpenOnClick = {| openBlade = true |} - timespan = - {| - relative = {| duration = 3600000 |} - |} - timeContext = - {| - options = - {| - useDashboardTimeRange = false - grain = 1 - |} - relative = {| duration = 3600000 |} - |} - metrics = - [ - {| - resourceMetadata = - {| - id = databaseResourceId.Eval() - kind = "v12.0,user" - |} - name = insightsName - metricVisualization = null - aggregationType = 3 - thresholds = [] - |} - ] - itemDataModel = - {| - id = $"defaultAiChartDiv{System.Guid.NewGuid()}" - grouping = null - chartHeight = 1 - priorPeriod = false - horizontalBars = true - showOther = false - palette = "multiColor" - jsonDefinitionId = "" - yAxisOptions = {| options = 1 |} - appliedISOGrain = timeSpan - title = title - titleKind = "Auto" - visualization = - {| - chartType = 2 - legend = null - axis = null - |} - metrics = - [ - {| - id = - {| - resourceDefinition = - {| - id = databaseResourceId.Eval() - name = null - |} - name = - {| - id = insightsName - displayName = insightsNameClear - |} - |} - metricAggregation = 3 - color = "#47BDF5" - unit = 5 - useSIConversions = false - displaySIUnit = true - |} - ] - |} + resourceMetadata = {| + id = databaseResourceId.Eval() + kind = "v12.0,user" + |} + name = insightsName + metricVisualization = null + aggregationType = 3 + thresholds = [] |} ] - } - - - /// Azure ARM-template dasboard graph of Application Insights, user page render times. Needs the Javascript to the page. - let appInsights_PageResponseTimes - (appInsightsId: Farmer.ResourceId) - : Farmer.Arm.Dashboard.MonitorChartParameters = - let timeSpan = "PT5M" - - { - filters = - {| - MsPortalFx_TimeRange = + itemDataModel = {| + id = $"defaultAiChartDiv{System.Guid.NewGuid()}" + grouping = null + chartHeight = 1 + priorPeriod = false + horizontalBars = true + showOther = false + palette = "multiColor" + jsonDefinitionId = "" + yAxisOptions = {| options = 1 |} + appliedISOGrain = timeSpan + title = title + titleKind = "Auto" + visualization = {| + chartType = 2 + legend = null + axis = null + |} + metrics = [ {| - model = - {| - format = "local" - granularity = "auto" - relative = "1440m" + id = {| + resourceDefinition = {| + id = databaseResourceId.Eval() + name = null + |} + name = {| + id = insightsName + displayName = insightsNameClear |} + |} + metricAggregation = 3 + color = "#47BDF5" + unit = 5 + useSIConversions = false + displaySIUnit = true |} + ] |} - :> obj - chartSettings = - {| + |} + ] + } + + + /// Azure ARM-template dasboard graph of Application Insights, user page render times. Needs the Javascript to the page. + let appInsights_PageResponseTimes + (appInsightsId: Farmer.ResourceId) + : Farmer.Arm.Dashboard.MonitorChartParameters = + let timeSpan = "PT5M" + + { + filters = + {| + MsPortalFx_TimeRange = {| + model = {| + format = "local" + granularity = "auto" + relative = "1440m" + |} + |} + |} + :> obj + chartSettings = + {| + title = "Avg Page load network connect time, Avg Client processing time" + visualization = {| + chartType = 3 + legendVisualization = {| + isVisible = true + position = 2 + hideSubtitle = false + |} + axisVisualization = {| y = {| isVisible = true |} |} + disablePinning = true + |} + metrics = + [ + "networkDuration", "Page load network connect time", "#47BDF5" + "processingDuration", "Client processing time", "#7E58FF" + "sendDuration", "Send request time", "#44F1C8" + "receiveDuration", "Receiving response time", "#EB9371" + ] + |> List.map (fun (typ, nam, col) -> {| + resourceMetadata = {| + id = appInsightsId.Eval() + kind = "Historical" + |} + name = $"browserTimings/{typ}" + aggregationType = 4 + ``namespac`` = Farmer.Arm.Insights.components.Type + metricVisualization = {| displayName = nam; color = col |} + |}) + |} + :> obj + chartInputs = [ + {| + title = "Avg Page load network connect time, Avg Client processing time" + visualization = {| + chartType = 3 + legend = {| + isVisible = true + position = 2 + hideSubtitle = false + |} + |} + timeContext = {| + options = {| + useDashboardTimeRange = false + grain = 1 + appliedISOGrain = timeSpan + |} + relative = {| duration = 86400000 |} + |} + metrics = + [ "networkDuration"; "processingDuration"; "sendDuration"; "receiveDuration" ] + |> List.map (fun ctype -> {| + name = $"browserTimings/{ctype}" + ``type`` = "Historical" + resourceMetadata = {| resourceId = appInsightsId.Eval() |} + aggregationType = 1 + |}) + itemDataModel = {| + id = System.Guid.NewGuid() + chartHeight = 1 + priorPeriod = false + horizontalBars = true + showOther = false + aggregation = 1 + palette = "multiColor" + jsonDefinitionId = System.Guid.NewGuid() + titleKind = "Auto" + version = {| major = 1; minor = 0; build = 0 |} + yAxisOptions = {| options = 1 |} + appliedISOGrain = timeSpan + filters = {| + filterType = 0 + id = System.Guid.NewGuid() + OperandFilters = [] + LogicalOperator = 0 + |} title = "Avg Page load network connect time, Avg Client processing time" - visualization = - {| - chartType = 3 - legendVisualization = - {| - isVisible = true - position = 2 - hideSubtitle = false - |} - axisVisualization = {| y = {| isVisible = true |} |} - disablePinning = true + visualization = {| + chartType = 3 + legend = {| + isVisible = true + position = 2 + hideSubtitle = false |} + |} metrics = [ "networkDuration", "Page load network connect time", "#47BDF5" @@ -444,459 +474,325 @@ let tests = "sendDuration", "Send request time", "#44F1C8" "receiveDuration", "Receiving response time", "#EB9371" ] - |> List.map (fun (typ, nam, col) -> - {| - resourceMetadata = - {| - id = appInsightsId.Eval() - kind = "Historical" - |} - name = $"browserTimings/{typ}" - aggregationType = 4 - ``namespac`` = Farmer.Arm.Insights.components.Type - metricVisualization = {| displayName = nam; color = col |} - |}) - |} - :> obj - chartInputs = - [ - {| - title = "Avg Page load network connect time, Avg Client processing time" - visualization = - {| - chartType = 3 - legend = - {| - isVisible = true - position = 2 - hideSubtitle = false - |} - |} - timeContext = - {| - options = - {| - useDashboardTimeRange = false - grain = 1 - appliedISOGrain = timeSpan - |} - relative = {| duration = 86400000 |} - |} - metrics = - [ "networkDuration"; "processingDuration"; "sendDuration"; "receiveDuration" ] - |> List.map (fun ctype -> - {| - name = $"browserTimings/{ctype}" - ``type`` = "Historical" - resourceMetadata = {| resourceId = appInsightsId.Eval() |} - aggregationType = 1 - |}) - itemDataModel = - {| - id = System.Guid.NewGuid() - chartHeight = 1 - priorPeriod = false - horizontalBars = true - showOther = false - aggregation = 1 - palette = "multiColor" - jsonDefinitionId = System.Guid.NewGuid() - titleKind = "Auto" - version = {| major = 1; minor = 0; build = 0 |} - yAxisOptions = {| options = 1 |} - appliedISOGrain = timeSpan - filters = - {| - filterType = 0 - id = System.Guid.NewGuid() - OperandFilters = [] - LogicalOperator = 0 - |} - title = "Avg Page load network connect time, Avg Client processing time" - visualization = - {| - chartType = 3 - legend = - {| - isVisible = true - position = 2 - hideSubtitle = false - |} - |} - metrics = - [ - "networkDuration", "Page load network connect time", "#47BDF5" - "processingDuration", "Client processing time", "#7E58FF" - "sendDuration", "Send request time", "#44F1C8" - "receiveDuration", "Receiving response time", "#EB9371" - ] - |> List.map (fun (typ, nam, col) -> - {| - metricAggregation = 4 - color = col - id = - {| - dataSource = 1 - resourceDefinition = {| id = appInsightsId.Eval() |} - name = - {| - id = $"browserTimings/{typ}" - displayName = nam - |} - ``namespace`` = - {| - name = Farmer.Arm.Insights.components.Type - |} - kind = - {| - id = "Historical" - displayName = "Historical" - |} - |} - |}) + |> List.map (fun (typ, nam, col) -> {| + metricAggregation = 4 + color = col + id = {| + dataSource = 1 + resourceDefinition = {| id = appInsightsId.Eval() |} + name = {| + id = $"browserTimings/{typ}" + displayName = nam + |} + ``namespace`` = {| + name = Farmer.Arm.Insights.components.Type + |} + kind = {| + id = "Historical" + displayName = "Historical" + |} |} + |}) + |} + |} + ] + } + + /// Azure ARM-template dasboard graph of Application Insights, unique users count. Needs the Javascript to the page. + let appInsights_UniqueUsers + (appInsightsId: Farmer.ResourceId) + : Farmer.Arm.Dashboard.MonitorChartParameters = + let timeSpan = "PT5M" + + { + filters = + {| + MsPortalFx_TimeRange = {| + model = {| + format = "local" + granularity = "auto" + relative = "60m" |} - ] - } - - /// Azure ARM-template dasboard graph of Application Insights, unique users count. Needs the Javascript to the page. - let appInsights_UniqueUsers - (appInsightsId: Farmer.ResourceId) - : Farmer.Arm.Dashboard.MonitorChartParameters = - let timeSpan = "PT5M" - - { - filters = - {| - MsPortalFx_TimeRange = + |} + ``type`` = {| + model = {| + operator = "equals" + values = [ "pageView"; "customEvent"; "request" ] + |} + |} + ``operation/synthetic`` = {| + model = {| + operator = "equals" + values = [ "False" ] + |} + |} + |} + :> obj + chartSettings = + {| + title = "Unique Users" + visualization = {| + chartType = 1 + legendVisualization = null + disablePinning = true + axisVisualization = {| y = {| isVisible = true |} |} + |} + filterCollection = {| + filters = [ {| - model = - {| - format = "local" - granularity = "auto" - relative = "60m" - |} + key = "type" + operator = 0 + values = [ "pageView"; "customEvent"; "request" ] |} - ``type`` = {| - model = - {| - operator = "equals" - values = [ "pageView"; "customEvent"; "request" ] - |} + key = "operation/synthetic" + operator = 0 + values = [ "False" ] |} - ``operation/synthetic`` = - {| - model = - {| - operator = "equals" - values = [ "False" ] - |} + ] + |} + openBladeOnClick = {| + openBlade = true + destinationBlade = {| + parameters = {| + id = appInsightsId.Eval() + menuid = "segmentationUsers" + |} + bladeName = "ResourceMenuBlade" + extensionName = "HubsExtension" + options = {| + parameters = {| + id = appInsightsId.Eval() + menuid = "segmentationUsers" + |} |} + |} |} - :> obj - chartSettings = - {| - title = "Unique Users" - visualization = - {| - chartType = 1 - legendVisualization = null - disablePinning = true - axisVisualization = {| y = {| isVisible = true |} |} + metrics = [ + {| + resourceMetadata = {| id = appInsightsId.Eval() |} + name = "users/count" + aggregationType = 5 + ``namespace`` = "microsoft.insights/components/kusto" + metricVisualization = {| + displayName = "Users" + color = "#4683de" |} - filterCollection = + |} + ] + |} + :> obj + chartInputs = [ + {| + title = "Unique Users" + ariaLabel = null + grouping = null + visualization = {| + axisVisualization = null + legendVisualization = null + chartType = 1 + |} + filterCollection = {| + filters = [ {| - filters = - [ - {| - key = "type" - operator = 0 - values = [ "pageView"; "customEvent"; "request" ] - |} - {| - key = "operation/synthetic" - operator = 0 - values = [ "False" ] - |} - ] + key = "type" + values = [ "pageView"; "customEvent"; "request" ] |} - openBladeOnClick = {| - openBlade = true - destinationBlade = - {| - parameters = - {| - id = appInsightsId.Eval() - menuid = "segmentationUsers" - |} - bladeName = "ResourceMenuBlade" - extensionName = "HubsExtension" - options = - {| - parameters = - {| - id = appInsightsId.Eval() - menuid = "segmentationUsers" - |} - |} - |} + key = "operation/synthetic" + values = [ "False" ] |} - metrics = - [ - {| - resourceMetadata = {| id = appInsightsId.Eval() |} - name = "users/count" - aggregationType = 5 - ``namespace`` = "microsoft.insights/components/kusto" - metricVisualization = - {| - displayName = "Users" - color = "#4683de" - |} - |} - ] + ] + |} + timeContext = {| + options = {| + useDashboardTimeRange = false + grain = 1 + |} + relative = {| duration = 3600000 |} + |} + resolvedBladeToOpenOnClick = {| + openBlade = true + resolvedBlade = {| + extension = "HubsExtension" + detailBlade = "ResourceMenuBlade" + detailBladeInputs = {| + id = appInsightsId.Eval() + menuid = "segmentationUsers" + |} + |} + |} + timespan = {| + relative = {| duration = 3600000 |} |} - :> obj - chartInputs = - [ + metrics = [ {| - title = "Unique Users" - ariaLabel = null - grouping = null - visualization = - {| - axisVisualization = null - legendVisualization = null - chartType = 1 - |} - filterCollection = - {| - filters = - [ - {| - key = "type" - values = [ "pageView"; "customEvent"; "request" ] - |} - {| - key = "operation/synthetic" - values = [ "False" ] - |} - ] - |} - timeContext = - {| - options = - {| - useDashboardTimeRange = false - grain = 1 - |} - relative = {| duration = 3600000 |} + resourceMetadata = {| id = appInsightsId.Eval() |} + name = "users/count" + metricVisualization = {| color = "#4683de" |} + aggregationType = 5 + thresholds = [] + |} + ] + itemDataModel = {| + id = $"defaultAiChartDiv{System.Guid.NewGuid()}" + grouping = null + chartHeight = 1 + appliedISOGrain = timeSpan + priorPeriod = false + horizontalBars = true + showOther = false + palette = "multiColor" + jsonDefinitionId = "" + yAxisOptions = {| options = 1 |} + title = "Unique Users" + titleKind = "Auto" + visualization = {| + chartType = 1 + legend = null + axis = null + |} + metrics = [ + {| + metricAggregation = 5 + color = "#4683de" + unit = 1 + id = {| + resourceDefinition = {| id = appInsightsId.Eval() |} + name = {| + id = "users/count" + displayName = "Users" + |} + ``namespace`` = {| + name = "microsoft.insights/components/kusto" + |} |} - resolvedBladeToOpenOnClick = + |} + ] + filters = {| + filterType = 0 + id = System.Guid.NewGuid() + LogicalOperator = 0 + OperandFilters = [ {| - openBlade = true - resolvedBlade = - {| - extension = "HubsExtension" - detailBlade = "ResourceMenuBlade" - detailBladeInputs = - {| - id = appInsightsId.Eval() - menuid = "segmentationUsers" - |} - |} + filterType = 1 + id = System.Guid.NewGuid() + ComparisonOperator = 0 + OperandSelectedKey = {| dimensionName = {| id = "type" |} |} + OperandSelectedValues = [ "pageView"; "customEvent"; "request" ] |} - timespan = {| - relative = {| duration = 3600000 |} - |} - metrics = - [ - {| - resourceMetadata = {| id = appInsightsId.Eval() |} - name = "users/count" - metricVisualization = {| color = "#4683de" |} - aggregationType = 5 - thresholds = [] + filterType = 1 + id = System.Guid.NewGuid() + ComparisonOperator = 0 + OperandSelectedKey = {| + dimensionName = {| id = "operation/synthetic" |} |} - ] - itemDataModel = - {| - id = $"defaultAiChartDiv{System.Guid.NewGuid()}" - grouping = null - chartHeight = 1 - appliedISOGrain = timeSpan - priorPeriod = false - horizontalBars = true - showOther = false - palette = "multiColor" - jsonDefinitionId = "" - yAxisOptions = {| options = 1 |} - title = "Unique Users" - titleKind = "Auto" - visualization = - {| - chartType = 1 - legend = null - axis = null - |} - metrics = - [ - {| - metricAggregation = 5 - color = "#4683de" - unit = 1 - id = - {| - resourceDefinition = {| id = appInsightsId.Eval() |} - name = - {| - id = "users/count" - displayName = "Users" - |} - ``namespace`` = - {| - name = "microsoft.insights/components/kusto" - |} - |} - |} - ] - filters = - {| - filterType = 0 - id = System.Guid.NewGuid() - LogicalOperator = 0 - OperandFilters = - [ - {| - filterType = 1 - id = System.Guid.NewGuid() - ComparisonOperator = 0 - OperandSelectedKey = - {| dimensionName = {| id = "type" |} |} - OperandSelectedValues = - [ "pageView"; "customEvent"; "request" ] - |} - {| - filterType = 1 - id = System.Guid.NewGuid() - ComparisonOperator = 0 - OperandSelectedKey = - {| - dimensionName = {| id = "operation/synthetic" |} - |} - OperandSelectedValues = [ "False" ] - |} - ] - |} + OperandSelectedValues = [ "False" ] |} + ] |} - ] + |} + |} + ] + } + + let dashboardId = "Monitor-MyEnvironment" + + let positions: Farmer.Arm.Dashboard.LensPosition list = [ // Little matrix of sizes and positins in the screen + { + x = 0 + y = 0 + colSpan = 10 + rowSpan = 6 + } // vm + { + x = 10 + y = 0 + colSpan = 11 + rowSpan = 6 + } // db + { + x = 0 + y = 6 + colSpan = 9 + rowSpan = 6 + } // response times + { + x = 9 + y = 6 + colSpan = 6 + rowSpan = 6 + } // user count + { + x = 15 + y = 10 + colSpan = 2 + rowSpan = 2 + } // clock + ] + + // Some resources + let ai = appInsights { name "myInsights" } + + let database = sqlServer { + name "server543c8" + admin_username "isaac" + + add_databases [ + sqlDb { + name "db" + sku Farmer.Sql.DtuSku.S0 } + ] + } - let dashboardId = "Monitor-MyEnvironment" - - let positions: Farmer.Arm.Dashboard.LensPosition list = - [ // Little matrix of sizes and positins in the screen - { - x = 0 - y = 0 - colSpan = 10 - rowSpan = 6 - } // vm - { - x = 10 - y = 0 - colSpan = 11 - rowSpan = 6 - } // db - { - x = 0 - y = 6 - colSpan = 9 - rowSpan = 6 - } // response times - { - x = 9 - y = 6 - colSpan = 6 - rowSpan = 6 - } // user count - { - x = 15 - y = 10 - colSpan = 2 - rowSpan = 2 - } // clock - ] + let vm = vm { + name "myVm" + username "foo" + custom_data "foo" + } - // Some resources - let ai = appInsights { name "myInsights" } + let aiResId = ResourceId.create (Farmer.Arm.Insights.components, ai.Name) + let vmResId = vm.ResourceId - let database = - sqlServer { - name "server543c8" - admin_username "isaac" + let dbResId = + Farmer.Arm.Sql.databases.resourceId (database.Name.ResourceName, database.Databases.Head.Name) - add_databases - [ - sqlDb { - name "db" - sku Farmer.Sql.DtuSku.S0 - } - ] - } + let lenspart = clockPart (System.TimeZoneInfo.Local) - let vm = - vm { - name "myVm" - username "foo" - custom_data "foo" - } + let dashboard2 = dashboard { + name dashboardId + title dashboardId + depends_on [ vm :> IBuilder; database :> IBuilder; ai :> IBuilder ] + add_monitor_chart (positions.[0], virtualMachineCPU vmResId) + add_monitor_chart (positions.[1], databaseUtilization dbResId) + add_monitor_chart (positions.[2], appInsights_PageResponseTimes aiResId) + add_monitor_chart (positions.[3], appInsights_UniqueUsers aiResId) - let aiResId = ResourceId.create (Farmer.Arm.Insights.components, ai.Name) - let vmResId = vm.ResourceId - - let dbResId = - Farmer.Arm.Sql.databases.resourceId (database.Name.ResourceName, database.Databases.Head.Name) - - let lenspart = clockPart (System.TimeZoneInfo.Local) - - let dashboard2 = - dashboard { - name dashboardId - title dashboardId - depends_on [ vm :> IBuilder; database :> IBuilder; ai :> IBuilder ] - add_monitor_chart (positions.[0], virtualMachineCPU vmResId) - add_monitor_chart (positions.[1], databaseUtilization dbResId) - add_monitor_chart (positions.[2], appInsights_PageResponseTimes aiResId) - add_monitor_chart (positions.[3], appInsights_UniqueUsers aiResId) - - add_custom_lens ( - { - position = positions.[4] - metadata = lenspart - } - ) + add_custom_lens ( + { + position = positions.[4] + metadata = lenspart } + ) + } - let template = arm { add_resources [ ai; database; vm; dashboard2 ] } - let jsn = template.Template |> Writer.toJson - let jobj = jsn |> Newtonsoft.Json.Linq.JObject.Parse + let template = arm { add_resources [ ai; database; vm; dashboard2 ] } + let jsn = template.Template |> Writer.toJson + let jobj = jsn |> Newtonsoft.Json.Linq.JObject.Parse - let title = - jobj - .SelectToken("resources[?(@.name=='Monitor-MyEnvironment')].tags.hidden-title") - .ToString() + let title = + jobj + .SelectToken("resources[?(@.name=='Monitor-MyEnvironment')].tags.hidden-title") + .ToString() - Expect.equal title "Monitor-MyEnvironment" "Incorrect title" + Expect.equal title "Monitor-MyEnvironment" "Incorrect title" - let lenses = - jobj.SelectToken("resources[?(@.name=='Monitor-MyEnvironment')].properties.lenses") + let lenses = + jobj.SelectToken("resources[?(@.name=='Monitor-MyEnvironment')].properties.lenses") - Expect.isNotNull lenses "Lenses missing" - } + Expect.isNotNull lenses "Lenses missing" + } - ] + ] diff --git a/src/Tests/Databricks.fs b/src/Tests/Databricks.fs index 4d5ca85af..4e50b5aee 100644 --- a/src/Tests/Databricks.fs +++ b/src/Tests/Databricks.fs @@ -12,162 +12,167 @@ open System type ValueObj<'T> = {| value: 'T |} -type WorkspaceJson = - { - name: string - dependsOn: string array - sku: {| name: string |} - properties: {| managedResourceGroupId: string - parameters: {| enableNoPublicIp: bool ValueObj - prepareEncryption: bool ValueObj - customVirtualNetworkId: string ValueObj - customPublicSubnetName: string ValueObj - customPrivateSubnetName: string ValueObj - encryption: {| keySource: string - keyName: string - keyversion: string - keyvaulturi: string |} ValueObj |} |} - } +type WorkspaceJson = { + name: string + dependsOn: string array + sku: {| name: string |} + properties: {| + managedResourceGroupId: string + parameters: + {| + enableNoPublicIp: bool ValueObj + prepareEncryption: bool ValueObj + customVirtualNetworkId: string ValueObj + customPublicSubnetName: string ValueObj + customPrivateSubnetName: string ValueObj + encryption: + {| + keySource: string + keyName: string + keyversion: string + keyvaulturi: string + |} ValueObj + |} + |} +} let fromJson (db: DatabricksConfig) = toTypedTemplate Location.NorthEurope db let tests = - testList - "Databricks Tests" - [ - let getWorkspaceArm (db: DatabricksConfig) = - (db :> IBuilder).BuildResources Location.NorthEurope |> List.head :?> Workspace - - test "Creates a basic workspace" { - let bricks = databricks { name "databricks-workspace" } |> fromJson - Expect.equal bricks.name "databricks-workspace" "Wrong workspace name" - Expect.equal bricks.sku.name "standard" "Wrong pricing tier" - - Expect.equal - bricks.properties.managedResourceGroupId - "[concat(subscription().id, '/resourceGroups/', 'databricks-workspace-rg')]" - "Wrong managed resource group name" - - Expect.isFalse bricks.properties.parameters.enableNoPublicIp.value "Public IP enabled by default" - Expect.isFalse bricks.properties.parameters.prepareEncryption.value "Encryption off by default" - } - - test "Allows overriding managed resource group name" { - let bricks = databricks { managed_resource_group_id "databricks-rg" } |> fromJson - - Expect.equal - bricks.properties.managedResourceGroupId - "[concat(subscription().id, '/resourceGroups/', 'databricks-rg')]" - "Wrong managed resource group name" - } - - test "Handles use_public_ip feature flag" { - let bricks = databricks { allow_public_ip Disabled } |> fromJson - Expect.isTrue bricks.properties.parameters.enableNoPublicIp.value "Enable public IP not set correctly" - } - - test "Sets BYOV configuration" { - let bricks = - databricks { attach_to_vnet "databricks-vnet" "databricks-pub-snet" "databricks-priv-snet" } - |> fromJson - - Expect.equal - bricks.properties.parameters.customVirtualNetworkId.value - "[resourceId('Microsoft.Network/virtualNetworks', 'databricks-vnet')]" - "BYOV vnet is not set correctly" - - Expect.equal - bricks.properties.parameters.customPublicSubnetName.value - "databricks-pub-snet" - "BYOV public subnet not set correctly" - - Expect.equal - bricks.properties.parameters.customPrivateSubnetName.value - "databricks-priv-snet" - "BYOV private subnet not set correctly" - - let vn = vnet { name "test" } - let pubSubnet = buildSubnet "public" 26 - let privSubnet = buildSubnet "private" 24 - let bricks = databricks { attach_to_vnet vn pubSubnet privSubnet } |> fromJson - - Expect.equal - bricks.properties.parameters.customVirtualNetworkId.value - "[resourceId('Microsoft.Network/virtualNetworks', 'test')]" - "BYOV vnet is not set correctly" - - Expect.equal - bricks.properties.parameters.customPublicSubnetName.value - "public" - "BYOV public subnet not set correctly" - - Expect.equal - bricks.properties.parameters.customPrivateSubnetName.value - "private" - "BYOV private subnet not set correctly" - - Expect.contains bricks.dependsOn (virtualNetworks.resourceId("test").Eval()) "Incorrect dependency" - } - - test "Encryption works correctly" { - let bricks = - databricks { - sku Premium - encrypt_with_key_vault "databricks-kv" "databricks-encryption-key" - } - |> fromJson - - let parameters = bricks.properties.parameters - Expect.isTrue parameters.prepareEncryption.value "Encryption off by default" - Expect.equal parameters.encryption.value.keySource "Microsoft.Keyvault" "Incorrect source" - - Expect.equal - parameters.encryption.value.keyName - "databricks-encryption-key" - "Key name not initialised correctly" - - Expect.isNull parameters.encryption.value.keyversion "Key version not initialised correctly" - - Expect.equal - parameters.encryption.value.keyvaulturi - "https://databricks-kv.vault.azure.net" - "Key vault uri not initialised correctly" - - Expect.equal bricks.sku.name "premium" "Wrong sku" - Expect.contains bricks.dependsOn (vaults.resourceId("databricks-kv").Eval()) "Incorrect dependency" - - let bricks = - databricks { - sku Premium - encrypt_with_key_vault "databricks-kv" "databricks-encryption-key" - key_vault_key_version (Guid.Parse "74135499-7a08-45fa-9ebd-94670097b04a") // arbitrary for test - } - |> fromJson - - Expect.equal - bricks.properties.parameters.encryption.value.keyversion - "74135499-7a08-45fa-9ebd-94670097b04a" - "Key vault version not set correctly" - - Expect.throws - (fun () -> databricks { key_vault_key_version Guid.Empty } |> ignore) - "Should not be able to set key version without vault config" - - let bricks = - databricks { - sku Premium - encrypt_with_databricks - } - |> fromJson - - Expect.equal - bricks.properties.parameters.encryption.value.keySource - "Default" - "encryption mode should be databricks" - - Expect.throws - (fun () -> databricks { encrypt_with_key_vault "test" "test" } |> ignore) - "Should not be able to set key vault without Premium set" - } - ] + testList "Databricks Tests" [ + let getWorkspaceArm (db: DatabricksConfig) = + (db :> IBuilder).BuildResources Location.NorthEurope |> List.head :?> Workspace + + test "Creates a basic workspace" { + let bricks = databricks { name "databricks-workspace" } |> fromJson + Expect.equal bricks.name "databricks-workspace" "Wrong workspace name" + Expect.equal bricks.sku.name "standard" "Wrong pricing tier" + + Expect.equal + bricks.properties.managedResourceGroupId + "[concat(subscription().id, '/resourceGroups/', 'databricks-workspace-rg')]" + "Wrong managed resource group name" + + Expect.isFalse bricks.properties.parameters.enableNoPublicIp.value "Public IP enabled by default" + Expect.isFalse bricks.properties.parameters.prepareEncryption.value "Encryption off by default" + } + + test "Allows overriding managed resource group name" { + let bricks = databricks { managed_resource_group_id "databricks-rg" } |> fromJson + + Expect.equal + bricks.properties.managedResourceGroupId + "[concat(subscription().id, '/resourceGroups/', 'databricks-rg')]" + "Wrong managed resource group name" + } + + test "Handles use_public_ip feature flag" { + let bricks = databricks { allow_public_ip Disabled } |> fromJson + Expect.isTrue bricks.properties.parameters.enableNoPublicIp.value "Enable public IP not set correctly" + } + + test "Sets BYOV configuration" { + let bricks = + databricks { attach_to_vnet "databricks-vnet" "databricks-pub-snet" "databricks-priv-snet" } + |> fromJson + + Expect.equal + bricks.properties.parameters.customVirtualNetworkId.value + "[resourceId('Microsoft.Network/virtualNetworks', 'databricks-vnet')]" + "BYOV vnet is not set correctly" + + Expect.equal + bricks.properties.parameters.customPublicSubnetName.value + "databricks-pub-snet" + "BYOV public subnet not set correctly" + + Expect.equal + bricks.properties.parameters.customPrivateSubnetName.value + "databricks-priv-snet" + "BYOV private subnet not set correctly" + + let vn = vnet { name "test" } + let pubSubnet = buildSubnet "public" 26 + let privSubnet = buildSubnet "private" 24 + let bricks = databricks { attach_to_vnet vn pubSubnet privSubnet } |> fromJson + + Expect.equal + bricks.properties.parameters.customVirtualNetworkId.value + "[resourceId('Microsoft.Network/virtualNetworks', 'test')]" + "BYOV vnet is not set correctly" + + Expect.equal + bricks.properties.parameters.customPublicSubnetName.value + "public" + "BYOV public subnet not set correctly" + + Expect.equal + bricks.properties.parameters.customPrivateSubnetName.value + "private" + "BYOV private subnet not set correctly" + + Expect.contains bricks.dependsOn (virtualNetworks.resourceId("test").Eval()) "Incorrect dependency" + } + + test "Encryption works correctly" { + let bricks = + databricks { + sku Premium + encrypt_with_key_vault "databricks-kv" "databricks-encryption-key" + } + |> fromJson + + let parameters = bricks.properties.parameters + Expect.isTrue parameters.prepareEncryption.value "Encryption off by default" + Expect.equal parameters.encryption.value.keySource "Microsoft.Keyvault" "Incorrect source" + + Expect.equal + parameters.encryption.value.keyName + "databricks-encryption-key" + "Key name not initialised correctly" + + Expect.isNull parameters.encryption.value.keyversion "Key version not initialised correctly" + + Expect.equal + parameters.encryption.value.keyvaulturi + "https://databricks-kv.vault.azure.net" + "Key vault uri not initialised correctly" + + Expect.equal bricks.sku.name "premium" "Wrong sku" + Expect.contains bricks.dependsOn (vaults.resourceId("databricks-kv").Eval()) "Incorrect dependency" + + let bricks = + databricks { + sku Premium + encrypt_with_key_vault "databricks-kv" "databricks-encryption-key" + key_vault_key_version (Guid.Parse "74135499-7a08-45fa-9ebd-94670097b04a") // arbitrary for test + } + |> fromJson + + Expect.equal + bricks.properties.parameters.encryption.value.keyversion + "74135499-7a08-45fa-9ebd-94670097b04a" + "Key vault version not set correctly" + + Expect.throws + (fun () -> databricks { key_vault_key_version Guid.Empty } |> ignore) + "Should not be able to set key version without vault config" + + let bricks = + databricks { + sku Premium + encrypt_with_databricks + } + |> fromJson + + Expect.equal + bricks.properties.parameters.encryption.value.keySource + "Default" + "encryption mode should be databricks" + + Expect.throws + (fun () -> databricks { encrypt_with_key_vault "test" "test" } |> ignore) + "Should not be able to set key vault without Premium set" + } + ] diff --git a/src/Tests/DedicatedHosts.fs b/src/Tests/DedicatedHosts.fs index cdc4861e9..6f6d7a05a 100644 --- a/src/Tests/DedicatedHosts.fs +++ b/src/Tests/DedicatedHosts.fs @@ -14,123 +14,116 @@ let client = new ComputeManagementClient(Uri "http://management.azure.com", TokenCredentials "NotNullOrWhiteSpace") let tests = - testList - "Dedicated Hosts" - [ - test "Can create a basic dedicated host group" { - let deployment = - arm { - location Location.EastUS - - add_resources [ hostGroup { name "myhostgroup" } ] - } - - let jobj = deployment.Template |> Writer.toJson |> JObject.Parse - - let hostGroup = - jobj.SelectToken "resources[?(@.type=='Microsoft.Compute/hostGroups')]" + testList "Dedicated Hosts" [ + test "Can create a basic dedicated host group" { + let deployment = arm { + location Location.EastUS - let hostGroupProps = hostGroup["properties"] - - let supportAutomaticPlacement: bool = - JToken.op_Explicit hostGroupProps["supportAutomaticPlacement"] - - Expect.equal supportAutomaticPlacement false "Incorrect default value for supportAutomaticPlacement" + add_resources [ hostGroup { name "myhostgroup" } ] } - test "Can create a basic dedicated host group with a host" { - let parentHostGroupName = "myhostGroup" - - let deployment = - arm { - location Location.EastUS - - add_resources - [ - hostGroup { name parentHostGroupName } - host { - name "myhost" - parent_host_group (ResourceName parentHostGroupName) - sku "Fsv2-Type2" - } - ] - } - let jobj = deployment.Template |> Writer.toJson |> JObject.Parse + let jobj = deployment.Template |> Writer.toJson |> JObject.Parse - let host = - jobj.SelectToken "resources[?(@.type=='Microsoft.Compute/hostGroups/hosts')]" + let hostGroup = + jobj.SelectToken "resources[?(@.type=='Microsoft.Compute/hostGroups')]" - let hostProps = host["properties"] - let dependsOn = host["dependsOn"] :?> JArray |> Seq.map string - let licenseType: string = JToken.op_Explicit hostProps["licenseType"] - let platformFaultDomain: int = JToken.op_Explicit hostProps["platformFaultDomain"] + let hostGroupProps = hostGroup["properties"] - Expect.equal - licenseType - (HostLicenseType.Print HostLicenseType.NoLicense) - "Default license type should be no license" + let supportAutomaticPlacement: bool = + JToken.op_Explicit hostGroupProps["supportAutomaticPlacement"] - Expect.equal - platformFaultDomain - (PlatformFaultDomainCount.ToArmValue(PlatformFaultDomainCount 1)) - "Default fault domain should be 1" + Expect.equal supportAutomaticPlacement false "Incorrect default value for supportAutomaticPlacement" + } + test "Can create a basic dedicated host group with a host" { + let parentHostGroupName = "myhostGroup" - Expect.hasLength dependsOn 1 "Should only depend on one resource, the host group" + let deployment = arm { + location Location.EastUS - Expect.contains - dependsOn - """[resourceId('Microsoft.Compute/hostGroups', 'myhostGroup')]""" - "Parent host group is incorrect" - - () + add_resources [ + hostGroup { name parentHostGroupName } + host { + name "myhost" + parent_host_group (ResourceName parentHostGroupName) + sku "Fsv2-Type2" + } + ] } - test "Can create a host group with an and a valid domain count" { - let deployment = - arm { - location Location.EastUS - - add_resources - [ - hostGroup { - name "myhostgroup" - support_automatic_placement true - add_availability_zone "1" - platform_fault_domain_count 2 - } - host { - name "myhost" - parent_host_group (ResourceName "myHostGroup") - sku "Fsv2-Type2" - } - ] + + let jobj = deployment.Template |> Writer.toJson |> JObject.Parse + + let host = + jobj.SelectToken "resources[?(@.type=='Microsoft.Compute/hostGroups/hosts')]" + + let hostProps = host["properties"] + let dependsOn = host["dependsOn"] :?> JArray |> Seq.map string + let licenseType: string = JToken.op_Explicit hostProps["licenseType"] + let platformFaultDomain: int = JToken.op_Explicit hostProps["platformFaultDomain"] + + Expect.equal + licenseType + (HostLicenseType.Print HostLicenseType.NoLicense) + "Default license type should be no license" + + Expect.equal + platformFaultDomain + (PlatformFaultDomainCount.ToArmValue(PlatformFaultDomainCount 1)) + "Default fault domain should be 1" + + Expect.hasLength dependsOn 1 "Should only depend on one resource, the host group" + + Expect.contains + dependsOn + """[resourceId('Microsoft.Compute/hostGroups', 'myhostGroup')]""" + "Parent host group is incorrect" + + () + } + test "Can create a host group with an and a valid domain count" { + let deployment = arm { + location Location.EastUS + + add_resources [ + hostGroup { + name "myhostgroup" + support_automatic_placement true + add_availability_zone "1" + platform_fault_domain_count 2 } + host { + name "myhost" + parent_host_group (ResourceName "myHostGroup") + sku "Fsv2-Type2" + } + ] + } - let jobj = deployment.Template |> Writer.toJson |> JObject.Parse - let a: string = deployment.ToString() + let jobj = deployment.Template |> Writer.toJson |> JObject.Parse + let a: string = deployment.ToString() - let hostGroup = - jobj.SelectToken "resources[?(@.type=='Microsoft.Compute/hostGroups')]" + let hostGroup = + jobj.SelectToken "resources[?(@.type=='Microsoft.Compute/hostGroups')]" - let hostGroupProps = hostGroup["properties"] - let zones = hostGroup["zones"] :?> JArray |> Seq.map string + let hostGroupProps = hostGroup["properties"] + let zones = hostGroup["zones"] :?> JArray |> Seq.map string - let platformFaultDomainCount: int = - JToken.op_Explicit hostGroupProps["platformFaultDomainCount"] + let platformFaultDomainCount: int = + JToken.op_Explicit hostGroupProps["platformFaultDomainCount"] - let supportAutomaticPlacement: bool = - JToken.op_Explicit hostGroupProps["supportAutomaticPlacement"] + let supportAutomaticPlacement: bool = + JToken.op_Explicit hostGroupProps["supportAutomaticPlacement"] - Expect.equal supportAutomaticPlacement true "Automatic placement should be true" + Expect.equal supportAutomaticPlacement true "Automatic placement should be true" - Expect.equal - platformFaultDomainCount - 2 - $"Platform fault domain count should equal 2, it is {platformFaultDomainCount}" + Expect.equal + platformFaultDomainCount + 2 + $"Platform fault domain count should equal 2, it is {platformFaultDomainCount}" - Expect.hasLength zones 1 "The host group should have one availability zone" + Expect.hasLength zones 1 "The host group should have one availability zone" - Expect.contains zones "1" "The zones should contain zone 1" - () - } - ] + Expect.contains zones "1" "The zones should contain zone 1" + () + } + ] diff --git a/src/Tests/DeploymentScript.fs b/src/Tests/DeploymentScript.fs index b414e6cb3..ed0867322 100644 --- a/src/Tests/DeploymentScript.fs +++ b/src/Tests/DeploymentScript.fs @@ -7,133 +7,121 @@ open Farmer.Arm.Storage open Farmer.Builders let tests = - testList - "deploymentScripts" - [ - test "creates a script" { - let script = - deploymentScript { - name "some-script" - arguments [ "foo"; "bar" ] - env_vars [ "FOO", "bar" ] - script_content """ echo 'hello' """ - } - - Expect.equal script.Name.Value "some-script" "Deployment script resource name incorrect" - Expect.equal script.ScriptSource (Content " echo 'hello' ") "Script content not set" - Expect.equal script.Cli (AzCli "2.9.1") "Script default CLI was not az cli 2.9.1" - Expect.equal script.Timeout None "Script timeout should not have a value" - Expect.equal script.CleanupPreference Cleanup.Always "Script should default to cleanup Always" - Expect.hasLength script.Arguments 2 "Incorrect number of script arguments" - Expect.hasLength script.EnvironmentVariables 1 "Incorrect number of environment variables" + testList "deploymentScripts" [ + test "creates a script" { + let script = deploymentScript { + name "some-script" + arguments [ "foo"; "bar" ] + env_vars [ "FOO", "bar" ] + script_content """ echo 'hello' """ } - test "creates a script that is cleaned up only on success" { - let script = - deploymentScript { - name "some-script" - script_content """ echo 'hello' """ - cleanup_on_success - } - - Expect.equal script.CleanupPreference Cleanup.OnSuccess "Cleanup preference was incorrect" + Expect.equal script.Name.Value "some-script" "Deployment script resource name incorrect" + Expect.equal script.ScriptSource (Content " echo 'hello' ") "Script content not set" + Expect.equal script.Cli (AzCli "2.9.1") "Script default CLI was not az cli 2.9.1" + Expect.equal script.Timeout None "Script timeout should not have a value" + Expect.equal script.CleanupPreference Cleanup.Always "Script should default to cleanup Always" + Expect.hasLength script.Arguments 2 "Incorrect number of script arguments" + Expect.hasLength script.EnvironmentVariables 1 "Incorrect number of environment variables" + } + + test "creates a script that is cleaned up only on success" { + let script = deploymentScript { + name "some-script" + script_content """ echo 'hello' """ + cleanup_on_success } - test "creates a script that is cleaned up after the retention interval" { - let script = - deploymentScript { - name "some-script" - script_content """ echo 'hello' """ - retention_interval 4 - } - - Expect.equal - script.CleanupPreference - (Cleanup.OnExpiration(System.TimeSpan.FromHours 4.)) - "Cleanup preference should be on expiration of 4 hours" + Expect.equal script.CleanupPreference Cleanup.OnSuccess "Cleanup preference was incorrect" + } + + test "creates a script that is cleaned up after the retention interval" { + let script = deploymentScript { + name "some-script" + script_content """ echo 'hello' """ + retention_interval 4 } - test "creates a script with explicit identity" { - let scriptIdentity = userAssignedIdentity { name "my-aks-user" } + Expect.equal + script.CleanupPreference + (Cleanup.OnExpiration(System.TimeSpan.FromHours 4.)) + "Cleanup preference should be on expiration of 4 hours" + } + + test "creates a script with explicit identity" { + let scriptIdentity = userAssignedIdentity { name "my-aks-user" } - let deployToAks = - deploymentScript { - name "some-kubectl-stuff" - identity scriptIdentity + let deployToAks = deploymentScript { + name "some-kubectl-stuff" + identity scriptIdentity - script_content - """ set -e; + script_content + """ set -e; az aks install-cli; az aks get-credentials -n my-cluster; kubectl apply -f https://some/awesome/deployment.yml; """ - } + } - Expect.equal deployToAks.Name.Value "some-kubectl-stuff" "Deployment script resource name incorrect" + Expect.equal deployToAks.Name.Value "some-kubectl-stuff" "Deployment script resource name incorrect" - let scriptIdentityValue = - Expect.wantSome deployToAks.CustomIdentity "Script identity not set" + let scriptIdentityValue = + Expect.wantSome deployToAks.CustomIdentity "Script identity not set" - Expect.equal - scriptIdentityValue - scriptIdentity.UserAssignedIdentity - "Script did not have identity assigned" - } + Expect.equal scriptIdentityValue scriptIdentity.UserAssignedIdentity "Script did not have identity assigned" + } - test "Outputs are generated correctly" { - let s = deploymentScript { name "thing" } + test "Outputs are generated correctly" { + let s = deploymentScript { name "thing" } - Expect.equal - (s.Outputs.["test"].Eval()) - "[reference(resourceId('Microsoft.Resources/deploymentScripts', 'thing'), '2019-10-01-preview').outputs.test]" - "" - } + Expect.equal + (s.Outputs.["test"].Eval()) + "[reference(resourceId('Microsoft.Resources/deploymentScripts', 'thing'), '2019-10-01-preview').outputs.test]" + "" + } - test "Secure parameters are generated correctly" { - let s = - deploymentScript { - name "thing" - env_vars [ EnvVar.createSecure "foo" "secret-foo" ] - } - - let deployment = arm { add_resource s } - Expect.hasLength deployment.Template.Parameters 1 "Should have a secure parameter" + test "Secure parameters are generated correctly" { + let s = deploymentScript { + name "thing" + env_vars [ EnvVar.createSecure "foo" "secret-foo" ] + } - Expect.equal - (deployment.Template.Parameters.Head.ArmExpression.Eval()) - "[parameters('secret-foo')]" - "Generated incorrect secure parameter." + let deployment = arm { add_resource s } + Expect.hasLength deployment.Template.Parameters 1 "Should have a secure parameter" + + Expect.equal + (deployment.Template.Parameters.Head.ArmExpression.Eval()) + "[parameters('secret-foo')]" + "Generated incorrect secure parameter." + } + test "Script runs after dependency is created" { + let storage = storageAccount { + name "storagewithstuff" + add_public_container "public" } - test "Script runs after dependency is created" { - let storage = - storageAccount { - name "storagewithstuff" - add_public_container "public" - } - let script = - deploymentScript { - name "write-files" + let script = deploymentScript { + name "write-files" - script_content - "echo 'hello world' > hello && az storage blob upload --account-name storagewithstuff -f hello -c public -n hello" + script_content + "echo 'hello world' > hello && az storage blob upload --account-name storagewithstuff -f hello -c public -n hello" - depends_on storage - } + depends_on storage + } - Expect.hasLength script.Dependencies 1 "Should have additional dependency" + Expect.hasLength script.Dependencies 1 "Should have additional dependency" - Expect.equal - (Set.toList script.Dependencies).[0].Name.Value - "storagewithstuff" - "Dependency should be on storage account" - } + Expect.equal + (Set.toList script.Dependencies).[0].Name.Value + "storagewithstuff" + "Dependency should be on storage account" + } - test "Retention period cannot be more than 26 hours" { - let createScript hours () = - deploymentScript { retention_interval (hours * 1) } |> ignore + test "Retention period cannot be more than 26 hours" { + let createScript hours () = + deploymentScript { retention_interval (hours * 1) } |> ignore - Expect.equal (createScript 26 ()) () "Should have not thrown" - Expect.throws (createScript 27) "" - } - ] + Expect.equal (createScript 26 ()) () "Should have not thrown" + Expect.throws (createScript 27) "" + } + ] diff --git a/src/Tests/DiagnosticSettings.fs b/src/Tests/DiagnosticSettings.fs index 9b0c2069f..ed1321eda 100644 --- a/src/Tests/DiagnosticSettings.fs +++ b/src/Tests/DiagnosticSettings.fs @@ -24,160 +24,146 @@ let asAzureResource (ws: DiagnosticSettingsConfig) = |> List.head let tests = - testList - "Diagnostic Settings" - [ - test "Creates diagnostic settings with raw sinks" { - let storageAccount = - ResourceId.create (storageAccounts, ResourceName "storagename", "storage-rg") + testList "Diagnostic Settings" [ + test "Creates diagnostic settings with raw sinks" { + let storageAccount = + ResourceId.create (storageAccounts, ResourceName "storagename", "storage-rg") + + let workspace = workspaces.resourceId "workspacename" + + let eventHub = + Namespaces.authorizationRules.resourceId ( + ResourceName "eventhubns", + ResourceName "RootManageSharedAccessKey" + ) + + let config = diagnosticSettings { + name "myDiagnosticSetting" + metrics_source logicAppResource + add_destination storageAccount + add_destination workspace + add_destination eventHub + capture_metrics [ MetricSetting.Create("AllMetrics", 2, TimeSpan.FromMinutes 1.) ] + capture_logs [ LogSetting.Create("WorkflowRuntime", 1) ] + } - let workspace = workspaces.resourceId "workspacename" + let result = asAzureResource config - let eventHub = - Namespaces.authorizationRules.resourceId ( - ResourceName "eventhubns", - ResourceName "RootManageSharedAccessKey" - ) + Expect.equal + result.Name + "LogicApp/Microsoft.Insights/myDiagnosticSetting" + ("Incorrect Name : " + result.Name) + + Expect.equal result.StorageAccountId (storageAccount.Eval()) "Incorrect StorageAccount ResourceId" + Expect.equal result.WorkspaceId (workspace.Eval()) "Incorrect Workspace ResourceId" + Expect.equal result.EventHubAuthorizationRuleId (eventHub.Eval()) "Incorrect Event Hub Auth Rule" + Expect.equal result.Metrics.[0].Category "AllMetrics" "Incorrect MetricCategory" + Expect.equal result.Metrics.[0].RetentionPolicy.Days 2 "Incorrect MetricretentionPeriod" - let config = + Expect.equal result.Metrics.[0].TimeGrain (Nullable(TimeSpan(0, 1, 0))) "Incorrect MetricRetentionPeriod" + + Expect.equal result.Logs.[0].Category "WorkflowRuntime" "Incorrect LogCategory" + Expect.equal result.Logs.[0].RetentionPolicy.Days 1 "Incorrect LogRetentionPeriod" + } + + test "Event hub name can't be specified without the Event hub authorization rule id" { + Expect.throws + (fun _ -> diagnosticSettings { - name "myDiagnosticSetting" metrics_source logicAppResource - add_destination storageAccount - add_destination workspace - add_destination eventHub - capture_metrics [ MetricSetting.Create("AllMetrics", 2, TimeSpan.FromMinutes 1.) ] - capture_logs [ LogSetting.Create("WorkflowRuntime", 1) ] + event_hub_destination_name "myeventhubname" } + |> ignore) + (sprintf "Should have thrown an exception for not specifying Event Hub authorization rule id") + } + test "Event hub name is set correctly" { + let settings = diagnosticSettings { + metrics_source logicAppResource + + add_destination ( + Namespaces.authorizationRules.resourceId ( + ResourceName "eventhubns", + ResourceName "RootManageSharedAccessKey" + ) + ) - let result = asAzureResource config - - Expect.equal - result.Name - "LogicApp/Microsoft.Insights/myDiagnosticSetting" - ("Incorrect Name : " + result.Name) - - Expect.equal result.StorageAccountId (storageAccount.Eval()) "Incorrect StorageAccount ResourceId" - Expect.equal result.WorkspaceId (workspace.Eval()) "Incorrect Workspace ResourceId" - Expect.equal result.EventHubAuthorizationRuleId (eventHub.Eval()) "Incorrect Event Hub Auth Rule" - Expect.equal result.Metrics.[0].Category "AllMetrics" "Incorrect MetricCategory" - Expect.equal result.Metrics.[0].RetentionPolicy.Days 2 "Incorrect MetricretentionPeriod" + event_hub_destination_name "myeventhubname" + capture_logs [ LogSetting.Create "WorkflowRuntime" ] + } - Expect.equal - result.Metrics.[0].TimeGrain - (Nullable(TimeSpan(0, 1, 0))) - "Incorrect MetricRetentionPeriod" + let result = asAzureResource settings + Expect.equal result.EventHubName "myeventhubname" "Incorrect event hub name" + } + test "Works with Farmer resources" { + let storageAccount = storageAccount { name "foo" } + let workspace = logAnalytics { name "logs" } - Expect.equal result.Logs.[0].Category "WorkflowRuntime" "Incorrect LogCategory" - Expect.equal result.Logs.[0].RetentionPolicy.Days 1 "Incorrect LogRetentionPeriod" + let eventHub = eventHub { + name "hub" + namespace_name "ns" } - test "Event hub name can't be specified without the Event hub authorization rule id" { - Expect.throws - (fun _ -> - diagnosticSettings { - metrics_source logicAppResource - event_hub_destination_name "myeventhubname" - } - |> ignore) - (sprintf "Should have thrown an exception for not specifying Event Hub authorization rule id") + let config = diagnosticSettings { + add_destination storageAccount + add_destination workspace + add_destination eventHub + capture_logs [ LogSetting.Create "WorkflowRuntime" ] } - test "Event hub name is set correctly" { - let settings = - diagnosticSettings { - metrics_source logicAppResource - add_destination ( - Namespaces.authorizationRules.resourceId ( - ResourceName "eventhubns", - ResourceName "RootManageSharedAccessKey" - ) - ) + let result = asAzureResource config - event_hub_destination_name "myeventhubname" - capture_logs [ LogSetting.Create "WorkflowRuntime" ] - } + Expect.equal + result.StorageAccountId + (storageAccount.ResourceId.Eval()) + "Incorrect StorageAccount ResourceId" - let result = asAzureResource settings - Expect.equal result.EventHubName "myeventhubname" "Incorrect event hub name" - } - test "Works with Farmer resources" { - let storageAccount = storageAccount { name "foo" } - let workspace = logAnalytics { name "logs" } + Expect.equal result.WorkspaceId ((workspace :> IBuilder).ResourceId.Eval()) "Incorrect Workspace ResourceId" - let eventHub = - eventHub { - name "hub" - namespace_name "ns" - } + Expect.equal + result.EventHubAuthorizationRuleId + (eventHub.DefaultAuthorizationRule.Eval()) + "Incorrect Event Hub Auth Rule" + + Expect.equal result.EventHubName eventHub.Name.Value "Incorrect Event Hub Name" + } - let config = + test "Can't create Diagnostic Settings without at least one data sink" { + Expect.throws + (fun _ -> diagnosticSettings { - add_destination storageAccount - add_destination workspace - add_destination eventHub + name "myDiagnosticSetting" + metrics_source logicAppResource capture_logs [ LogSetting.Create "WorkflowRuntime" ] } + |> ignore) + "Should have thrown an exception for not specifying at least on data sink" + } - let result = asAzureResource config - - Expect.equal - result.StorageAccountId - (storageAccount.ResourceId.Eval()) - "Incorrect StorageAccount ResourceId" - - Expect.equal - result.WorkspaceId - ((workspace :> IBuilder).ResourceId.Eval()) - "Incorrect Workspace ResourceId" - - Expect.equal - result.EventHubAuthorizationRuleId - (eventHub.DefaultAuthorizationRule.Eval()) - "Incorrect Event Hub Auth Rule" - - Expect.equal result.EventHubName eventHub.Name.Value "Incorrect Event Hub Name" - } - - test "Can't create Diagnostic Settings without at least one data sink" { + test "Can't create test with retention period outside 1 and 365" { + for days in [ 0; 366 ] do Expect.throws - (fun _ -> - diagnosticSettings { - name "myDiagnosticSetting" - metrics_source logicAppResource - capture_logs [ LogSetting.Create "WorkflowRuntime" ] - } - |> ignore) - "Should have thrown an exception for not specifying at least on data sink" - } + (fun _ -> LogSetting.Create("", days) |> ignore) + (sprintf "Should have thrown for %d" days) - test "Can't create test with retention period outside 1 and 365" { - for days in [ 0; 366 ] do - Expect.throws - (fun _ -> LogSetting.Create("", days) |> ignore) - (sprintf "Should have thrown for %d" days) - - Expect.throws - (fun _ -> MetricSetting.Create("", days) |> ignore) - (sprintf "Should have thrown for %d" days) - } + Expect.throws + (fun _ -> MetricSetting.Create("", days) |> ignore) + (sprintf "Should have thrown for %d" days) + } - test "Supports segmented names such as SQL databases" { - let config = - let storageAccount = storageAccount { name "foo" } + test "Supports segmented names such as SQL databases" { + let config = + let storageAccount = storageAccount { name "foo" } - diagnosticSettings { - name "myDiagnosticSetting" - add_destination storageAccount + diagnosticSettings { + name "myDiagnosticSetting" + add_destination storageAccount - metrics_source ( - Arm.Sql.databases.resourceId (ResourceName "sqlserver", ResourceName "sqldatabase") - ) + metrics_source (Arm.Sql.databases.resourceId (ResourceName "sqlserver", ResourceName "sqldatabase")) - capture_logs [ Logging.Sql.Servers.Databases.AutomaticTuning ] - } + capture_logs [ Logging.Sql.Servers.Databases.AutomaticTuning ] + } - let result = asAzureResource config - Expect.equal result.Name "sqlserver/sqldatabase/Microsoft.Insights/myDiagnosticSetting" "Incorrect Name" - } - ] + let result = asAzureResource config + Expect.equal result.Name "sqlserver/sqldatabase/Microsoft.Insights/myDiagnosticSetting" "Incorrect Name" + } + ] diff --git a/src/Tests/Disk.fs b/src/Tests/Disk.fs index df1f24ce8..3d636b49a 100644 --- a/src/Tests/Disk.fs +++ b/src/Tests/Disk.fs @@ -6,84 +6,78 @@ open Farmer.Builders open Newtonsoft.Json.Linq let tests = - testList - "Disk Tests" - [ - test "Import disk builder from VHD" { - let deployment = - arm { - add_resources - [ - disk { - name "imported-disk-image" - sku Vm.DiskType.Premium_LRS - os_type Linux + testList "Disk Tests" [ + test "Import disk builder from VHD" { + let deployment = arm { + add_resources [ + disk { + name "imported-disk-image" + sku Vm.DiskType.Premium_LRS + os_type Linux - import - (System.Uri - "https://rxw1n3qxt54dnvfen1gnza5n.blob.core.windows.net/vhds/Ubuntu2004WithJava_20230213141703.vhd") - (ResourceId.create ( - Arm.Storage.storageAccounts, - ResourceName "rxw1n3qxt54dnvfen1gnza5n", - "IT_farmer-imgbldr_Ubuntu2004WithJava_aea5facc-e1b5-47de-aa5b-2c6aafe2161d" - )) - } - ] + import + (System.Uri + "https://rxw1n3qxt54dnvfen1gnza5n.blob.core.windows.net/vhds/Ubuntu2004WithJava_20230213141703.vhd") + (ResourceId.create ( + Arm.Storage.storageAccounts, + ResourceName "rxw1n3qxt54dnvfen1gnza5n", + "IT_farmer-imgbldr_Ubuntu2004WithJava_aea5facc-e1b5-47de-aa5b-2c6aafe2161d" + )) } + ] + } - let jobj = deployment.Template |> Writer.toJson |> JObject.Parse + let jobj = deployment.Template |> Writer.toJson |> JObject.Parse - let diskProps = - jobj.SelectToken("resources[?(@.name=='imported-disk-image')].properties") + let diskProps = + jobj.SelectToken("resources[?(@.name=='imported-disk-image')].properties") - Expect.isNotNull diskProps "Unable to get disk properties" - let os = diskProps.SelectToken "osType" - Expect.equal os (JValue "Linux") "osType incorrect" - let createOption = diskProps.SelectToken "creationData.createOption" - Expect.equal createOption (JValue "Import") "createOption incorrect" - let sourceUri = diskProps.SelectToken "creationData.sourceUri" + Expect.isNotNull diskProps "Unable to get disk properties" + let os = diskProps.SelectToken "osType" + Expect.equal os (JValue "Linux") "osType incorrect" + let createOption = diskProps.SelectToken "creationData.createOption" + Expect.equal createOption (JValue "Import") "createOption incorrect" + let sourceUri = diskProps.SelectToken "creationData.sourceUri" - Expect.equal - sourceUri - (JValue - "https://rxw1n3qxt54dnvfen1gnza5n.blob.core.windows.net/vhds/Ubuntu2004WithJava_20230213141703.vhd") - "sourceUri incorrect" + Expect.equal + sourceUri + (JValue + "https://rxw1n3qxt54dnvfen1gnza5n.blob.core.windows.net/vhds/Ubuntu2004WithJava_20230213141703.vhd") + "sourceUri incorrect" - let storageAccountId = diskProps.SelectToken "creationData.storageAccountId" + let storageAccountId = diskProps.SelectToken "creationData.storageAccountId" - Expect.equal - storageAccountId - (JValue - "[resourceId('IT_farmer-imgbldr_Ubuntu2004WithJava_aea5facc-e1b5-47de-aa5b-2c6aafe2161d', 'Microsoft.Storage/storageAccounts', 'rxw1n3qxt54dnvfen1gnza5n')]") - "storageAccountId incorrect" + Expect.equal + storageAccountId + (JValue + "[resourceId('IT_farmer-imgbldr_Ubuntu2004WithJava_aea5facc-e1b5-47de-aa5b-2c6aafe2161d', 'Microsoft.Storage/storageAccounts', 'rxw1n3qxt54dnvfen1gnza5n')]") + "storageAccountId incorrect" - let diskSku = - jobj.SelectToken("resources[?(@.name=='imported-disk-image')].sku.name") + let diskSku = + jobj.SelectToken("resources[?(@.name=='imported-disk-image')].sku.name") - Expect.equal diskSku (JValue "Premium_LRS") "disk sku incorrect" - } + Expect.equal diskSku (JValue "Premium_LRS") "disk sku incorrect" + } - test "Simple empty disk" { - let deployment = - arm { - add_resources - [ - disk { - name "empty-disk" - os_type Linux - create_empty 128 - } - ] + test "Simple empty disk" { + let deployment = arm { + add_resources [ + disk { + name "empty-disk" + os_type Linux + create_empty 128 } - - let jobj = deployment.Template |> Writer.toJson |> JObject.Parse - let diskProps = jobj.SelectToken("resources[?(@.name=='empty-disk')].properties") - Expect.isNotNull diskProps "Unable to get disk properties" - let diskSizeGB = diskProps.SelectToken "diskSizeGB" - Expect.equal diskSizeGB (JValue 128) "diskSizeGB incorrect" - let os = diskProps.SelectToken "osType" - Expect.equal os (JValue "Linux") "osType incorrect" - let createOption = diskProps.SelectToken "creationData.createOption" - Expect.equal createOption (JValue "Empty") "createOption incorrect" + ] } - ] + + let jobj = deployment.Template |> Writer.toJson |> JObject.Parse + let diskProps = jobj.SelectToken("resources[?(@.name=='empty-disk')].properties") + Expect.isNotNull diskProps "Unable to get disk properties" + let diskSizeGB = diskProps.SelectToken "diskSizeGB" + Expect.equal diskSizeGB (JValue 128) "diskSizeGB incorrect" + let os = diskProps.SelectToken "osType" + Expect.equal os (JValue "Linux") "osType incorrect" + let createOption = diskProps.SelectToken "creationData.createOption" + Expect.equal createOption (JValue "Empty") "createOption incorrect" + } + ] diff --git a/src/Tests/Dns.fs b/src/Tests/Dns.fs index 00adb97fa..4ee4b9ff0 100644 --- a/src/Tests/Dns.fs +++ b/src/Tests/Dns.fs @@ -14,961 +14,916 @@ let client = new DnsManagementClient(Uri "http://management.azure.com", TokenCredentials "NotNullOrWhiteSpace") let tests = - testList - "DNS Zone" - [ - test "Public DNS Zone is created with a CNAME record" { + testList "DNS Zone" [ + test "Public DNS Zone is created with a CNAME record" { - let resources = - arm { - add_resources - [ - dnsZone { - name "farmer.com" - - add_records - [ - cnameRecord { - name "www" - ttl 3600 - cname "farmer.com" - } - aRecord { - name "aName" - ttl 7200 - add_ipv4_addresses [ "192.168.0.1" ] - } - soaRecord { - name "soaName" - host "ns1-09.azure-dns.com." - ttl 3600 - email "azuredns-hostmaster.microsoft.com" - serial_number 1L - minimum_ttl 300L - refresh_time 3600L - retry_time 300L - expire_time 2419200L - } - srvRecord { - name "_sip._tcp.name" - ttl 3600 - - add_values - [ - { - Priority = Some 100 - Weight = Some 1 - Port = Some 5061 - Target = Some "farmer.online.com." - } - ] - } - txtRecord { - name "txtName" - ttl 3600 - add_values [ "somevalue" ] - } - ] - } - ] - } - - let dnsZones = - resources - |> findAzureResources client.SerializationSettings - |> Array.ofList - - Expect.equal dnsZones.[0].Name "farmer.com" "DNS Zone name is wrong" - Expect.equal dnsZones.[0].Type "Microsoft.Network/dnsZones" "DNS Zone type is wrong" - Expect.equal dnsZones.[0].ZoneType (Nullable ZoneType.Public) "DNS Zone ZoneType is wrong" - - let dnsRecords = - resources - |> findAzureResources client.SerializationSettings - |> Array.ofList - - Expect.equal dnsRecords.[1].Name "farmer.com/www" "DNS CNAME record name is wrong" - Expect.equal dnsRecords.[1].Type "Microsoft.Network/dnsZones/CNAME" "DNS record type is wrong" - Expect.equal dnsRecords.[1].CnameRecord.Cname "farmer.com" "DNS CNAME record is wrong" - Expect.equal dnsRecords.[1].TTL (Nullable 3600L) "DNS record TTL is wrong" - - Expect.equal dnsRecords.[2].Name "farmer.com/aName" "DNS A record name is wrong" - Expect.equal dnsRecords.[2].Type "Microsoft.Network/dnsZones/A" "DNS record type is wrong" - - Expect.sequenceEqual - (dnsRecords.[2].ARecords |> Seq.map (fun x -> x.Ipv4Address)) - [ "192.168.0.1" ] - "DNS A record IP address is wrong" - - Expect.equal dnsRecords.[2].TTL (Nullable(7200L)) "DNS record TTL is wrong" - - Expect.equal dnsRecords.[3].Name "farmer.com/soaName" "DNS SOA record name is wrong" - Expect.equal dnsRecords.[3].Type "Microsoft.Network/dnsZones/SOA" "DNS record type is wrong" - Expect.equal dnsRecords.[3].SoaRecord.Host "ns1-09.azure-dns.com." "DNS SOA record host wrong" - - Expect.equal - dnsRecords.[3].SoaRecord.Email - "azuredns-hostmaster.microsoft.com" - "DNS SOA record email wrong" - - Expect.equal dnsRecords.[3].SoaRecord.SerialNumber (Nullable 1L) "DNS SOA record serial number wrong" - Expect.equal dnsRecords.[3].SoaRecord.MinimumTtl (Nullable 300L) "DNS SOA record minimum ttl wrong" - Expect.equal dnsRecords.[3].SoaRecord.RefreshTime (Nullable 3600L) "DNS SOA record refresh time wrong" - Expect.equal dnsRecords.[3].SoaRecord.RetryTime (Nullable 300L) "DNS SOA record retry time wrong" - Expect.equal dnsRecords.[3].SoaRecord.ExpireTime (Nullable 2419200L) "DNS SOA record expire time wrong" - Expect.equal dnsRecords.[3].TTL (Nullable 3600L) "DNS record TTL is wrong" - - Expect.equal dnsRecords.[4].Name "farmer.com/_sip._tcp.name" "DNS SRV record name is wrong" - Expect.equal dnsRecords.[4].Type "Microsoft.Network/dnsZones/SRV" "DNS record type is wrong" - Expect.equal dnsRecords.[4].SrvRecords.[0].Priority (Nullable 100) "DNS SRV record priority wrong" - Expect.equal dnsRecords.[4].SrvRecords.[0].Weight (Nullable 1) "DNS SRV record weight wrong" - Expect.equal dnsRecords.[4].SrvRecords.[0].Port (Nullable 5061) "DNS SRV record port wrong" - Expect.equal dnsRecords.[4].SrvRecords.[0].Target "farmer.online.com." "DNS SRV record target wrong" - Expect.equal dnsRecords.[4].TTL (Nullable 3600L) "DNS record TTL is wrong" - - Expect.equal dnsRecords.[5].Name "farmer.com/txtName" "DNS TXT record name is wrong" - Expect.equal dnsRecords.[5].Type "Microsoft.Network/dnsZones/TXT" "DNS record type is wrong" - Expect.sequenceEqual dnsRecords.[5].TxtRecords.[0].Value.[0] "somevalue" "DNS TXT record value is wrong" - Expect.equal dnsRecords.[5].TTL (Nullable 3600L) "DNS record TTL is wrong" - } - test "Adding A record to existing zone" { - let template = - arm { - add_resources - [ - aRecord { - name "arm" - ttl 3600 - add_ipv4_addresses [ "10.100.200.28" ] - link_to_unmanaged_dns_zone (Farmer.Arm.Dns.zones.resourceId "farmer.com") - } - ] - } - - let jobj = template.Template |> Writer.toJson |> JObject.Parse - - let dependsOn = - jobj.SelectToken("resources[?(@.name=='farmer.com/arm')].dependsOn") :?> JArray - - Expect.isEmpty dependsOn "DNS 'A' record linked to existing zone dependsOn." - - let expectedARecordType = - { - ResourceId.Type = ResourceType("Microsoft.Network/dnsZones/A", "2018-05-01") - ResourceGroup = None - Subscription = None - Name = ResourceName "farmer.com" - Segments = [ ResourceName "arm" ] - } - - Expect.equal - template.Resources.[0].ResourceId - expectedARecordType - "Incorrect resourceId generated from standalone record builder" - } - test "DNS zone depends_on emits 'dependsOn'" { - let zone = + let resources = arm { + add_resources [ dnsZone { name "farmer.com" - depends_on (Farmer.Arm.TrafficManager.profiles.resourceId "foo") - } - - let template = arm { add_resources [ zone ] } - let jobj = template.Template |> Writer.toJson |> JObject.Parse - let zoneDependsOn = jobj.SelectToken("resources[?(@.name=='farmer.com')].dependsOn") - Expect.isNotNull zoneDependsOn "Zone missing dependsOn" - let zoneDependsOn = zoneDependsOn :?> JArray |> Seq.map string - Expect.hasLength zoneDependsOn 1 "Zone should have one dependency" - - Expect.contains - zoneDependsOn - "[resourceId('Microsoft.Network/trafficManagerProfiles', 'foo')]" - "Missing expected resource dependency" - } - test "Sequencing DNS record deployment through depends_on" { - let zone = dnsZone { name "farmer.com" } - let first = - cnameRecord { - name "first" - link_to_dns_zone zone - cname "farmer.com" - ttl 3600 + add_records [ + cnameRecord { + name "www" + ttl 3600 + cname "farmer.com" + } + aRecord { + name "aName" + ttl 7200 + add_ipv4_addresses [ "192.168.0.1" ] + } + soaRecord { + name "soaName" + host "ns1-09.azure-dns.com." + ttl 3600 + email "azuredns-hostmaster.microsoft.com" + serial_number 1L + minimum_ttl 300L + refresh_time 3600L + retry_time 300L + expire_time 2419200L + } + srvRecord { + name "_sip._tcp.name" + ttl 3600 + + add_values [ + { + Priority = Some 100 + Weight = Some 1 + Port = Some 5061 + Target = Some "farmer.online.com." + } + ] + } + txtRecord { + name "txtName" + ttl 3600 + add_values [ "somevalue" ] + } + ] } + ] + } - let second = - cnameRecord { - name "second" - link_to_dns_zone zone - cname "farmer.com" - depends_on first + let dnsZones = + resources + |> findAzureResources client.SerializationSettings + |> Array.ofList + + Expect.equal dnsZones.[0].Name "farmer.com" "DNS Zone name is wrong" + Expect.equal dnsZones.[0].Type "Microsoft.Network/dnsZones" "DNS Zone type is wrong" + Expect.equal dnsZones.[0].ZoneType (Nullable ZoneType.Public) "DNS Zone ZoneType is wrong" + + let dnsRecords = + resources + |> findAzureResources client.SerializationSettings + |> Array.ofList + + Expect.equal dnsRecords.[1].Name "farmer.com/www" "DNS CNAME record name is wrong" + Expect.equal dnsRecords.[1].Type "Microsoft.Network/dnsZones/CNAME" "DNS record type is wrong" + Expect.equal dnsRecords.[1].CnameRecord.Cname "farmer.com" "DNS CNAME record is wrong" + Expect.equal dnsRecords.[1].TTL (Nullable 3600L) "DNS record TTL is wrong" + + Expect.equal dnsRecords.[2].Name "farmer.com/aName" "DNS A record name is wrong" + Expect.equal dnsRecords.[2].Type "Microsoft.Network/dnsZones/A" "DNS record type is wrong" + + Expect.sequenceEqual + (dnsRecords.[2].ARecords |> Seq.map (fun x -> x.Ipv4Address)) + [ "192.168.0.1" ] + "DNS A record IP address is wrong" + + Expect.equal dnsRecords.[2].TTL (Nullable(7200L)) "DNS record TTL is wrong" + + Expect.equal dnsRecords.[3].Name "farmer.com/soaName" "DNS SOA record name is wrong" + Expect.equal dnsRecords.[3].Type "Microsoft.Network/dnsZones/SOA" "DNS record type is wrong" + Expect.equal dnsRecords.[3].SoaRecord.Host "ns1-09.azure-dns.com." "DNS SOA record host wrong" + + Expect.equal dnsRecords.[3].SoaRecord.Email "azuredns-hostmaster.microsoft.com" "DNS SOA record email wrong" + + Expect.equal dnsRecords.[3].SoaRecord.SerialNumber (Nullable 1L) "DNS SOA record serial number wrong" + Expect.equal dnsRecords.[3].SoaRecord.MinimumTtl (Nullable 300L) "DNS SOA record minimum ttl wrong" + Expect.equal dnsRecords.[3].SoaRecord.RefreshTime (Nullable 3600L) "DNS SOA record refresh time wrong" + Expect.equal dnsRecords.[3].SoaRecord.RetryTime (Nullable 300L) "DNS SOA record retry time wrong" + Expect.equal dnsRecords.[3].SoaRecord.ExpireTime (Nullable 2419200L) "DNS SOA record expire time wrong" + Expect.equal dnsRecords.[3].TTL (Nullable 3600L) "DNS record TTL is wrong" + + Expect.equal dnsRecords.[4].Name "farmer.com/_sip._tcp.name" "DNS SRV record name is wrong" + Expect.equal dnsRecords.[4].Type "Microsoft.Network/dnsZones/SRV" "DNS record type is wrong" + Expect.equal dnsRecords.[4].SrvRecords.[0].Priority (Nullable 100) "DNS SRV record priority wrong" + Expect.equal dnsRecords.[4].SrvRecords.[0].Weight (Nullable 1) "DNS SRV record weight wrong" + Expect.equal dnsRecords.[4].SrvRecords.[0].Port (Nullable 5061) "DNS SRV record port wrong" + Expect.equal dnsRecords.[4].SrvRecords.[0].Target "farmer.online.com." "DNS SRV record target wrong" + Expect.equal dnsRecords.[4].TTL (Nullable 3600L) "DNS record TTL is wrong" + + Expect.equal dnsRecords.[5].Name "farmer.com/txtName" "DNS TXT record name is wrong" + Expect.equal dnsRecords.[5].Type "Microsoft.Network/dnsZones/TXT" "DNS record type is wrong" + Expect.sequenceEqual dnsRecords.[5].TxtRecords.[0].Value.[0] "somevalue" "DNS TXT record value is wrong" + Expect.equal dnsRecords.[5].TTL (Nullable 3600L) "DNS record TTL is wrong" + } + test "Adding A record to existing zone" { + let template = arm { + add_resources [ + aRecord { + name "arm" ttl 3600 + add_ipv4_addresses [ "10.100.200.28" ] + link_to_unmanaged_dns_zone (Farmer.Arm.Dns.zones.resourceId "farmer.com") } + ] + } - let template = arm { add_resources [ zone; first; second ] } - let jobj = template.Template |> Writer.toJson |> JObject.Parse - - let firstDependsOn = - jobj.SelectToken("resources[?(@.name=='farmer.com/first')].dependsOn") :?> JArray - |> Seq.map string - - Expect.hasLength firstDependsOn 1 "first 'CNAME' record dependsOn zone only." + let jobj = template.Template |> Writer.toJson |> JObject.Parse - Expect.contains - firstDependsOn - "[resourceId('Microsoft.Network/dnsZones', 'farmer.com')]" - "Missing dependency on zone" + let dependsOn = + jobj.SelectToken("resources[?(@.name=='farmer.com/arm')].dependsOn") :?> JArray - let secondDependsOn = - jobj.SelectToken("resources[?(@.name=='farmer.com/second')].dependsOn") :?> JArray - |> Seq.map string + Expect.isEmpty dependsOn "DNS 'A' record linked to existing zone dependsOn." - Expect.hasLength secondDependsOn 2 "second 'CNAME' record linked to first 'CNAME' record dependsOn." + let expectedARecordType = { + ResourceId.Type = ResourceType("Microsoft.Network/dnsZones/A", "2018-05-01") + ResourceGroup = None + Subscription = None + Name = ResourceName "farmer.com" + Segments = [ ResourceName "arm" ] + } - Expect.contains - secondDependsOn - "[resourceId('Microsoft.Network/dnsZones', 'farmer.com')]" - "Missing dependency on zone" + Expect.equal + template.Resources.[0].ResourceId + expectedARecordType + "Incorrect resourceId generated from standalone record builder" + } + test "DNS zone depends_on emits 'dependsOn'" { + let zone = dnsZone { + name "farmer.com" + depends_on (Farmer.Arm.TrafficManager.profiles.resourceId "foo") + } - Expect.contains - secondDependsOn - "[resourceId('Microsoft.Network/dnsZones/CNAME', 'farmer.com', 'first')]" - "Missing dependency on first 'CNAME' record" + let template = arm { add_resources [ zone ] } + let jobj = template.Template |> Writer.toJson |> JObject.Parse + let zoneDependsOn = jobj.SelectToken("resources[?(@.name=='farmer.com')].dependsOn") + Expect.isNotNull zoneDependsOn "Zone missing dependsOn" + let zoneDependsOn = zoneDependsOn :?> JArray |> Seq.map string + Expect.hasLength zoneDependsOn 1 "Zone should have one dependency" + + Expect.contains + zoneDependsOn + "[resourceId('Microsoft.Network/trafficManagerProfiles', 'foo')]" + "Missing expected resource dependency" + } + test "Sequencing DNS record deployment through depends_on" { + let zone = dnsZone { name "farmer.com" } + + let first = cnameRecord { + name "first" + link_to_dns_zone zone + cname "farmer.com" + ttl 3600 } - test "Assigning target_resource on DNS record emits correct resource id" { - let zone = dnsZone { name "farmer.com" } - let tm = trafficManager { name "my-tm" } - let targetA = - aRecord { - name "tm-a" - link_to_dns_zone zone - ttl 60 - target_resource tm - } + let second = cnameRecord { + name "second" + link_to_dns_zone zone + cname "farmer.com" + depends_on first + ttl 3600 + } - let targetCname = - cnameRecord { - name "tm-cname" - link_to_dns_zone zone - target_resource tm - ttl 60 - depends_on targetA - } + let template = arm { add_resources [ zone; first; second ] } + let jobj = template.Template |> Writer.toJson |> JObject.Parse + + let firstDependsOn = + jobj.SelectToken("resources[?(@.name=='farmer.com/first')].dependsOn") :?> JArray + |> Seq.map string + + Expect.hasLength firstDependsOn 1 "first 'CNAME' record dependsOn zone only." + + Expect.contains + firstDependsOn + "[resourceId('Microsoft.Network/dnsZones', 'farmer.com')]" + "Missing dependency on zone" + + let secondDependsOn = + jobj.SelectToken("resources[?(@.name=='farmer.com/second')].dependsOn") :?> JArray + |> Seq.map string + + Expect.hasLength secondDependsOn 2 "second 'CNAME' record linked to first 'CNAME' record dependsOn." + + Expect.contains + secondDependsOn + "[resourceId('Microsoft.Network/dnsZones', 'farmer.com')]" + "Missing dependency on zone" + + Expect.contains + secondDependsOn + "[resourceId('Microsoft.Network/dnsZones/CNAME', 'farmer.com', 'first')]" + "Missing dependency on first 'CNAME' record" + } + test "Assigning target_resource on DNS record emits correct resource id" { + let zone = dnsZone { name "farmer.com" } + let tm = trafficManager { name "my-tm" } + + let targetA = aRecord { + name "tm-a" + link_to_dns_zone zone + ttl 60 + target_resource tm + } - let template = arm { add_resources [ zone; tm; targetA; targetCname ] } - let jobj = template.Template |> Writer.toJson |> JObject.Parse + let targetCname = cnameRecord { + name "tm-cname" + link_to_dns_zone zone + target_resource tm + ttl 60 + depends_on targetA + } - let tmAresourceId = - jobj.SelectToken("resources[?(@.name=='farmer.com/tm-a')].properties.targetResource.id") - |> string + let template = arm { add_resources [ zone; tm; targetA; targetCname ] } + let jobj = template.Template |> Writer.toJson |> JObject.Parse + + let tmAresourceId = + jobj.SelectToken("resources[?(@.name=='farmer.com/tm-a')].properties.targetResource.id") + |> string + + Expect.equal + tmAresourceId + "[resourceId('Microsoft.Network/trafficManagerProfiles', 'my-tm')]" + "Incorrect ID on target resource" + + let tmAresourceId = + jobj.SelectToken("resources[?(@.name=='farmer.com/tm-cname')].properties.targetResource.id") + |> string + + Expect.equal + tmAresourceId + "[resourceId('Microsoft.Network/trafficManagerProfiles', 'my-tm')]" + "Incorrect ID on target resource" + } + test "DNS zone get NameServers" { + let zone = dnsZone { name "farmer.com" } + + let template = arm { + add_resources [ zone ] + output "nameservers" zone.NameServers + } - Expect.equal - tmAresourceId - "[resourceId('Microsoft.Network/trafficManagerProfiles', 'my-tm')]" - "Incorrect ID on target resource" + let expected = + "[string(reference(resourceId('Microsoft.Network/dnsZones', 'farmer.com'), '2018-05-01').nameServers)]" - let tmAresourceId = - jobj.SelectToken("resources[?(@.name=='farmer.com/tm-cname')].properties.targetResource.id") - |> string + let jobj = template.Template |> Writer.toJson |> JObject.Parse + let nsArm = jobj.SelectToken("outputs.nameservers.value").ToString() + Expect.equal nsArm expected "Nameservers not gotten" + } + test "Delegate subdomain to another zone" { + let nsrecords = + "[reference(resourceId('Microsoft.Network/dnsZones/NS', 'subdomain.farmer.com', '@'), '2018-05-01').NSRecords]" - Expect.equal - tmAresourceId - "[resourceId('Microsoft.Network/trafficManagerProfiles', 'my-tm')]" - "Incorrect ID on target resource" - } - test "DNS zone get NameServers" { - let zone = dnsZone { name "farmer.com" } + let subdomainZone = dnsZone { + name "subdomain.farmer.com" + zone_type Dns.Public - let template = - arm { - add_resources [ zone ] - output "nameservers" zone.NameServers + add_records [ + aRecord { + name "aName" + ttl 7200 + add_ipv4_addresses [ "192.168.0.1"; "192.168.0.2" ] } - - let expected = - "[string(reference(resourceId('Microsoft.Network/dnsZones', 'farmer.com'), '2018-05-01').nameServers)]" - - let jobj = template.Template |> Writer.toJson |> JObject.Parse - let nsArm = jobj.SelectToken("outputs.nameservers.value").ToString() - Expect.equal nsArm expected "Nameservers not gotten" + ] } - test "Delegate subdomain to another zone" { - let nsrecords = - "[reference(resourceId('Microsoft.Network/dnsZones/NS', 'subdomain.farmer.com', '@'), '2018-05-01').NSRecords]" - let subdomainZone = - dnsZone { - name "subdomain.farmer.com" - zone_type Dns.Public - - add_records - [ - aRecord { - name "aName" - ttl 7200 - add_ipv4_addresses [ "192.168.0.1"; "192.168.0.2" ] - } - ] + let template = arm { + add_resources [ + subdomainZone + // When delegating lookups to another DNS zone, you add an NS record to your existing zone and reference the delegated zone to get it's NSRecords. + nsRecord { + name "subdomain" + link_to_unmanaged_dns_zone (Farmer.Arm.Dns.zones.resourceId "farmer.com") + ttl (int (TimeSpan.FromDays 2.).TotalSeconds) + add_nsd_reference subdomainZone } + ] + } - let template = - arm { - add_resources - [ - subdomainZone - // When delegating lookups to another DNS zone, you add an NS record to your existing zone and reference the delegated zone to get it's NSRecords. - nsRecord { - name "subdomain" - link_to_unmanaged_dns_zone (Farmer.Arm.Dns.zones.resourceId "farmer.com") - ttl (int (TimeSpan.FromDays 2.).TotalSeconds) - add_nsd_reference subdomainZone - } - ] + let jobj = template.Template |> Writer.toJson |> JObject.Parse + + let delegatedNsRecord = + jobj.SelectToken("resources[?(@.name=='farmer.com/subdomain')].properties.NSRecords") + |> string + + Expect.equal + delegatedNsRecord + nsrecords + "Incorrect reference generated for NS record of delegated subdomain." + } + test "Delegate subdomain to a zone in another group and subscription" { + let fakeSubId = "8231b360-0d7f-460c-b421-62146c4716b3" + + let nsrecords = + $"[reference(resourceId('{fakeSubId}', 'res-group', 'Microsoft.Network/dnsZones/NS', 'subdomain.farmer.com', '@'), '2018-05-01').NSRecords]" + + let template = arm { + add_resources [ + nsRecord { + name "subdomain" + link_to_unmanaged_dns_zone (Farmer.Arm.Dns.zones.resourceId "farmer.com") + ttl (int (TimeSpan.FromDays 2.).TotalSeconds) + + add_nsd_reference ( + ResourceId.create ( + Farmer.Arm.Dns.zones, + ResourceName "subdomain.farmer.com", + "res-group", + fakeSubId + ) + ) } - - let jobj = template.Template |> Writer.toJson |> JObject.Parse - - let delegatedNsRecord = - jobj.SelectToken("resources[?(@.name=='farmer.com/subdomain')].properties.NSRecords") - |> string - - Expect.equal - delegatedNsRecord - nsrecords - "Incorrect reference generated for NS record of delegated subdomain." + ] } - test "Delegate subdomain to a zone in another group and subscription" { - let fakeSubId = "8231b360-0d7f-460c-b421-62146c4716b3" - let nsrecords = - $"[reference(resourceId('{fakeSubId}', 'res-group', 'Microsoft.Network/dnsZones/NS', 'subdomain.farmer.com', '@'), '2018-05-01').NSRecords]" + let jobj = template.Template |> Writer.toJson |> JObject.Parse + + let delegatedNsRecord = + jobj.SelectToken("resources[?(@.name=='farmer.com/subdomain')].properties.NSRecords") + |> string - let template = + Expect.equal + delegatedNsRecord + nsrecords + "Incorrect reference generated for NS record of delegated subdomain in different group/subscription." + } + test "Disallow adding NSD reference after NSD names are added to prevent overwriting" { + Expect.throws + (fun _ -> arm { - add_resources - [ - nsRecord { - name "subdomain" - link_to_unmanaged_dns_zone (Farmer.Arm.Dns.zones.resourceId "farmer.com") - ttl (int (TimeSpan.FromDays 2.).TotalSeconds) - - add_nsd_reference ( - ResourceId.create ( - Farmer.Arm.Dns.zones, - ResourceName "subdomain.farmer.com", - "res-group", - fakeSubId - ) - ) - } - ] + add_resources [ + nsRecord { + name "subdomain" + link_to_unmanaged_dns_zone (Farmer.Arm.Dns.zones.resourceId "farmer.com") + ttl (int (TimeSpan.FromDays 2.).TotalSeconds) + add_nsd_names [ "ns01.foo.bar " ] + add_nsd_reference (Farmer.Arm.Dns.zones.resourceId "subdomain.farmer.com") + } + ] } - - let jobj = template.Template |> Writer.toJson |> JObject.Parse - - let delegatedNsRecord = - jobj.SelectToken("resources[?(@.name=='farmer.com/subdomain')].properties.NSRecords") - |> string - - Expect.equal - delegatedNsRecord - nsrecords - "Incorrect reference generated for NS record of delegated subdomain in different group/subscription." - } - test "Disallow adding NSD reference after NSD names are added to prevent overwriting" { - Expect.throws - (fun _ -> - arm { - add_resources - [ - nsRecord { - name "subdomain" - link_to_unmanaged_dns_zone (Farmer.Arm.Dns.zones.resourceId "farmer.com") - ttl (int (TimeSpan.FromDays 2.).TotalSeconds) - add_nsd_names [ "ns01.foo.bar " ] - add_nsd_reference (Farmer.Arm.Dns.zones.resourceId "subdomain.farmer.com") + |> ignore) + "Should fail when add_nsd_records was already called" + } + test "Disallow adding NSD records after NSD reference to prevent overwriting" { + Expect.throws + (fun _ -> + arm { + add_resources [ + nsRecord { + name "subdomain" + link_to_unmanaged_dns_zone (Farmer.Arm.Dns.zones.resourceId "farmer.com") + ttl (int (TimeSpan.FromDays 2.).TotalSeconds) + add_nsd_reference (Farmer.Arm.Dns.zones.resourceId "subdomain.farmer.com") + add_nsd_names [ "ns01.foo.bar " ] + } + ] + } + |> ignore) + "Should fail when add_nsd_reference was already called" + } + test "Private DNS Zone is created with records" { + let resources = arm { + add_resources [ + dnsZone { + name "farmer.com" + zone_type Private + + add_records [ + cnameRecord { + name "www" + ttl 3600 + cname "farmer.com" + } + aRecord { + name "aName" + ttl 7200 + add_ipv4_addresses [ "192.168.0.1" ] + } + aaaaRecord { + name "aaaaName" + ttl 7200 + add_ipv6_addresses [ "2001:0db8:85a3:0000:0000:8a2e:0370:7334" ] + } + ptrRecord { + name "ptrName" + ttl 3600 + add_ptrd_names [ "farmer.com" ] + } + txtRecord { + name "txtName" + ttl 3600 + add_values [ "somevalue" ] + } + mxRecord { + name "mxName" + ttl 7200 + + add_values [ + 0, "farmer-com.mail.protection.outlook.com" + 1, "farmer2-com.mail.protection.outlook.com" + ] + } + srvRecord { + name "_sip._tcp.name" + ttl 3600 + + add_values [ + { + Priority = Some 100 + Weight = Some 1 + Port = Some 5061 + Target = Some "farmer.online.com." } ] - } - |> ignore) - "Should fail when add_nsd_records was already called" + } + soaRecord { + name "soaName" + host "azureprivatedns.net" + ttl 3600 + email "azuredns-hostmaster.microsoft.com" + serial_number 1L + minimum_ttl 300L + refresh_time 3600L + retry_time 300L + expire_time 2419200L + } + ] + } + ] } - test "Disallow adding NSD records after NSD reference to prevent overwriting" { - Expect.throws - (fun _ -> - arm { - add_resources - [ + + let dnsZones = + resources + |> findAzureResources client.SerializationSettings + |> Array.ofList + + Expect.equal dnsZones.[0].Name "farmer.com" "DNS Zone name is wrong" + Expect.equal dnsZones.[0].Type "Microsoft.Network/privateDnsZones" "DNS Zone type is wrong" + Expect.equal dnsZones.[0].ZoneType (Nullable ZoneType.Private) "DNS Zone ZoneType is wrong" + + let jsn = resources.Template |> Writer.toJson + let jobj = jsn |> Newtonsoft.Json.Linq.JObject.Parse + + Expect.equal + (jobj.SelectToken("resources[?(@.name=='farmer.com/www')].type").ToString()) + "Microsoft.Network/privateDnsZones/CNAME" + "DNS record type is wrong" + + Expect.equal + (jobj + .SelectToken("resources[?(@.name=='farmer.com/www')].dependsOn[0]") + .ToString()) + "[resourceId('Microsoft.Network/privateDnsZones', 'farmer.com')]" + "DNS dependsOn is wrong" + + Expect.equal + (jobj + .SelectToken("resources[?(@.name=='farmer.com/www')].properties.cnameRecord.cname") + .ToString()) + "farmer.com" + "DNS CNAME record is wrong" + + Expect.equal + (jobj + .SelectToken("resources[?(@.name=='farmer.com/www')].properties.ttl") + .ToString()) + "3600" + "DNS TTL is wrong" + + Expect.equal + (jobj.SelectToken("resources[?(@.name=='farmer.com/aName')].type").ToString()) + "Microsoft.Network/privateDnsZones/A" + "DNS record type is wrong" + + Expect.equal + (jobj + .SelectToken("resources[?(@.name=='farmer.com/aName')].dependsOn[0]") + .ToString()) + "[resourceId('Microsoft.Network/privateDnsZones', 'farmer.com')]" + "DNS dependsOn is wrong" + + Expect.equal + (jobj + .SelectToken("resources[?(@.name=='farmer.com/aName')].properties.aRecords[0].ipv4Address") + .ToString()) + "192.168.0.1" + "DNS A record is wrong" + + Expect.equal + (jobj + .SelectToken("resources[?(@.name=='farmer.com/aName')].properties.ttl") + .ToString()) + "7200" + "DNS TTL is wrong" + + Expect.equal + (jobj.SelectToken("resources[?(@.name=='farmer.com/aaaaName')].type").ToString()) + "Microsoft.Network/privateDnsZones/AAAA" + "DNS record type is wrong" + + Expect.equal + (jobj + .SelectToken("resources[?(@.name=='farmer.com/aaaaName')].dependsOn[0]") + .ToString()) + "[resourceId('Microsoft.Network/privateDnsZones', 'farmer.com')]" + "DNS dependsOn is wrong" + + Expect.equal + (jobj + .SelectToken("resources[?(@.name=='farmer.com/aaaaName')].properties.aaaaRecords[0].ipv6Address") + .ToString()) + "2001:0db8:85a3:0000:0000:8a2e:0370:7334" + "DNS AAAA record is wrong" + + Expect.equal + (jobj + .SelectToken("resources[?(@.name=='farmer.com/aaaaName')].properties.ttl") + .ToString()) + "7200" + "DNS TTL is wrong" + + Expect.equal + (jobj.SelectToken("resources[?(@.name=='farmer.com/ptrName')].type").ToString()) + "Microsoft.Network/privateDnsZones/PTR" + "DNS record type is wrong" + + Expect.equal + (jobj + .SelectToken("resources[?(@.name=='farmer.com/ptrName')].dependsOn[0]") + .ToString()) + "[resourceId('Microsoft.Network/privateDnsZones', 'farmer.com')]" + "DNS dependsOn is wrong" + + Expect.equal + (jobj + .SelectToken("resources[?(@.name=='farmer.com/ptrName')].properties.ptrRecords[0].ptrdname") + .ToString()) + "farmer.com" + "DNS PTR record is wrong" + + Expect.equal + (jobj + .SelectToken("resources[?(@.name=='farmer.com/ptrName')].properties.ttl") + .ToString()) + "3600" + "DNS TTL is wrong" + + Expect.equal + (jobj.SelectToken("resources[?(@.name=='farmer.com/txtName')].type").ToString()) + "Microsoft.Network/privateDnsZones/TXT" + "DNS record type is wrong" + + Expect.equal + (jobj + .SelectToken("resources[?(@.name=='farmer.com/txtName')].dependsOn[0]") + .ToString()) + "[resourceId('Microsoft.Network/privateDnsZones', 'farmer.com')]" + "DNS dependsOn is wrong" + + Expect.equal + (jobj + .SelectToken("resources[?(@.name=='farmer.com/txtName')].properties.txtRecords[0].value[0]") + .ToString()) + "somevalue" + "DNS TXT record is wrong" + + Expect.equal + (jobj + .SelectToken("resources[?(@.name=='farmer.com/txtName')].properties.ttl") + .ToString()) + "3600" + "DNS TTL is wrong" + + Expect.equal + (jobj.SelectToken("resources[?(@.name=='farmer.com/mxName')].type").ToString()) + "Microsoft.Network/privateDnsZones/MX" + "DNS record type is wrong" + + Expect.equal + (jobj + .SelectToken("resources[?(@.name=='farmer.com/mxName')].dependsOn[0]") + .ToString()) + "[resourceId('Microsoft.Network/privateDnsZones', 'farmer.com')]" + "DNS dependsOn is wrong" + + Expect.equal + (jobj + .SelectToken("resources[?(@.name=='farmer.com/mxName')].properties.mxRecords[0].exchange") + .ToString()) + "farmer-com.mail.protection.outlook.com" + "DNS MX record exchange is wrong" + + Expect.equal + (jobj + .SelectToken("resources[?(@.name=='farmer.com/mxName')].properties.mxRecords[0].preference") + .ToString()) + "0" + "DNS MX record preference is wrong" + + Expect.equal + (jobj + .SelectToken("resources[?(@.name=='farmer.com/mxName')].properties.mxRecords[1].exchange") + .ToString()) + "farmer2-com.mail.protection.outlook.com" + "DNS MX record exchange is wrong" + + Expect.equal + (jobj + .SelectToken("resources[?(@.name=='farmer.com/mxName')].properties.mxRecords[1].preference") + .ToString()) + "1" + "DNS MX record preference is wrong" + + Expect.equal + (jobj + .SelectToken("resources[?(@.name=='farmer.com/mxName')].properties.ttl") + .ToString()) + "7200" + "DNS TTL is wrong" + + Expect.equal + (jobj + .SelectToken("resources[?(@.name=='farmer.com/_sip._tcp.name')].type") + .ToString()) + "Microsoft.Network/privateDnsZones/SRV" + "DNS record type is wrong" + + Expect.equal + (jobj + .SelectToken("resources[?(@.name=='farmer.com/_sip._tcp.name')].dependsOn[0]") + .ToString()) + "[resourceId('Microsoft.Network/privateDnsZones', 'farmer.com')]" + "DNS dependsOn is wrong" + + Expect.equal + (jobj + .SelectToken("resources[?(@.name=='farmer.com/_sip._tcp.name')].properties.srvRecords[0].port") + .ToString()) + "5061" + "DNS SRV record port is wrong" + + Expect.equal + (jobj + .SelectToken("resources[?(@.name=='farmer.com/_sip._tcp.name')].properties.srvRecords[0].priority") + .ToString()) + "100" + "DNS SRV record priority is wrong" + + Expect.equal + (jobj + .SelectToken("resources[?(@.name=='farmer.com/_sip._tcp.name')].properties.srvRecords[0].target") + .ToString()) + "farmer.online.com." + "DNS SRV record target is wrong" + + Expect.equal + (jobj + .SelectToken("resources[?(@.name=='farmer.com/_sip._tcp.name')].properties.srvRecords[0].weight") + .ToString()) + "1" + "DNS SRV record weight is wrong" + + Expect.equal + (jobj + .SelectToken("resources[?(@.name=='farmer.com/_sip._tcp.name')].properties.ttl") + .ToString()) + "3600" + "DNS TTL is wrong" + + Expect.equal + (jobj.SelectToken("resources[?(@.name=='farmer.com/soaName')].type").ToString()) + "Microsoft.Network/privateDnsZones/SOA" + "DNS record type is wrong" + + Expect.equal + (jobj + .SelectToken("resources[?(@.name=='farmer.com/soaName')].dependsOn[0]") + .ToString()) + "[resourceId('Microsoft.Network/privateDnsZones', 'farmer.com')]" + "DNS dependsOn is wrong" + + Expect.equal + (jobj + .SelectToken("resources[?(@.name=='farmer.com/soaName')].properties.soaRecord.email") + .ToString()) + "azuredns-hostmaster.microsoft.com" + "DNS SOA record email is wrong" + + Expect.equal + (jobj + .SelectToken("resources[?(@.name=='farmer.com/soaName')].properties.soaRecord.expireTime") + .ToString()) + "2419200" + "DNS SOA record expireTime is wrong" + + Expect.equal + (jobj + .SelectToken("resources[?(@.name=='farmer.com/soaName')].properties.soaRecord.host") + .ToString()) + "azureprivatedns.net" + "DNS SOA record host is wrong" + + Expect.equal + (jobj + .SelectToken("resources[?(@.name=='farmer.com/soaName')].properties.soaRecord.minimumTTL") + .ToString()) + "300" + "DNS SOA record minimumTTL is wrong" + + Expect.equal + (jobj + .SelectToken("resources[?(@.name=='farmer.com/soaName')].properties.soaRecord.refreshTime") + .ToString()) + "3600" + "DNS SOA record refreshTime is wrong" + + Expect.equal + (jobj + .SelectToken("resources[?(@.name=='farmer.com/soaName')].properties.soaRecord.retryTime") + .ToString()) + "300" + "DNS SOA record retryTime is wrong" + + Expect.equal + (jobj + .SelectToken("resources[?(@.name=='farmer.com/soaName')].properties.soaRecord.serialNumber") + .ToString()) + "1" + "DNS SOA record serialNumber is wrong" + + Expect.equal + (jobj + .SelectToken("resources[?(@.name=='farmer.com/soaName')].properties.ttl") + .ToString()) + "3600" + "DNS TTL is wrong" + } + test "Disallow adding private NS records" { + Expect.throws + (fun _ -> + arm { + add_resources [ + dnsZone { + name "farmer.com" + zone_type Private + + add_records [ nsRecord { name "subdomain" - link_to_unmanaged_dns_zone (Farmer.Arm.Dns.zones.resourceId "farmer.com") ttl (int (TimeSpan.FromDays 2.).TotalSeconds) - add_nsd_reference (Farmer.Arm.Dns.zones.resourceId "subdomain.farmer.com") add_nsd_names [ "ns01.foo.bar " ] } ] - } - |> ignore) - "Should fail when add_nsd_reference was already called" - } - test "Private DNS Zone is created with records" { - let resources = - arm { - add_resources - [ - dnsZone { - name "farmer.com" - zone_type Private - - add_records - [ - cnameRecord { - name "www" - ttl 3600 - cname "farmer.com" - } - aRecord { - name "aName" - ttl 7200 - add_ipv4_addresses [ "192.168.0.1" ] - } - aaaaRecord { - name "aaaaName" - ttl 7200 - add_ipv6_addresses [ "2001:0db8:85a3:0000:0000:8a2e:0370:7334" ] - } - ptrRecord { - name "ptrName" - ttl 3600 - add_ptrd_names [ "farmer.com" ] - } - txtRecord { - name "txtName" - ttl 3600 - add_values [ "somevalue" ] - } - mxRecord { - name "mxName" - ttl 7200 - - add_values - [ - 0, "farmer-com.mail.protection.outlook.com" - 1, "farmer2-com.mail.protection.outlook.com" - ] - } - srvRecord { - name "_sip._tcp.name" - ttl 3600 - - add_values - [ - { - Priority = Some 100 - Weight = Some 1 - Port = Some 5061 - Target = Some "farmer.online.com." - } - ] - } - soaRecord { - name "soaName" - host "azureprivatedns.net" - ttl 3600 - email "azuredns-hostmaster.microsoft.com" - serial_number 1L - minimum_ttl 300L - refresh_time 3600L - retry_time 300L - expire_time 2419200L - } - ] - } - ] + } + ] + } + |> ignore) + "Should fail when adding NS record to private zone" + } + test "Can link dns record to unmanaged private DNS zone" { + let resources = arm { + add_resources [ + cnameRecord { + name "www" + ttl 3600 + cname "farmer.com" + zone_type Private + link_to_unmanaged_dns_zone (Farmer.Arm.Dns.zones.resourceId "private.farmer.com") + } + aRecord { + name "aName" + ttl 7200 + add_ipv4_addresses [ "192.168.0.1" ] + zone_type Private + link_to_unmanaged_dns_zone (Farmer.Arm.Dns.zones.resourceId "private.farmer.com") + } + aaaaRecord { + name "aaaaName" + ttl 7200 + add_ipv6_addresses [ "2001:0db8:85a3:0000:0000:8a2e:0370:7334" ] + zone_type Private + link_to_unmanaged_dns_zone (Farmer.Arm.Dns.zones.resourceId "private.farmer.com") + } + ptrRecord { + name "ptrName" + ttl 3600 + add_ptrd_names [ "farmer.com" ] + zone_type Private + link_to_unmanaged_dns_zone (Farmer.Arm.Dns.zones.resourceId "private.farmer.com") + } + txtRecord { + name "txtName" + ttl 3600 + add_values [ "somevalue" ] + zone_type Private + link_to_unmanaged_dns_zone (Farmer.Arm.Dns.zones.resourceId "private.farmer.com") } + mxRecord { + name "mxName" + ttl 7200 - let dnsZones = - resources - |> findAzureResources client.SerializationSettings - |> Array.ofList - - Expect.equal dnsZones.[0].Name "farmer.com" "DNS Zone name is wrong" - Expect.equal dnsZones.[0].Type "Microsoft.Network/privateDnsZones" "DNS Zone type is wrong" - Expect.equal dnsZones.[0].ZoneType (Nullable ZoneType.Private) "DNS Zone ZoneType is wrong" - - let jsn = resources.Template |> Writer.toJson - let jobj = jsn |> Newtonsoft.Json.Linq.JObject.Parse - - Expect.equal - (jobj.SelectToken("resources[?(@.name=='farmer.com/www')].type").ToString()) - "Microsoft.Network/privateDnsZones/CNAME" - "DNS record type is wrong" - - Expect.equal - (jobj - .SelectToken("resources[?(@.name=='farmer.com/www')].dependsOn[0]") - .ToString()) - "[resourceId('Microsoft.Network/privateDnsZones', 'farmer.com')]" - "DNS dependsOn is wrong" - - Expect.equal - (jobj - .SelectToken("resources[?(@.name=='farmer.com/www')].properties.cnameRecord.cname") - .ToString()) - "farmer.com" - "DNS CNAME record is wrong" - - Expect.equal - (jobj - .SelectToken("resources[?(@.name=='farmer.com/www')].properties.ttl") - .ToString()) - "3600" - "DNS TTL is wrong" - - Expect.equal - (jobj.SelectToken("resources[?(@.name=='farmer.com/aName')].type").ToString()) - "Microsoft.Network/privateDnsZones/A" - "DNS record type is wrong" - - Expect.equal - (jobj - .SelectToken("resources[?(@.name=='farmer.com/aName')].dependsOn[0]") - .ToString()) - "[resourceId('Microsoft.Network/privateDnsZones', 'farmer.com')]" - "DNS dependsOn is wrong" - - Expect.equal - (jobj - .SelectToken("resources[?(@.name=='farmer.com/aName')].properties.aRecords[0].ipv4Address") - .ToString()) - "192.168.0.1" - "DNS A record is wrong" - - Expect.equal - (jobj - .SelectToken("resources[?(@.name=='farmer.com/aName')].properties.ttl") - .ToString()) - "7200" - "DNS TTL is wrong" - - Expect.equal - (jobj.SelectToken("resources[?(@.name=='farmer.com/aaaaName')].type").ToString()) - "Microsoft.Network/privateDnsZones/AAAA" - "DNS record type is wrong" - - Expect.equal - (jobj - .SelectToken("resources[?(@.name=='farmer.com/aaaaName')].dependsOn[0]") - .ToString()) - "[resourceId('Microsoft.Network/privateDnsZones', 'farmer.com')]" - "DNS dependsOn is wrong" - - Expect.equal - (jobj - .SelectToken( - "resources[?(@.name=='farmer.com/aaaaName')].properties.aaaaRecords[0].ipv6Address" - ) - .ToString()) - "2001:0db8:85a3:0000:0000:8a2e:0370:7334" - "DNS AAAA record is wrong" - - Expect.equal - (jobj - .SelectToken("resources[?(@.name=='farmer.com/aaaaName')].properties.ttl") - .ToString()) - "7200" - "DNS TTL is wrong" - - Expect.equal - (jobj.SelectToken("resources[?(@.name=='farmer.com/ptrName')].type").ToString()) - "Microsoft.Network/privateDnsZones/PTR" - "DNS record type is wrong" - - Expect.equal - (jobj - .SelectToken("resources[?(@.name=='farmer.com/ptrName')].dependsOn[0]") - .ToString()) - "[resourceId('Microsoft.Network/privateDnsZones', 'farmer.com')]" - "DNS dependsOn is wrong" - - Expect.equal - (jobj - .SelectToken("resources[?(@.name=='farmer.com/ptrName')].properties.ptrRecords[0].ptrdname") - .ToString()) - "farmer.com" - "DNS PTR record is wrong" - - Expect.equal - (jobj - .SelectToken("resources[?(@.name=='farmer.com/ptrName')].properties.ttl") - .ToString()) - "3600" - "DNS TTL is wrong" - - Expect.equal - (jobj.SelectToken("resources[?(@.name=='farmer.com/txtName')].type").ToString()) - "Microsoft.Network/privateDnsZones/TXT" - "DNS record type is wrong" - - Expect.equal - (jobj - .SelectToken("resources[?(@.name=='farmer.com/txtName')].dependsOn[0]") - .ToString()) - "[resourceId('Microsoft.Network/privateDnsZones', 'farmer.com')]" - "DNS dependsOn is wrong" - - Expect.equal - (jobj - .SelectToken("resources[?(@.name=='farmer.com/txtName')].properties.txtRecords[0].value[0]") - .ToString()) - "somevalue" - "DNS TXT record is wrong" - - Expect.equal - (jobj - .SelectToken("resources[?(@.name=='farmer.com/txtName')].properties.ttl") - .ToString()) - "3600" - "DNS TTL is wrong" - - Expect.equal - (jobj.SelectToken("resources[?(@.name=='farmer.com/mxName')].type").ToString()) - "Microsoft.Network/privateDnsZones/MX" - "DNS record type is wrong" - - Expect.equal - (jobj - .SelectToken("resources[?(@.name=='farmer.com/mxName')].dependsOn[0]") - .ToString()) - "[resourceId('Microsoft.Network/privateDnsZones', 'farmer.com')]" - "DNS dependsOn is wrong" - - Expect.equal - (jobj - .SelectToken("resources[?(@.name=='farmer.com/mxName')].properties.mxRecords[0].exchange") - .ToString()) - "farmer-com.mail.protection.outlook.com" - "DNS MX record exchange is wrong" - - Expect.equal - (jobj - .SelectToken("resources[?(@.name=='farmer.com/mxName')].properties.mxRecords[0].preference") - .ToString()) - "0" - "DNS MX record preference is wrong" - - Expect.equal - (jobj - .SelectToken("resources[?(@.name=='farmer.com/mxName')].properties.mxRecords[1].exchange") - .ToString()) - "farmer2-com.mail.protection.outlook.com" - "DNS MX record exchange is wrong" - - Expect.equal - (jobj - .SelectToken("resources[?(@.name=='farmer.com/mxName')].properties.mxRecords[1].preference") - .ToString()) - "1" - "DNS MX record preference is wrong" - - Expect.equal - (jobj - .SelectToken("resources[?(@.name=='farmer.com/mxName')].properties.ttl") - .ToString()) - "7200" - "DNS TTL is wrong" - - Expect.equal - (jobj - .SelectToken("resources[?(@.name=='farmer.com/_sip._tcp.name')].type") - .ToString()) - "Microsoft.Network/privateDnsZones/SRV" - "DNS record type is wrong" - - Expect.equal - (jobj - .SelectToken("resources[?(@.name=='farmer.com/_sip._tcp.name')].dependsOn[0]") - .ToString()) - "[resourceId('Microsoft.Network/privateDnsZones', 'farmer.com')]" - "DNS dependsOn is wrong" - - Expect.equal - (jobj - .SelectToken("resources[?(@.name=='farmer.com/_sip._tcp.name')].properties.srvRecords[0].port") - .ToString()) - "5061" - "DNS SRV record port is wrong" - - Expect.equal - (jobj - .SelectToken( - "resources[?(@.name=='farmer.com/_sip._tcp.name')].properties.srvRecords[0].priority" - ) - .ToString()) - "100" - "DNS SRV record priority is wrong" - - Expect.equal - (jobj - .SelectToken( - "resources[?(@.name=='farmer.com/_sip._tcp.name')].properties.srvRecords[0].target" - ) - .ToString()) - "farmer.online.com." - "DNS SRV record target is wrong" - - Expect.equal - (jobj - .SelectToken( - "resources[?(@.name=='farmer.com/_sip._tcp.name')].properties.srvRecords[0].weight" - ) - .ToString()) - "1" - "DNS SRV record weight is wrong" - - Expect.equal - (jobj - .SelectToken("resources[?(@.name=='farmer.com/_sip._tcp.name')].properties.ttl") - .ToString()) - "3600" - "DNS TTL is wrong" - - Expect.equal - (jobj.SelectToken("resources[?(@.name=='farmer.com/soaName')].type").ToString()) - "Microsoft.Network/privateDnsZones/SOA" - "DNS record type is wrong" - - Expect.equal - (jobj - .SelectToken("resources[?(@.name=='farmer.com/soaName')].dependsOn[0]") - .ToString()) - "[resourceId('Microsoft.Network/privateDnsZones', 'farmer.com')]" - "DNS dependsOn is wrong" - - Expect.equal - (jobj - .SelectToken("resources[?(@.name=='farmer.com/soaName')].properties.soaRecord.email") - .ToString()) - "azuredns-hostmaster.microsoft.com" - "DNS SOA record email is wrong" - - Expect.equal - (jobj - .SelectToken("resources[?(@.name=='farmer.com/soaName')].properties.soaRecord.expireTime") - .ToString()) - "2419200" - "DNS SOA record expireTime is wrong" - - Expect.equal - (jobj - .SelectToken("resources[?(@.name=='farmer.com/soaName')].properties.soaRecord.host") - .ToString()) - "azureprivatedns.net" - "DNS SOA record host is wrong" - - Expect.equal - (jobj - .SelectToken("resources[?(@.name=='farmer.com/soaName')].properties.soaRecord.minimumTTL") - .ToString()) - "300" - "DNS SOA record minimumTTL is wrong" - - Expect.equal - (jobj - .SelectToken("resources[?(@.name=='farmer.com/soaName')].properties.soaRecord.refreshTime") - .ToString()) - "3600" - "DNS SOA record refreshTime is wrong" - - Expect.equal - (jobj - .SelectToken("resources[?(@.name=='farmer.com/soaName')].properties.soaRecord.retryTime") - .ToString()) - "300" - "DNS SOA record retryTime is wrong" - - Expect.equal - (jobj - .SelectToken("resources[?(@.name=='farmer.com/soaName')].properties.soaRecord.serialNumber") - .ToString()) - "1" - "DNS SOA record serialNumber is wrong" - - Expect.equal - (jobj - .SelectToken("resources[?(@.name=='farmer.com/soaName')].properties.ttl") - .ToString()) - "3600" - "DNS TTL is wrong" - } - test "Disallow adding private NS records" { - Expect.throws - (fun _ -> - arm { - add_resources - [ - dnsZone { - name "farmer.com" - zone_type Private - - add_records - [ - nsRecord { - name "subdomain" - ttl (int (TimeSpan.FromDays 2.).TotalSeconds) - add_nsd_names [ "ns01.foo.bar " ] - } - ] - } - ] - } - |> ignore) - "Should fail when adding NS record to private zone" - } - test "Can link dns record to unmanaged private DNS zone" { - let resources = - arm { - add_resources - [ - cnameRecord { - name "www" - ttl 3600 - cname "farmer.com" - zone_type Private - link_to_unmanaged_dns_zone (Farmer.Arm.Dns.zones.resourceId "private.farmer.com") - } - aRecord { - name "aName" - ttl 7200 - add_ipv4_addresses [ "192.168.0.1" ] - zone_type Private - link_to_unmanaged_dns_zone (Farmer.Arm.Dns.zones.resourceId "private.farmer.com") - } - aaaaRecord { - name "aaaaName" - ttl 7200 - add_ipv6_addresses [ "2001:0db8:85a3:0000:0000:8a2e:0370:7334" ] - zone_type Private - link_to_unmanaged_dns_zone (Farmer.Arm.Dns.zones.resourceId "private.farmer.com") - } - ptrRecord { - name "ptrName" - ttl 3600 - add_ptrd_names [ "farmer.com" ] - zone_type Private - link_to_unmanaged_dns_zone (Farmer.Arm.Dns.zones.resourceId "private.farmer.com") - } - txtRecord { - name "txtName" - ttl 3600 - add_values [ "somevalue" ] - zone_type Private - link_to_unmanaged_dns_zone (Farmer.Arm.Dns.zones.resourceId "private.farmer.com") - } - mxRecord { - name "mxName" - ttl 7200 - - add_values - [ - 0, "farmer-com.mail.protection.outlook.com" - 1, "farmer2-com.mail.protection.outlook.com" - ] - - zone_type Private - link_to_unmanaged_dns_zone (Farmer.Arm.Dns.zones.resourceId "private.farmer.com") - } - srvRecord { - name "_sip._tcp.name" - ttl 3600 - - add_values - [ - { - Priority = Some 100 - Weight = Some 1 - Port = Some 5061 - Target = Some "farmer.online.com." - } - ] - - zone_type Private - link_to_unmanaged_dns_zone (Farmer.Arm.Dns.zones.resourceId "private.farmer.com") - } - soaRecord { - name "soaName" - host "azureprivatedns.net" - ttl 3600 - email "azuredns-hostmaster.microsoft.com" - serial_number 1L - minimum_ttl 300L - refresh_time 3600L - retry_time 300L - expire_time 2419200L - zone_type Private - link_to_unmanaged_dns_zone (Farmer.Arm.Dns.zones.resourceId "private.farmer.com") - } - ] + add_values [ + 0, "farmer-com.mail.protection.outlook.com" + 1, "farmer2-com.mail.protection.outlook.com" + ] + + zone_type Private + link_to_unmanaged_dns_zone (Farmer.Arm.Dns.zones.resourceId "private.farmer.com") } + srvRecord { + name "_sip._tcp.name" + ttl 3600 - let jsn = resources.Template |> Writer.toJson - let jobj = jsn |> Newtonsoft.Json.Linq.JObject.Parse - - Expect.equal - (jobj - .SelectToken("resources[?(@.name=='private.farmer.com/www')].type") - .ToString()) - "Microsoft.Network/privateDnsZones/CNAME" - "DNS record type is wrong" - - Expect.equal - (jobj - .SelectToken("resources[?(@.name=='private.farmer.com/aName')].type") - .ToString()) - "Microsoft.Network/privateDnsZones/A" - "DNS record type is wrong" - - Expect.equal - (jobj - .SelectToken("resources[?(@.name=='private.farmer.com/aaaaName')].type") - .ToString()) - "Microsoft.Network/privateDnsZones/AAAA" - "DNS record type is wrong" - - Expect.equal - (jobj - .SelectToken("resources[?(@.name=='private.farmer.com/ptrName')].type") - .ToString()) - "Microsoft.Network/privateDnsZones/PTR" - "DNS record type is wrong" - - Expect.equal - (jobj - .SelectToken("resources[?(@.name=='private.farmer.com/txtName')].type") - .ToString()) - "Microsoft.Network/privateDnsZones/TXT" - "DNS record type is wrong" - - Expect.equal - (jobj - .SelectToken("resources[?(@.name=='private.farmer.com/mxName')].type") - .ToString()) - "Microsoft.Network/privateDnsZones/MX" - "DNS record type is wrong" - - Expect.equal - (jobj - .SelectToken("resources[?(@.name=='private.farmer.com/_sip._tcp.name')].type") - .ToString()) - "Microsoft.Network/privateDnsZones/SRV" - "DNS record type is wrong" - - Expect.equal - (jobj - .SelectToken("resources[?(@.name=='private.farmer.com/soaName')].type") - .ToString()) - "Microsoft.Network/privateDnsZones/SOA" - "DNS record type is wrong" + add_values [ + { + Priority = Some 100 + Weight = Some 1 + Port = Some 5061 + Target = Some "farmer.online.com." + } + ] + + zone_type Private + link_to_unmanaged_dns_zone (Farmer.Arm.Dns.zones.resourceId "private.farmer.com") + } + soaRecord { + name "soaName" + host "azureprivatedns.net" + ttl 3600 + email "azuredns-hostmaster.microsoft.com" + serial_number 1L + minimum_ttl 300L + refresh_time 3600L + retry_time 300L + expire_time 2419200L + zone_type Private + link_to_unmanaged_dns_zone (Farmer.Arm.Dns.zones.resourceId "private.farmer.com") + } + ] } - ] + + let jsn = resources.Template |> Writer.toJson + let jobj = jsn |> Newtonsoft.Json.Linq.JObject.Parse + + Expect.equal + (jobj + .SelectToken("resources[?(@.name=='private.farmer.com/www')].type") + .ToString()) + "Microsoft.Network/privateDnsZones/CNAME" + "DNS record type is wrong" + + Expect.equal + (jobj + .SelectToken("resources[?(@.name=='private.farmer.com/aName')].type") + .ToString()) + "Microsoft.Network/privateDnsZones/A" + "DNS record type is wrong" + + Expect.equal + (jobj + .SelectToken("resources[?(@.name=='private.farmer.com/aaaaName')].type") + .ToString()) + "Microsoft.Network/privateDnsZones/AAAA" + "DNS record type is wrong" + + Expect.equal + (jobj + .SelectToken("resources[?(@.name=='private.farmer.com/ptrName')].type") + .ToString()) + "Microsoft.Network/privateDnsZones/PTR" + "DNS record type is wrong" + + Expect.equal + (jobj + .SelectToken("resources[?(@.name=='private.farmer.com/txtName')].type") + .ToString()) + "Microsoft.Network/privateDnsZones/TXT" + "DNS record type is wrong" + + Expect.equal + (jobj + .SelectToken("resources[?(@.name=='private.farmer.com/mxName')].type") + .ToString()) + "Microsoft.Network/privateDnsZones/MX" + "DNS record type is wrong" + + Expect.equal + (jobj + .SelectToken("resources[?(@.name=='private.farmer.com/_sip._tcp.name')].type") + .ToString()) + "Microsoft.Network/privateDnsZones/SRV" + "DNS record type is wrong" + + Expect.equal + (jobj + .SelectToken("resources[?(@.name=='private.farmer.com/soaName')].type") + .ToString()) + "Microsoft.Network/privateDnsZones/SOA" + "DNS record type is wrong" + } + ] diff --git a/src/Tests/DnsResolver.fs b/src/Tests/DnsResolver.fs index ba641f20b..3971c8ca9 100644 --- a/src/Tests/DnsResolver.fs +++ b/src/Tests/DnsResolver.fs @@ -7,235 +7,218 @@ open Farmer.Network open Newtonsoft.Json.Linq let tests = - testList - "DNS Resolver Tests" - [ - test "Basic resolver with single inbound endpoint" { - let deployment = - arm { - add_resources - [ - vnet { - name "mynet" - add_address_spaces [ "100.72.2.0/24" ] - - add_subnets - [ - subnet { - name "resolver-subnet" - prefix "100.72.2.240/28" - add_delegations [ SubnetDelegationService.DnsResolvers ] - } - ] - } - dnsResolver { - name "my-dns-resolver" - vnet "mynet" - inbound_subnet "resolver-subnet" - } - ] + testList "DNS Resolver Tests" [ + test "Basic resolver with single inbound endpoint" { + let deployment = arm { + add_resources [ + vnet { + name "mynet" + add_address_spaces [ "100.72.2.0/24" ] + + add_subnets [ + subnet { + name "resolver-subnet" + prefix "100.72.2.240/28" + add_delegations [ SubnetDelegationService.DnsResolvers ] + } + ] } - - let jobj = JObject.Parse(deployment.Template |> Writer.toJson) - let dnsResolver = jobj.SelectToken "resources[?(@name=='my-dns-resolver')]" - Expect.isNotNull dnsResolver "DNS resolver resource missing from template" - let dnsResolverDependencies = dnsResolver.SelectToken "dependsOn" - Expect.hasLength dnsResolverDependencies 1 "Incorrect number of dnsResolver dependencies" - let dnsResolverVnetId = dnsResolver.SelectToken "properties.virtualNetwork.id" - - Expect.equal - (dnsResolverVnetId |> string) - "[resourceId('Microsoft.Network/virtualNetworks', 'mynet')]" - "Incorrect vnet ID" - - let dnsResolverInbound = - jobj.SelectToken "resources[?(@name=='my-dns-resolver/resolver-subnet')]" - - Expect.isNotNull dnsResolverInbound "Generated DNS resolver inbound resource missing from template" - let ipAllocation = dnsResolverInbound.SelectToken "properties.ipConfigurations[0]" - let ipAllocationMethod = ipAllocation.["privateIpAllocationMethod"] - Expect.equal ipAllocationMethod (JValue "Dynamic") "Incorrect generated IP allocation method" - let ipAllocationSubnet = ipAllocation.SelectToken "subnet.id" - - Expect.equal - (ipAllocationSubnet |> string) - "[resourceId('Microsoft.Network/virtualNetworks/subnets', 'mynet', 'resolver-subnet')]" - "Incorrect subnet id on generated resolver inbound" + dnsResolver { + name "my-dns-resolver" + vnet "mynet" + inbound_subnet "resolver-subnet" + } + ] } - test "Adding an inbound to an existing DNS resolver" { - let deployment = - arm { - add_resources - [ - dnsInboundEndpoint { - name "another-inbound" - - link_to_subnet ( - Farmer.Arm.Network.subnets.resourceId ( - ResourceName "mynet", - ResourceName "another-resolver-subnet" - ) - ) - link_to_dns_resolver "my-dns-resolver" - } - ] + let jobj = JObject.Parse(deployment.Template |> Writer.toJson) + let dnsResolver = jobj.SelectToken "resources[?(@name=='my-dns-resolver')]" + Expect.isNotNull dnsResolver "DNS resolver resource missing from template" + let dnsResolverDependencies = dnsResolver.SelectToken "dependsOn" + Expect.hasLength dnsResolverDependencies 1 "Incorrect number of dnsResolver dependencies" + let dnsResolverVnetId = dnsResolver.SelectToken "properties.virtualNetwork.id" + + Expect.equal + (dnsResolverVnetId |> string) + "[resourceId('Microsoft.Network/virtualNetworks', 'mynet')]" + "Incorrect vnet ID" + + let dnsResolverInbound = + jobj.SelectToken "resources[?(@name=='my-dns-resolver/resolver-subnet')]" + + Expect.isNotNull dnsResolverInbound "Generated DNS resolver inbound resource missing from template" + let ipAllocation = dnsResolverInbound.SelectToken "properties.ipConfigurations[0]" + let ipAllocationMethod = ipAllocation.["privateIpAllocationMethod"] + Expect.equal ipAllocationMethod (JValue "Dynamic") "Incorrect generated IP allocation method" + let ipAllocationSubnet = ipAllocation.SelectToken "subnet.id" + + Expect.equal + (ipAllocationSubnet |> string) + "[resourceId('Microsoft.Network/virtualNetworks/subnets', 'mynet', 'resolver-subnet')]" + "Incorrect subnet id on generated resolver inbound" + } + test "Adding an inbound to an existing DNS resolver" { + let deployment = arm { + add_resources [ + dnsInboundEndpoint { + name "another-inbound" + + link_to_subnet ( + Farmer.Arm.Network.subnets.resourceId ( + ResourceName "mynet", + ResourceName "another-resolver-subnet" + ) + ) + + link_to_dns_resolver "my-dns-resolver" } - - let jobj = JObject.Parse(deployment.Template |> Writer.toJson) - - let dnsResolverInbound = - jobj.SelectToken "resources[?(@name=='my-dns-resolver/another-inbound')]" - - Expect.isNotNull dnsResolverInbound "DNS resolver inbound resource missing from template" - - Expect.isEmpty - dnsResolverInbound.["dependsOn"] - "Adding to existing resolver should have no dependencies." - - let ipAllocation = dnsResolverInbound.SelectToken "properties.ipConfigurations[0]" - let ipAllocationSubnet = ipAllocation.SelectToken "subnet.id" - - Expect.equal - (ipAllocationSubnet |> string) - "[resourceId('Microsoft.Network/virtualNetworks/subnets', 'mynet', 'another-resolver-subnet')]" - "Incorrect subnet id on generated resolver inbound" + ] } - test "Use external DNS servers for a domain" { - let deployment = - arm { - add_resources - [ - vnet { - name "mynet" - add_address_spaces [ "100.72.2.0/24" ] - - add_subnets - [ - subnet { - name "resolver-subnet" - prefix "100.72.2.240/28" - add_delegations [ SubnetDelegationService.DnsResolvers ] - } - ] - } - dnsResolver { - name "my-dns-resolver" - vnet "mynet" - - add_outbound_endpoints - [ - dnsOutboundEndpoint { - name "outbound-dns" - - link_to_subnet ( - Farmer.Arm.Network.subnets.resourceId ( - ResourceName "mynet", - ResourceName "resolver-subnet" - ) - ) - } - ] - } - dnsForwardingRuleset { - name "route-dns-requests" - depends_on [ Farmer.Arm.Network.virtualNetworks.resourceId (ResourceName "mynet") ] - - add_resolver_outbound_endpoints - [ - // list of outbound endpoint IDs - Farmer.Arm.Dns.dnsResolverOutboundEndpoints.resourceId ( - ResourceName "my-dns-resolver", - ResourceName "outbound-dns" - ) - ] - - add_vnet_links - [ - // List of vnet IDs - Farmer.Arm.Network.virtualNetworks.resourceId (ResourceName "mynet") - ] - - add_rules - [ - // List of rule sets - dnsForwardingRule { - name "rule-1" - domain_name "example.com" - state Enabled - - add_target_dns_servers - [ - System.Net.IPEndPoint.Parse("192.168.100.74:53") - System.Net.IPEndPoint.Parse("192.168.100.75:53") - ] - } - ] - } - ] + + let jobj = JObject.Parse(deployment.Template |> Writer.toJson) + + let dnsResolverInbound = + jobj.SelectToken "resources[?(@name=='my-dns-resolver/another-inbound')]" + + Expect.isNotNull dnsResolverInbound "DNS resolver inbound resource missing from template" + + Expect.isEmpty dnsResolverInbound.["dependsOn"] "Adding to existing resolver should have no dependencies." + + let ipAllocation = dnsResolverInbound.SelectToken "properties.ipConfigurations[0]" + let ipAllocationSubnet = ipAllocation.SelectToken "subnet.id" + + Expect.equal + (ipAllocationSubnet |> string) + "[resourceId('Microsoft.Network/virtualNetworks/subnets', 'mynet', 'another-resolver-subnet')]" + "Incorrect subnet id on generated resolver inbound" + } + test "Use external DNS servers for a domain" { + let deployment = arm { + add_resources [ + vnet { + name "mynet" + add_address_spaces [ "100.72.2.0/24" ] + + add_subnets [ + subnet { + name "resolver-subnet" + prefix "100.72.2.240/28" + add_delegations [ SubnetDelegationService.DnsResolvers ] + } + ] + } + dnsResolver { + name "my-dns-resolver" + vnet "mynet" + + add_outbound_endpoints [ + dnsOutboundEndpoint { + name "outbound-dns" + + link_to_subnet ( + Farmer.Arm.Network.subnets.resourceId ( + ResourceName "mynet", + ResourceName "resolver-subnet" + ) + ) + } + ] } + dnsForwardingRuleset { + name "route-dns-requests" + depends_on [ Farmer.Arm.Network.virtualNetworks.resourceId (ResourceName "mynet") ] + + add_resolver_outbound_endpoints [ + // list of outbound endpoint IDs + Farmer.Arm.Dns.dnsResolverOutboundEndpoints.resourceId ( + ResourceName "my-dns-resolver", + ResourceName "outbound-dns" + ) + ] + + add_vnet_links [ + // List of vnet IDs + Farmer.Arm.Network.virtualNetworks.resourceId (ResourceName "mynet") + ] + + add_rules [ + // List of rule sets + dnsForwardingRule { + name "rule-1" + domain_name "example.com" + state Enabled + + add_target_dns_servers [ + System.Net.IPEndPoint.Parse("192.168.100.74:53") + System.Net.IPEndPoint.Parse("192.168.100.75:53") + ] + } + ] + } + ] + } - let jobj = JObject.Parse(deployment.Template |> Writer.toJson) - // RuleSet - let dnsRuleset = jobj.SelectToken "resources[?(@name=='route-dns-requests')]" - Expect.isNotNull dnsRuleset "DNS forwarding ruleset missing from template" - Expect.hasLength dnsRuleset.["dependsOn"] 2 "Incorrect number of dependencies on ruleset." + let jobj = JObject.Parse(deployment.Template |> Writer.toJson) + // RuleSet + let dnsRuleset = jobj.SelectToken "resources[?(@name=='route-dns-requests')]" + Expect.isNotNull dnsRuleset "DNS forwarding ruleset missing from template" + Expect.hasLength dnsRuleset.["dependsOn"] 2 "Incorrect number of dependencies on ruleset." - let expectedRulesetDeps = - JArray( - "[resourceId('Microsoft.Network/dnsResolvers/outboundEndpoints', 'my-dns-resolver', 'outbound-dns')]", - "[resourceId('Microsoft.Network/virtualNetworks', 'mynet')]" - ) + let expectedRulesetDeps = + JArray( + "[resourceId('Microsoft.Network/dnsResolvers/outboundEndpoints', 'my-dns-resolver', 'outbound-dns')]", + "[resourceId('Microsoft.Network/virtualNetworks', 'mynet')]" + ) - Expect.containsAll - dnsRuleset.["dependsOn"] - expectedRulesetDeps - "Incorrect number of dependencies on ruleset." + Expect.containsAll + dnsRuleset.["dependsOn"] + expectedRulesetDeps + "Incorrect number of dependencies on ruleset." - let dnsOutboundEndpointId = - dnsRuleset.SelectToken "properties.dnsResolverOutboundEndpoints[0].id" + let dnsOutboundEndpointId = + dnsRuleset.SelectToken "properties.dnsResolverOutboundEndpoints[0].id" - Expect.equal - (dnsOutboundEndpointId |> string) - "[resourceId('Microsoft.Network/dnsResolvers/outboundEndpoints', 'my-dns-resolver', 'outbound-dns')]" - "Incorrect resolver outbound id on ruleset" + Expect.equal + (dnsOutboundEndpointId |> string) + "[resourceId('Microsoft.Network/dnsResolvers/outboundEndpoints', 'my-dns-resolver', 'outbound-dns')]" + "Incorrect resolver outbound id on ruleset" - // Rule - let rule1 = jobj.SelectToken "resources[?(@name=='route-dns-requests/rule-1')]" - Expect.isNotNull rule1 "DNS forwarding rule 'rule-1' missing from template" + // Rule + let rule1 = jobj.SelectToken "resources[?(@name=='route-dns-requests/rule-1')]" + Expect.isNotNull rule1 "DNS forwarding rule 'rule-1' missing from template" - let expectedRuleDeps = - JArray("[resourceId('Microsoft.Network/dnsForwardingRulesets', 'route-dns-requests')]") + let expectedRuleDeps = + JArray("[resourceId('Microsoft.Network/dnsForwardingRulesets', 'route-dns-requests')]") - Expect.containsAll rule1.["dependsOn"] expectedRuleDeps "Missing dependencies for rule 'rule-1'." + Expect.containsAll rule1.["dependsOn"] expectedRuleDeps "Missing dependencies for rule 'rule-1'." - Expect.equal - (rule1.SelectToken "properties.domainName" |> string) - "example.com." - "Incorrect domain name on 'rule-1'" + Expect.equal + (rule1.SelectToken "properties.domainName" |> string) + "example.com." + "Incorrect domain name on 'rule-1'" - Expect.equal - (rule1.SelectToken "properties.targetDnsServers[0].ipAddress" |> string) - "192.168.100.74" - "Incorrect targetDnsServer ipAddress on 'rule-1'" + Expect.equal + (rule1.SelectToken "properties.targetDnsServers[0].ipAddress" |> string) + "192.168.100.74" + "Incorrect targetDnsServer ipAddress on 'rule-1'" - Expect.equal - (rule1.SelectToken "properties.targetDnsServers[0].port" |> int) - 53 - "Incorrect targetDnsServer port on 'rule-1'" + Expect.equal + (rule1.SelectToken "properties.targetDnsServers[0].port" |> int) + 53 + "Incorrect targetDnsServer port on 'rule-1'" - // vNet link - let mynetLink = jobj.SelectToken "resources[?(@name=='route-dns-requests/mynet')]" - Expect.isNotNull mynetLink "DNS ruleset vnet link 'mynet' missing from template" + // vNet link + let mynetLink = jobj.SelectToken "resources[?(@name=='route-dns-requests/mynet')]" + Expect.isNotNull mynetLink "DNS ruleset vnet link 'mynet' missing from template" - let mynetLinkDeps = - JArray(JValue "[resourceId('Microsoft.Network/dnsForwardingRulesets', 'route-dns-requests')]") + let mynetLinkDeps = + JArray(JValue "[resourceId('Microsoft.Network/dnsForwardingRulesets', 'route-dns-requests')]") - Expect.containsAll mynetLink.["dependsOn"] mynetLinkDeps "Missing dependencies for vnetLink 'mynet'." + Expect.containsAll mynetLink.["dependsOn"] mynetLinkDeps "Missing dependencies for vnetLink 'mynet'." - Expect.equal - (mynetLink.SelectToken "properties.virtualNetwork.id" |> string) - "[resourceId('Microsoft.Network/virtualNetworks', 'mynet')]" - "Incorrect id on vnet link" - } - ] + Expect.equal + (mynetLink.SelectToken "properties.virtualNetwork.id" |> string) + "[resourceId('Microsoft.Network/virtualNetworks', 'mynet')]" + "Incorrect id on vnet link" + } + ] diff --git a/src/Tests/EventGrid.fs b/src/Tests/EventGrid.fs index b4ce7a744..2304a203d 100644 --- a/src/Tests/EventGrid.fs +++ b/src/Tests/EventGrid.fs @@ -8,103 +8,96 @@ open Microsoft.Rest open System let tests = - testList - "Event Grid" - [ - test "Creates topics correctly" { - let b = eventGrid { topic_name "my-topic" } :> IBuilder - let resources = b.BuildResources Location.WestEurope - let t = resources.[0] :?> Topic - Expect.equal t.Location Location.WestEurope "Incorrect location" - Expect.equal t.Name (ResourceName "my-topic") "Incorrect name" + testList "Event Grid" [ + test "Creates topics correctly" { + let b = eventGrid { topic_name "my-topic" } :> IBuilder + let resources = b.BuildResources Location.WestEurope + let t = resources.[0] :?> Topic + Expect.equal t.Location Location.WestEurope "Incorrect location" + Expect.equal t.Name (ResourceName "my-topic") "Incorrect name" + } + test "Creates a storage source correctly" { + let storage = storageAccount { name "test" } + + let grid = eventGrid { + topic_name "topic-test" + source storage } - test "Creates a storage source correctly" { - let storage = storageAccount { name "test" } - let grid = - eventGrid { - topic_name "topic-test" - source storage - } - - Expect.equal grid.Source (ResourceName "test", Topics.StorageAccount) "Invalid Source" + Expect.equal grid.Source (ResourceName "test", Topics.StorageAccount) "Invalid Source" + } + test "Creates a queue subscriber correctly" { + let storage = storageAccount { name "test" } + + let grid = eventGrid { add_queue_subscriber storage "thequeue" [ SystemEvents.Storage.BlobCreated ] } + + let sub = grid.Subscriptions.[0] + Expect.equal sub.Name (ResourceName "test-thequeue-queue") "Incorrect subscription name" + Expect.equal sub.Endpoint (EndpointType.StorageQueue(ResourceName "thequeue")) "Incorrect endpoint type" + Expect.equal sub.Destination (ResourceName "test") "Incorrect destination" + Expect.equal sub.SystemEvents [ SystemEvents.Storage.BlobCreated ] "Incorrect system events" + } + test "Creates a webhook subscriber correctly" { + let app = webApp { name "test" } + let grid = eventGrid { add_webhook_subscriber app "api/events" [] } + let sub = grid.Subscriptions.[0] + Expect.equal sub.Name (ResourceName "test-/api/events-webhook") "Incorrect subscription name" + + Expect.equal + sub.Endpoint + (EndpointType.WebHook(Uri "https://test.azurewebsites.net/api/events")) + "Incorrect endpoint type" + + Expect.equal sub.Destination (ResourceName "test") "Incorrect destination" + } + test "Creates an eventhub subscriber correctly" { + let hub = eventHub { + name "hub" + namespace_name "ns" } - test "Creates a queue subscriber correctly" { - let storage = storageAccount { name "test" } - - let grid = - eventGrid { add_queue_subscriber storage "thequeue" [ SystemEvents.Storage.BlobCreated ] } - let sub = grid.Subscriptions.[0] - Expect.equal sub.Name (ResourceName "test-thequeue-queue") "Incorrect subscription name" - Expect.equal sub.Endpoint (EndpointType.StorageQueue(ResourceName "thequeue")) "Incorrect endpoint type" - Expect.equal sub.Destination (ResourceName "test") "Incorrect destination" - Expect.equal sub.SystemEvents [ SystemEvents.Storage.BlobCreated ] "Incorrect system events" - } - test "Creates a webhook subscriber correctly" { - let app = webApp { name "test" } - let grid = eventGrid { add_webhook_subscriber app "api/events" [] } - let sub = grid.Subscriptions.[0] - Expect.equal sub.Name (ResourceName "test-/api/events-webhook") "Incorrect subscription name" - - Expect.equal - sub.Endpoint - (EndpointType.WebHook(Uri "https://test.azurewebsites.net/api/events")) - "Incorrect endpoint type" - - Expect.equal sub.Destination (ResourceName "test") "Incorrect destination" + let grid = eventGrid { add_eventhub_subscriber hub [] } + let sub = grid.Subscriptions.[0] + Expect.equal sub.Name (ResourceName "ns-hub-eventhub") "Incorrect subscription name" + Expect.equal sub.Endpoint (EndpointType.EventHub hub.Name) "Incorrect endpoint type" + Expect.equal sub.Destination hub.EventHubNamespaceName "Incorrect destination" + } + test "Creates a service bus queue subscriber correctly" { + let q = queue { name "queuequeue" } + + let bus = serviceBus { + name "busbus" + add_queues [ q ] } - test "Creates an eventhub subscriber correctly" { - let hub = - eventHub { - name "hub" - namespace_name "ns" - } - - let grid = eventGrid { add_eventhub_subscriber hub [] } - let sub = grid.Subscriptions.[0] - Expect.equal sub.Name (ResourceName "ns-hub-eventhub") "Incorrect subscription name" - Expect.equal sub.Endpoint (EndpointType.EventHub hub.Name) "Incorrect endpoint type" - Expect.equal sub.Destination hub.EventHubNamespaceName "Incorrect destination" - } - test "Creates a service bus queue subscriber correctly" { - let q = queue { name "queuequeue" } - let bus = - serviceBus { - name "busbus" - add_queues [ q ] - } + let grid = eventGrid { add_servicebus_queue_subscriber bus q [] } + let sub = grid.Subscriptions.[0] + Expect.equal sub.Name (ResourceName "queuequeue-busbus-servicebus-queue") "Incorrect subscription name" - let grid = eventGrid { add_servicebus_queue_subscriber bus q [] } - let sub = grid.Subscriptions.[0] - Expect.equal sub.Name (ResourceName "queuequeue-busbus-servicebus-queue") "Incorrect subscription name" + Expect.equal + sub.Endpoint + (EndpointType.ServiceBus(ServiceBusEndpointType.Queue { Queue = q.Name; Bus = bus.Name })) + "Incorrect endpoint type" - Expect.equal - sub.Endpoint - (EndpointType.ServiceBus(ServiceBusEndpointType.Queue { Queue = q.Name; Bus = bus.Name })) - "Incorrect endpoint type" + Expect.equal sub.Destination q.Name "Incorrect destination" + } + test "Creates a service bus topic subscriber correctly" { + let t = topic { name "topictopic" } - Expect.equal sub.Destination q.Name "Incorrect destination" + let bus = serviceBus { + name "busbus" + add_topics [ t ] } - test "Creates a service bus topic subscriber correctly" { - let t = topic { name "topictopic" } - - let bus = - serviceBus { - name "busbus" - add_topics [ t ] - } - let grid = eventGrid { add_servicebus_topic_subscriber bus t [] } - let sub = grid.Subscriptions.[0] - Expect.equal sub.Name (ResourceName "topictopic-busbus-servicebus-topic") "Incorrect subscription name" + let grid = eventGrid { add_servicebus_topic_subscriber bus t [] } + let sub = grid.Subscriptions.[0] + Expect.equal sub.Name (ResourceName "topictopic-busbus-servicebus-topic") "Incorrect subscription name" - Expect.equal - sub.Endpoint - (EndpointType.ServiceBus(ServiceBusEndpointType.Topic { Topic = t.Name; Bus = bus.Name })) - "Incorrect endpoint type" + Expect.equal + sub.Endpoint + (EndpointType.ServiceBus(ServiceBusEndpointType.Topic { Topic = t.Name; Bus = bus.Name })) + "Incorrect endpoint type" - Expect.equal sub.Destination t.Name "Incorrect destination" - } - ] + Expect.equal sub.Destination t.Name "Incorrect destination" + } + ] diff --git a/src/Tests/EventHub.fs b/src/Tests/EventHub.fs index 99f95dcb5..40f98df3d 100644 --- a/src/Tests/EventHub.fs +++ b/src/Tests/EventHub.fs @@ -6,33 +6,30 @@ open Farmer.Builders open Farmer.EventHub let tests = - testList - "EventHub" - [ - test "Gets key on a Hub correctly" { - let hub = eventHub { name "foo" } + testList "EventHub" [ + test "Gets key on a Hub correctly" { + let hub = eventHub { name "foo" } - Expect.equal - hub.DefaultKey.Owner.Value.ArmExpression.Value - "resourceId('Microsoft.EventHub/namespaces/eventhubs', 'foo')" - "Incorrect owner" + Expect.equal + hub.DefaultKey.Owner.Value.ArmExpression.Value + "resourceId('Microsoft.EventHub/namespaces/eventhubs', 'foo')" + "Incorrect owner" - Expect.equal - hub.DefaultKey.Value - "listkeys(resourceId('Microsoft.EventHub/namespaces/AuthorizationRules', 'foo-ns', 'RootManageSharedAccessKey'), '2017-04-01').primaryConnectionString" - "Incorrect key" + Expect.equal + hub.DefaultKey.Value + "listkeys(resourceId('Microsoft.EventHub/namespaces/AuthorizationRules', 'foo-ns', 'RootManageSharedAccessKey'), '2017-04-01').primaryConnectionString" + "Incorrect key" + } + test "Does not explicitly create default consumer group" { + let hub = eventHub { + name "test-event-hub" + // When using Basic tier, attempting to explicitly create a "$Default" consumer group + // will give an error because Basic doesn't support creating consumer groups. + sku EventHubSku.Basic } - test "Does not explicitly create default consumer group" { - let hub = - eventHub { - name "test-event-hub" - // When using Basic tier, attempting to explicitly create a "$Default" consumer group - // will give an error because Basic doesn't support creating consumer groups. - sku EventHubSku.Basic - } - let defaultResourceName = ResourceName "$Default" - let defaultConsumerGroupExists = hub.ConsumerGroups.Contains defaultResourceName - Expect.isFalse defaultConsumerGroupExists "Created a default consumer group" - } - ] + let defaultResourceName = ResourceName "$Default" + let defaultConsumerGroupExists = hub.ConsumerGroups.Contains defaultResourceName + Expect.isFalse defaultConsumerGroupExists "Created a default consumer group" + } + ] diff --git a/src/Tests/ExpressRoute.fs b/src/Tests/ExpressRoute.fs index 05267aa1f..1fd305ea5 100644 --- a/src/Tests/ExpressRoute.fs +++ b/src/Tests/ExpressRoute.fs @@ -14,143 +14,133 @@ let client = new NetworkManagementClient(Uri "http://management.azure.com", TokenCredentials "NotNullOrWhiteSpace") let tests = - testList - "ExpressRoute" - [ - test "Can create a basic ExR" { - let resource = - let er = - expressRoute { - name "my-circuit" - service_provider "My ISP" - peering_location "My ISP's Location" - } - - arm { add_resource er } - |> findAzureResources client.SerializationSettings - |> List.head - - Expect.equal resource.Name "my-circuit" "" - Expect.equal resource.Sku.Name "Standard_MeteredData" "" - Expect.equal resource.Sku.Family "MeteredData" "" - Expect.equal resource.Sku.Tier "Standard" "" - Expect.equal resource.ServiceProviderProperties.BandwidthInMbps (Nullable 50) "" - Expect.equal resource.ServiceProviderProperties.ServiceProviderName "My ISP" "" - Expect.equal resource.ServiceProviderProperties.PeeringLocation "My ISP's Location" "" - Expect.equal resource.GlobalReachEnabled (Nullable false) "" - } - - test "Can create an ExR with one private peering and one authorization" { - let er = - expressRoute { - name "my-circuit" - service_provider "My ISP" - peering_location "My ISP's Location" - - add_peerings - [ - peering { - azure_asn 65412 - peer_asn 39917L - vlan 199 - primary_prefix (IPAddressCidr.parse "10.99.250.0/30") - secondary_prefix (IPAddressCidr.parse "10.99.250.4/30") - } - ] - - add_authorizations [ "myauth" ] - enable_global_reach - } - - let deployment = - arm { - add_resource er - output "auth-key" (er.AuthorizationKey "myauth") + testList "ExpressRoute" [ + test "Can create a basic ExR" { + let resource = + let er = expressRoute { + name "my-circuit" + service_provider "My ISP" + peering_location "My ISP's Location" + } + + arm { add_resource er } + |> findAzureResources client.SerializationSettings + |> List.head + + Expect.equal resource.Name "my-circuit" "" + Expect.equal resource.Sku.Name "Standard_MeteredData" "" + Expect.equal resource.Sku.Family "MeteredData" "" + Expect.equal resource.Sku.Tier "Standard" "" + Expect.equal resource.ServiceProviderProperties.BandwidthInMbps (Nullable 50) "" + Expect.equal resource.ServiceProviderProperties.ServiceProviderName "My ISP" "" + Expect.equal resource.ServiceProviderProperties.PeeringLocation "My ISP's Location" "" + Expect.equal resource.GlobalReachEnabled (Nullable false) "" + } + + test "Can create an ExR with one private peering and one authorization" { + let er = expressRoute { + name "my-circuit" + service_provider "My ISP" + peering_location "My ISP's Location" + + add_peerings [ + peering { + azure_asn 65412 + peer_asn 39917L + vlan 199 + primary_prefix (IPAddressCidr.parse "10.99.250.0/30") + secondary_prefix (IPAddressCidr.parse "10.99.250.4/30") } + ] - let circuit = - deployment - |> findAzureResources client.SerializationSettings - |> List.head - - Expect.hasLength circuit.Peerings 1 "Circuit has incorrect number of peerings" - Expect.equal circuit.Peerings.[0].AzureASN (Nullable 65412) "" - Expect.equal circuit.Peerings.[0].PeerASN (Nullable 39917L) "" - Expect.equal circuit.Peerings.[0].VlanId (Nullable 199) "" - Expect.equal circuit.Peerings.[0].PrimaryPeerAddressPrefix "10.99.250.0/30" "" - - let auth = - deployment - |> findAzureResources client.SerializationSettings - |> List.item 1 - - Expect.equal auth.Name "my-circuit/myauth" "Missing authorization in request" - - Expect.hasLength deployment.Outputs 1 "Missing deployment output for authorization key" + add_authorizations [ "myauth" ] + enable_global_reach + } - Expect.equal - deployment.Outputs.["auth-key"] - "[reference(resourceId('Microsoft.Network/expressRouteCircuits/authorizations', 'my-circuit', 'myauth')).authorizationKey]" - "Incorrect auth key reference" + let deployment = arm { + add_resource er + output "auth-key" (er.AuthorizationKey "myauth") } - test "Can create an ExR with global reach, premium tier, unlimited data" { - let resource = - let er = - expressRoute { - name "my-circuit" - service_provider "My ISP" - peering_location "My ISP's Location" - tier Premium - family UnlimitedData - - add_peerings - [ - peering { - azure_asn 65412 - peer_asn 39917L - vlan 199 - primary_prefix (IPAddressCidr.parse "10.99.250.0/30") - secondary_prefix (IPAddressCidr.parse "10.99.250.4/30") - } - ] - - enable_global_reach + let circuit = + deployment + |> findAzureResources client.SerializationSettings + |> List.head + + Expect.hasLength circuit.Peerings 1 "Circuit has incorrect number of peerings" + Expect.equal circuit.Peerings.[0].AzureASN (Nullable 65412) "" + Expect.equal circuit.Peerings.[0].PeerASN (Nullable 39917L) "" + Expect.equal circuit.Peerings.[0].VlanId (Nullable 199) "" + Expect.equal circuit.Peerings.[0].PrimaryPeerAddressPrefix "10.99.250.0/30" "" + + let auth = + deployment + |> findAzureResources client.SerializationSettings + |> List.item 1 + + Expect.equal auth.Name "my-circuit/myauth" "Missing authorization in request" + + Expect.hasLength deployment.Outputs 1 "Missing deployment output for authorization key" + + Expect.equal + deployment.Outputs.["auth-key"] + "[reference(resourceId('Microsoft.Network/expressRouteCircuits/authorizations', 'my-circuit', 'myauth')).authorizationKey]" + "Incorrect auth key reference" + } + + test "Can create an ExR with global reach, premium tier, unlimited data" { + let resource = + let er = expressRoute { + name "my-circuit" + service_provider "My ISP" + peering_location "My ISP's Location" + tier Premium + family UnlimitedData + + add_peerings [ + peering { + azure_asn 65412 + peer_asn 39917L + vlan 199 + primary_prefix (IPAddressCidr.parse "10.99.250.0/30") + secondary_prefix (IPAddressCidr.parse "10.99.250.4/30") } - - arm { add_resource er } - |> findAzureResources client.SerializationSettings - |> List.head - - Expect.equal resource.Sku.Name "Premium_UnlimitedData" "" - Expect.equal resource.Sku.Family "UnlimitedData" "" - Expect.equal resource.Sku.Tier "Premium" "" - Expect.equal resource.GlobalReachEnabled (Nullable true) "" + ] + + enable_global_reach + } + + arm { add_resource er } + |> findAzureResources client.SerializationSettings + |> List.head + + Expect.equal resource.Sku.Name "Premium_UnlimitedData" "" + Expect.equal resource.Sku.Family "UnlimitedData" "" + Expect.equal resource.Sku.Tier "Premium" "" + Expect.equal resource.GlobalReachEnabled (Nullable true) "" + } + + test "ExR service key output expression" { + let er = expressRoute { + name "my-circuit" + service_provider "My ISP" + peering_location "My ISP's Location" } - test "ExR service key output expression" { - let er = - expressRoute { - name "my-circuit" - service_provider "My ISP" - peering_location "My ISP's Location" - } - - let deployment = - arm { - add_resource er - output "er-service-key" er.ServiceKey - } - - let json = deployment.Template |> Writer.toJson - let jobj = JObject.Parse(json) - let serviceKey = jobj.SelectToken("outputs.er-service-key.value") - - Expect.equal - serviceKey - (JValue.CreateString - "[reference(resourceId('Microsoft.Network/expressRouteCircuits', 'my-circuit')).serviceKey]" - :> JToken) - "Incorrect expression generated for serviceKey" + let deployment = arm { + add_resource er + output "er-service-key" er.ServiceKey } - ] + + let json = deployment.Template |> Writer.toJson + let jobj = JObject.Parse(json) + let serviceKey = jobj.SelectToken("outputs.er-service-key.value") + + Expect.equal + serviceKey + (JValue.CreateString + "[reference(resourceId('Microsoft.Network/expressRouteCircuits', 'my-circuit')).serviceKey]" + :> JToken) + "Incorrect expression generated for serviceKey" + } + ] diff --git a/src/Tests/Functions.fs b/src/Tests/Functions.fs index d486e2b29..751b0fc9d 100644 --- a/src/Tests/Functions.fs +++ b/src/Tests/Functions.fs @@ -27,764 +27,721 @@ let getResourceAtIndex o = let getResources (b: #IBuilder) = b.BuildResources Location.WestEurope let tests = - testList - "Functions tests" - [ - test "Renames storage account correctly" { - let f = - functions { - name "test" - storage_account_name "foo" - } - - let resources = getResources f - let site = resources.[3] :?> Web.Site - let storage = resources.[1] :?> Storage.StorageAccount - - Expect.contains - site.Dependencies - (storageAccounts.resourceId "foo") - "Storage account has not been added a dependency" - - Expect.equal f.StorageAccountId.Name.Value "foo" "Incorrect storage account name on site" - Expect.equal storage.Name.ResourceName.Value "foo" "Incorrect storage account name" + testList "Functions tests" [ + test "Renames storage account correctly" { + let f = functions { + name "test" + storage_account_name "foo" } - test "Implicitly sets dependency on connection string" { - let db = sqlDb { name "mySql" } - - let sql = - sqlServer { - name "test2" - admin_username "isaac" - add_databases [ db ] - } - let f = - functions { - name "test" - storage_account_name "foo" - setting "db" (sql.ConnectionString db) - } - :> IBuilder - - let site = f.BuildResources Location.NorthEurope |> List.item 3 :?> Web.Site - - Expect.contains - site.Dependencies - (ResourceId.create (Sql.databases, ResourceName "test2", ResourceName "mySql")) - "Missing dependency" + let resources = getResources f + let site = resources.[3] :?> Web.Site + let storage = resources.[1] :?> Storage.StorageAccount + + Expect.contains + site.Dependencies + (storageAccounts.resourceId "foo") + "Storage account has not been added a dependency" + + Expect.equal f.StorageAccountId.Name.Value "foo" "Incorrect storage account name on site" + Expect.equal storage.Name.ResourceName.Value "foo" "Incorrect storage account name" + } + test "Implicitly sets dependency on connection string" { + let db = sqlDb { name "mySql" } + + let sql = sqlServer { + name "test2" + admin_username "isaac" + add_databases [ db ] } - test "Works with unmanaged storage account" { - let externalStorageAccount = - ResourceId.create (storageAccounts, ResourceName "foo", "group") - - let functionsBuilder = - functions { - name "test" - link_to_unmanaged_storage_account externalStorageAccount - } - - let f = functionsBuilder :> IBuilder - let resources = getResources f - let site = resources |> List.item 2 :?> Web.Site - - Expect.isFalse - (resources |> List.exists (fun r -> r.ResourceId.Type = storageAccounts)) - "Storage Account should not exist" - Expect.isFalse (site.Dependencies |> Set.contains externalStorageAccount) "Should not be a dependency" - let settings = Expect.wantSome site.AppSettings "AppSettings should be set" - - Expect.stringContains - settings.["AzureWebJobsStorage"].Value - "foo" - "Web Jobs Storage setting should have storage account name" - - Expect.stringContains - settings.["AzureWebJobsDashboard"].Value - "foo" - "Web Jobs Dashboard setting should have storage account name" + let f = + functions { + name "test" + storage_account_name "foo" + setting "db" (sql.ConnectionString db) + } + :> IBuilder + + let site = f.BuildResources Location.NorthEurope |> List.item 3 :?> Web.Site + + Expect.contains + site.Dependencies + (ResourceId.create (Sql.databases, ResourceName "test2", ResourceName "mySql")) + "Missing dependency" + } + test "Works with unmanaged storage account" { + let externalStorageAccount = + ResourceId.create (storageAccounts, ResourceName "foo", "group") + + let functionsBuilder = functions { + name "test" + link_to_unmanaged_storage_account externalStorageAccount } - test "Handles identity correctly" { - let f: Site = functions { name "testfunc" } |> getResourceAtIndex 0 - Expect.isNull f.Identity "Default managed identity should be null" - - let f: Site = - functions { - name "func2" - system_identity - } - |> getResourceAtIndex 3 - - Expect.equal - f.Identity.Type - (Nullable ManagedServiceIdentityType.SystemAssigned) - "Should have system identity" - - Expect.isNull f.Identity.UserAssignedIdentities "Should have no user assigned identities" - - let f: Site = - functions { - name "func3" - system_identity - add_identity (createUserAssignedIdentity "test") - add_identity (createUserAssignedIdentity "test2") - } - |> getResourceAtIndex 3 - - Expect.equal - f.Identity.Type - (Nullable ManagedServiceIdentityType.SystemAssignedUserAssigned) - "Should have system identity" - - Expect.sequenceEqual - (f.Identity.UserAssignedIdentities |> Seq.map (fun r -> r.Key)) - [ - "[resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', 'test2')]" - "[resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', 'test')]" - ] - "Should have two user assigned identities" + let f = functionsBuilder :> IBuilder + let resources = getResources f + let site = resources |> List.item 2 :?> Web.Site + + Expect.isFalse + (resources |> List.exists (fun r -> r.ResourceId.Type = storageAccounts)) + "Storage Account should not exist" + + Expect.isFalse (site.Dependencies |> Set.contains externalStorageAccount) "Should not be a dependency" + let settings = Expect.wantSome site.AppSettings "AppSettings should be set" + + Expect.stringContains + settings.["AzureWebJobsStorage"].Value + "foo" + "Web Jobs Storage setting should have storage account name" + + Expect.stringContains + settings.["AzureWebJobsDashboard"].Value + "foo" + "Web Jobs Dashboard setting should have storage account name" + } + test "Handles identity correctly" { + let f: Site = functions { name "testfunc" } |> getResourceAtIndex 0 + Expect.isNull f.Identity "Default managed identity should be null" + + let f: Site = + functions { + name "func2" + system_identity + } + |> getResourceAtIndex 3 + + Expect.equal + f.Identity.Type + (Nullable ManagedServiceIdentityType.SystemAssigned) + "Should have system identity" + + Expect.isNull f.Identity.UserAssignedIdentities "Should have no user assigned identities" + + let f: Site = + functions { + name "func3" + system_identity + add_identity (createUserAssignedIdentity "test") + add_identity (createUserAssignedIdentity "test2") + } + |> getResourceAtIndex 3 + + Expect.equal + f.Identity.Type + (Nullable ManagedServiceIdentityType.SystemAssignedUserAssigned) + "Should have system identity" + + Expect.sequenceEqual + (f.Identity.UserAssignedIdentities |> Seq.map (fun r -> r.Key)) + [ + "[resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', 'test2')]" + "[resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', 'test')]" + ] + "Should have two user assigned identities" + + } + + test "Supports always on" { + let f: Site = functions { name "testfunc" } |> getResourceAtIndex 3 + Expect.equal f.SiteConfig.AlwaysOn (Nullable false) "always on should be false by default" + + let f: Site = + functions { + name "func2" + always_on + } + |> getResourceAtIndex 3 + + Expect.equal f.SiteConfig.AlwaysOn (Nullable true) "always on should be true" + } + + test "Supports 32 and 64 bit worker processes" { + let f: Site = + functions { + name "func" + worker_process Bitness.Bits32 + } + |> getResourceAtIndex 3 + + Expect.equal f.SiteConfig.Use32BitWorkerProcess (Nullable true) "Should use 32 bit worker process" + + let f: Site = + functions { + name "func2" + worker_process Bitness.Bits64 + } + |> getResourceAtIndex 3 + + Expect.equal f.SiteConfig.Use32BitWorkerProcess (Nullable false) "Should not use 32 bit worker process" + } + + test "Managed KV integration works correctly" { + let sa = storageAccount { name "teststorage" } + + let wa = functions { + name "testfunc" + setting "storage" sa.Key + secret_setting "secret" + setting "literal" "value" + link_to_keyvault (ResourceName "testfuncvault") } - test "Supports always on" { - let f: Site = functions { name "testfunc" } |> getResourceAtIndex 3 - Expect.equal f.SiteConfig.AlwaysOn (Nullable false) "always on should be false by default" - - let f: Site = - functions { - name "func2" - always_on - } - |> getResourceAtIndex 3 - - Expect.equal f.SiteConfig.AlwaysOn (Nullable true) "always on should be true" + let vault = keyVault { + name "testfuncvault" + add_access_policy (AccessPolicy.create (wa.SystemIdentity.PrincipalId, [ KeyVault.Secret.Get ])) } - test "Supports 32 and 64 bit worker processes" { - let f: Site = - functions { - name "func" - worker_process Bitness.Bits32 - } - |> getResourceAtIndex 3 - - Expect.equal f.SiteConfig.Use32BitWorkerProcess (Nullable true) "Should use 32 bit worker process" - - let f: Site = - functions { - name "func2" - worker_process Bitness.Bits64 - } - |> getResourceAtIndex 3 - - Expect.equal f.SiteConfig.Use32BitWorkerProcess (Nullable false) "Should not use 32 bit worker process" + let vault = vault |> getResources |> getResource |> List.head + let secrets = wa |> getResources |> getResource + let site = wa |> getResources |> getResource |> List.head + + let expectedSettings = + Map [ + "storage", + LiteralSetting + "@Microsoft.KeyVault(SecretUri=https://testfuncvault.vault.azure.net/secrets/storage)" + "secret", + LiteralSetting "@Microsoft.KeyVault(SecretUri=https://testfuncvault.vault.azure.net/secrets/secret)" + "literal", LiteralSetting "value" + ] + + Expect.equal site.Identity.SystemAssigned Enabled "System Identity should be enabled" + let settings = Expect.wantSome site.AppSettings "AppSettings should be set" + Expect.containsAll settings expectedSettings "Incorrect settings" + + Expect.equal wa.CommonWebConfig.Identity.SystemAssigned Enabled "System Identity should be turned on" + + Expect.hasLength secrets 2 "Incorrect number of KV secrets" + + Expect.equal secrets.[0].Name.Value "testfuncvault/secret" "Incorrect secret name" + Expect.equal secrets.[0].Value (ParameterSecret(SecureParameter "secret")) "Incorrect secret value" + + Expect.sequenceEqual + secrets.[0].Dependencies + [ vaults.resourceId "testfuncvault" ] + "Incorrect secret dependencies" + + Expect.equal secrets.[1].Name.Value "testfuncvault/storage" "Incorrect secret name" + Expect.equal secrets.[1].Value (ExpressionSecret sa.Key) "Incorrect secret value" + + Expect.sequenceEqual + secrets.[1].Dependencies + [ vaults.resourceId "testfuncvault"; storageAccounts.resourceId "teststorage" ] + "Incorrect secret dependencies" + } + + test "Supports dotnet-isolated runtime" { + let f = functions { + name "func" + use_runtime (FunctionsRuntime.DotNetIsolated) } - test "Managed KV integration works correctly" { - let sa = storageAccount { name "teststorage" } - - let wa = - functions { - name "testfunc" - setting "storage" sa.Key - secret_setting "secret" - setting "literal" "value" - link_to_keyvault (ResourceName "testfuncvault") - } + let resources = (f :> IBuilder).BuildResources Location.WestEurope + let site = resources.[3] :?> Web.Site + let settings = Expect.wantSome site.AppSettings "AppSettings should be set" - let vault = - keyVault { - name "testfuncvault" - add_access_policy (AccessPolicy.create (wa.SystemIdentity.PrincipalId, [ KeyVault.Secret.Get ])) - } - - let vault = vault |> getResources |> getResource |> List.head - let secrets = wa |> getResources |> getResource - let site = wa |> getResources |> getResource |> List.head - - let expectedSettings = - Map - [ - "storage", - LiteralSetting - "@Microsoft.KeyVault(SecretUri=https://testfuncvault.vault.azure.net/secrets/storage)" - "secret", - LiteralSetting - "@Microsoft.KeyVault(SecretUri=https://testfuncvault.vault.azure.net/secrets/secret)" - "literal", LiteralSetting "value" - ] - - Expect.equal site.Identity.SystemAssigned Enabled "System Identity should be enabled" - let settings = Expect.wantSome site.AppSettings "AppSettings should be set" - Expect.containsAll settings expectedSettings "Incorrect settings" - - Expect.equal wa.CommonWebConfig.Identity.SystemAssigned Enabled "System Identity should be turned on" - - Expect.hasLength secrets 2 "Incorrect number of KV secrets" - - Expect.equal secrets.[0].Name.Value "testfuncvault/secret" "Incorrect secret name" - Expect.equal secrets.[0].Value (ParameterSecret(SecureParameter "secret")) "Incorrect secret value" - - Expect.sequenceEqual - secrets.[0].Dependencies - [ vaults.resourceId "testfuncvault" ] - "Incorrect secret dependencies" - - Expect.equal secrets.[1].Name.Value "testfuncvault/storage" "Incorrect secret name" - Expect.equal secrets.[1].Value (ExpressionSecret sa.Key) "Incorrect secret value" - - Expect.sequenceEqual - secrets.[1].Dependencies - [ vaults.resourceId "testfuncvault"; storageAccounts.resourceId "teststorage" ] - "Incorrect secret dependencies" - } - - test "Supports dotnet-isolated runtime" { - let f = - functions { - name "func" - use_runtime (FunctionsRuntime.DotNetIsolated) - } + Expect.equal + settings.["FUNCTIONS_WORKER_RUNTIME"] + (LiteralSetting "dotnet-isolated") + "Should use dotnet-isolated functions runtime" + } + test "Sets LinuxFxVersion correctly for dotnet runtimes" { + let getLinuxFxVersion f = let resources = (f :> IBuilder).BuildResources Location.WestEurope let site = resources.[3] :?> Web.Site - let settings = Expect.wantSome site.AppSettings "AppSettings should be set" + site.LinuxFxVersion - Expect.equal - settings.["FUNCTIONS_WORKER_RUNTIME"] - (LiteralSetting "dotnet-isolated") - "Should use dotnet-isolated functions runtime" + let f = functions { + name "func" + use_runtime (FunctionsRuntime.DotNet50Isolated) + operating_system Linux } - test "Sets LinuxFxVersion correctly for dotnet runtimes" { - let getLinuxFxVersion f = - let resources = (f :> IBuilder).BuildResources Location.WestEurope - let site = resources.[3] :?> Web.Site - site.LinuxFxVersion - - let f = - functions { - name "func" - use_runtime (FunctionsRuntime.DotNet50Isolated) - operating_system Linux - } - - Expect.equal (getLinuxFxVersion f) (Some "DOTNET-ISOLATED|5.0") "Should set linux fx runtime" - - let f = - functions { - name "func" - use_runtime (FunctionsRuntime.DotNet60Isolated) - operating_system Linux - } - - Expect.equal (getLinuxFxVersion f) (Some "DOTNET-ISOLATED|6.0") "Should set linux fx runtime" - - let f = - functions { - name "func" - use_runtime (FunctionsRuntime.DotNetCore31) - operating_system Linux - } - - Expect.equal (getLinuxFxVersion f) (Some "DOTNETCORE|3.1") "Should set linux fx runtime" + Expect.equal (getLinuxFxVersion f) (Some "DOTNET-ISOLATED|5.0") "Should set linux fx runtime" + let f = functions { + name "func" + use_runtime (FunctionsRuntime.DotNet60Isolated) + operating_system Linux } - test "FunctionsApp supports adding slots" { - let slot = appSlot { name "warm-up" } - - let site = - functions { - name "func" - add_slot slot - } - - Expect.isTrue (site.CommonWebConfig.Slots.ContainsKey "warm-up") "config should contain slot" - - let slots = - site - |> getResources - |> getResource - |> List.filter (fun s -> s.ResourceType = Arm.Web.slots) + Expect.equal (getLinuxFxVersion f) (Some "DOTNET-ISOLATED|6.0") "Should set linux fx runtime" - Expect.hasLength slots 1 "Should only be 1 slot" + let f = functions { + name "func" + use_runtime (FunctionsRuntime.DotNetCore31) + operating_system Linux } - test "Functions App with slot that has system assigned identity adds identity to slot" { - let slot = - appSlot { - name "warm-up" - system_identity - } + Expect.equal (getLinuxFxVersion f) (Some "DOTNETCORE|3.1") "Should set linux fx runtime" - let site = - functions { - name "func" - add_slot slot - } - - Expect.isTrue (site.CommonWebConfig.Slots.ContainsKey "warm-up") "Config should contain slot" + } - let slots = - site - |> getResources - |> getResource - |> List.filter (fun s -> s.ResourceType = Arm.Web.slots) - // Default "production" slot is not included as it is created automatically in Azure - Expect.hasLength slots 1 "Should only be 1 slot" - - let expected = - { - SystemAssigned = Enabled - UserAssigned = [] - } + test "FunctionsApp supports adding slots" { + let slot = appSlot { name "warm-up" } - Expect.equal (slots.Item 0).Identity expected "Slot should have slot setting" + let site = functions { + name "func" + add_slot slot } - test "Functions App with slot adds settings to slot" { - let slot = appSlot { name "warm-up" } + Expect.isTrue (site.CommonWebConfig.Slots.ContainsKey "warm-up") "config should contain slot" - let site = - functions { - name "func" - add_slot slot - setting "setting" "some value" - } + let slots = + site + |> getResources + |> getResource + |> List.filter (fun s -> s.ResourceType = Arm.Web.slots) - Expect.isTrue (site.CommonWebConfig.Slots.ContainsKey "warm-up") "Config should contain slot" + Expect.hasLength slots 1 "Should only be 1 slot" + } - let slots = - site - |> getResources - |> getResource - |> List.filter (fun s -> s.ResourceType = Arm.Web.slots) - // Default "production" slot is not included as it is created automatically in Azure - Expect.hasLength slots 1 "Should only be 1 slot" - - let settings = Expect.wantSome slots.[0].AppSettings "AppSettings should be set" - Expect.isTrue (settings.ContainsKey("setting")) "Slot should have slot setting" + test "Functions App with slot that has system assigned identity adds identity to slot" { + let slot = appSlot { + name "warm-up" + system_identity } - test "Functions App with slot does not add settings to app service" { - let slot = appSlot { name "warm-up" } - - let config = - functions { - name "func" - add_slot slot - setting "setting" "some value" - } - - let sites = config |> getResources |> getResource - let slots = sites |> List.filter (fun s -> s.ResourceType = Arm.Web.slots) - - // Default "production" slot is not included as it is created automatically in Azure - Expect.hasLength slots 1 "Should only be 1 slot" - - Expect.isNone (sites.[0].AppSettings) "App service should not have any settings" - Expect.isNone (sites.[0].ConnectionStrings) "App service should not have any connection strings" + let site = functions { + name "func" + add_slot slot } - test "Functions App adds literal settings to slots" { - let slot = appSlot { name "warm-up" } + Expect.isTrue (site.CommonWebConfig.Slots.ContainsKey "warm-up") "Config should contain slot" - let site = - functions { - name "func" - add_slot slot - operating_system Windows - } - - Expect.isTrue (site.CommonWebConfig.Slots.ContainsKey "warm-up") "Config should contain slot" + let slots = + site + |> getResources + |> getResource + |> List.filter (fun s -> s.ResourceType = Arm.Web.slots) + // Default "production" slot is not included as it is created automatically in Azure + Expect.hasLength slots 1 "Should only be 1 slot" - let slots = - site - |> getResources - |> getResource - |> List.filter (fun s -> s.ResourceType = Arm.Web.slots) - // Default "production" slot is not included as it is created automatically in Azure - Expect.hasLength slots 1 "Should only be 1 slot" - - let settings = (slots.Item 0).AppSettings |> Option.defaultValue Map.empty - - let expectation = - [ - "FUNCTIONS_WORKER_RUNTIME" - "WEBSITE_NODE_DEFAULT_VERSION" - "FUNCTIONS_EXTENSION_VERSION" - "AzureWebJobsStorage" - "AzureWebJobsDashboard" - "APPINSIGHTS_INSTRUMENTATIONKEY" - "WEBSITE_CONTENTAZUREFILECONNECTIONSTRING" - "WEBSITE_CONTENTSHARE" - ] - |> List.map (settings.ContainsKey) - - Expect.allEqual expectation true "Slot should have all literal settings" + let expected = { + SystemAssigned = Enabled + UserAssigned = [] } - test "Functions App with different settings on slot and service adds both settings to slot" { - let slot = - appSlot { - name "warm-up" - setting "slot" "slot value" - } + Expect.equal (slots.Item 0).Identity expected "Slot should have slot setting" + } - let site = - functions { - name "testfunc" - add_slot slot - setting "appService" "app service value" - } + test "Functions App with slot adds settings to slot" { + let slot = appSlot { name "warm-up" } - Expect.isTrue (site.CommonWebConfig.Slots.ContainsKey "warm-up") "Config should contain slot" - - let slots = - site - |> getResources - |> getResource - |> List.filter (fun s -> s.ResourceType = Arm.Web.slots) - // Default "production" slot is not included as it is created automatically in Azure - Expect.hasLength slots 1 "Should only be 1 slot" - - let settings = Expect.wantSome slots.[0].AppSettings "AppSettings should be set" - Expect.isTrue (settings.ContainsKey("slot")) "Slot should have slot setting" - Expect.isTrue (settings.ContainsKey("appService")) "Slot should have app service setting" + let site = functions { + name "func" + add_slot slot + setting "setting" "some value" } - test "Functions App with slot, slot settings override app service setting" { - let slot = - appSlot { - name "warm-up" - setting "override" "overridden" - } + Expect.isTrue (site.CommonWebConfig.Slots.ContainsKey "warm-up") "Config should contain slot" - let site = - functions { - name "testfunc" - add_slot slot - setting "override" "some value" - } + let slots = + site + |> getResources + |> getResource + |> List.filter (fun s -> s.ResourceType = Arm.Web.slots) + // Default "production" slot is not included as it is created automatically in Azure + Expect.hasLength slots 1 "Should only be 1 slot" - Expect.isTrue (site.CommonWebConfig.Slots.ContainsKey "warm-up") "Config should contain slot" + let settings = Expect.wantSome slots.[0].AppSettings "AppSettings should be set" + Expect.isTrue (settings.ContainsKey("setting")) "Slot should have slot setting" + } - let sites = site |> getResources |> getResource - let slots = sites |> List.filter (fun s -> s.ResourceType = Arm.Web.slots) - // Default "production" slot is not included as it is created automatically in Azure - Expect.hasLength slots 1 "Should only be 1 slot" + test "Functions App with slot does not add settings to app service" { + let slot = appSlot { name "warm-up" } - let settings = Expect.wantSome slots.[0].AppSettings "AppSettings should be set" - let (hasValue, value) = settings.TryGetValue("override") - - Expect.isTrue hasValue "Slot should have app service setting" - Expect.equal value.Value "overridden" "Slot should have correct app service value" + let config = functions { + name "func" + add_slot slot + setting "setting" "some value" } - test "Publish as docker container" { - let f = - functions { - name "func" + let sites = config |> getResources |> getResource + let slots = sites |> List.filter (fun s -> s.ResourceType = Arm.Web.slots) - publish_as ( - DockerContainer(docker (new Uri("http://www.farmer.io")) "Robert Lewandowski" "do it") - ) - } + // Default "production" slot is not included as it is created automatically in Azure + Expect.hasLength slots 1 "Should only be 1 slot" - let resources = (f :> IBuilder).BuildResources Location.WestEurope - let site = resources.[3] :?> Web.Site - let settings = Expect.wantSome site.AppSettings "AppSettings should be set" - Expect.equal settings.["DOCKER_REGISTRY_SERVER_URL"] (LiteralSetting "http://www.farmer.io/") "" - Expect.equal settings.["DOCKER_REGISTRY_SERVER_USERNAME"] (LiteralSetting "Robert Lewandowski") "" + Expect.isNone (sites.[0].AppSettings) "App service should not have any settings" + Expect.isNone (sites.[0].ConnectionStrings) "App service should not have any connection strings" + } - Expect.equal - settings.["DOCKER_REGISTRY_SERVER_PASSWORD"] - (LiteralSetting "[parameters('Robert Lewandowski-password')]") - "" + test "Functions App adds literal settings to slots" { + let slot = appSlot { name "warm-up" } - Expect.equal site.AppCommandLine (Some "do it") "" + let site = functions { + name "func" + add_slot slot + operating_system Windows } - test "Service plans support Elastic Premium functions" { - let sp = - servicePlan { - name "test" - sku WebApp.Sku.EP2 - max_elastic_workers 25 - } - - let resources = (sp :> IBuilder).BuildResources Location.WestEurope - let serverFarm = resources.[0] :?> Web.ServerFarm - - Expect.equal serverFarm.Sku (ElasticPremium "EP2") "Incorrect SKU" - Expect.equal serverFarm.Kind (Some "elastic") "Incorrect Kind" - Expect.equal serverFarm.MaximumElasticWorkerCount (Some 25) "Incorrect worker count" + Expect.isTrue (site.CommonWebConfig.Slots.ContainsKey "warm-up") "Config should contain slot" + + let slots = + site + |> getResources + |> getResource + |> List.filter (fun s -> s.ResourceType = Arm.Web.slots) + // Default "production" slot is not included as it is created automatically in Azure + Expect.hasLength slots 1 "Should only be 1 slot" + + let settings = (slots.Item 0).AppSettings |> Option.defaultValue Map.empty + + let expectation = + [ + "FUNCTIONS_WORKER_RUNTIME" + "WEBSITE_NODE_DEFAULT_VERSION" + "FUNCTIONS_EXTENSION_VERSION" + "AzureWebJobsStorage" + "AzureWebJobsDashboard" + "APPINSIGHTS_INSTRUMENTATIONKEY" + "WEBSITE_CONTENTAZUREFILECONNECTIONSTRING" + "WEBSITE_CONTENTSHARE" + ] + |> List.map (settings.ContainsKey) + + Expect.allEqual expectation true "Slot should have all literal settings" + } + + test "Functions App with different settings on slot and service adds both settings to slot" { + let slot = appSlot { + name "warm-up" + setting "slot" "slot value" } - test "Supports health check" { - let f: Site = - functions { - name "test" - health_check_path "/status" - } - |> getResourceAtIndex 3 - - Expect.equal f.SiteConfig.HealthCheckPath "/status" "Health check path should be '/status'" + let site = functions { + name "testfunc" + add_slot slot + setting "appService" "app service value" } - test "Not setting the functions name causes an error" { - Expect.throws - (fun () -> functions { storage_account_name "foo" } |> ignore) - "Not setting functions name should throw" + Expect.isTrue (site.CommonWebConfig.Slots.ContainsKey "warm-up") "Config should contain slot" + + let slots = + site + |> getResources + |> getResource + |> List.filter (fun s -> s.ResourceType = Arm.Web.slots) + // Default "production" slot is not included as it is created automatically in Azure + Expect.hasLength slots 1 "Should only be 1 slot" + + let settings = Expect.wantSome slots.[0].AppSettings "AppSettings should be set" + Expect.isTrue (settings.ContainsKey("slot")) "Slot should have slot setting" + Expect.isTrue (settings.ContainsKey("appService")) "Slot should have app service setting" + } + + test "Functions App with slot, slot settings override app service setting" { + let slot = appSlot { + name "warm-up" + setting "override" "overridden" } - test "Sets ftp state correctly in builder" { - let f = - functions { - name "test" - ftp_state FTPState.Disabled - } - :> IBuilder - - let site = f.BuildResources Location.NorthEurope |> List.item 3 :?> Web.Site - Expect.equal site.FTPState (Some FTPState.Disabled) "Incorrect FTP state set" + let site = functions { + name "testfunc" + add_slot slot + setting "override" "some value" } - test "Sets ftp state correctly to 'disabled'" { - let f = - functions { - name "test" - ftp_state FTPState.Disabled - } - :> IBuilder - - let deployment = arm { add_resource f } - let jobj = Newtonsoft.Json.Linq.JObject.Parse(deployment.Template |> Writer.toJson) + Expect.isTrue (site.CommonWebConfig.Slots.ContainsKey "warm-up") "Config should contain slot" - let ftpsStateValue = - jobj.SelectToken "resources[?(@.name=='test')].properties.siteConfig.ftpsState" - |> string - - Expect.equal - ftpsStateValue - "Disabled" - $"Incorrect value ('{ftpsStateValue}') set for ftpsState in generated template" - } - - test "Correctly supports unmanaged storage account" { - let functionsApp = - functions { - name "func" - - link_to_unmanaged_storage_account ( - ResourceId.create ( - Farmer.Arm.Storage.storageAccounts, - ResourceName "accountName", - group = "shared-group" - ) - ) - } + let sites = site |> getResources |> getResource + let slots = sites |> List.filter (fun s -> s.ResourceType = Arm.Web.slots) + // Default "production" slot is not included as it is created automatically in Azure + Expect.hasLength slots 1 "Should only be 1 slot" - let template = arm { add_resource functionsApp } - let jsn = template.Template |> Writer.toJson - let jobj = jsn |> Newtonsoft.Json.Linq.JObject.Parse + let settings = Expect.wantSome slots.[0].AppSettings "AppSettings should be set" + let (hasValue, value) = settings.TryGetValue("override") - let appSettings = - jobj.SelectTokens - "$..resources[?(@type=='Microsoft.Web/sites')].properties.siteConfig.appSettings.[*]" - |> Seq.map (fun x -> x.ToObject<{| name: string; value: string |}>()) + Expect.isTrue hasValue "Slot should have app service setting" + Expect.equal value.Value "overridden" "Slot should have correct app service value" + } - Expect.contains - appSettings - {| - name = "AzureWebJobsDashboard" - value = - "[concat('DefaultEndpointsProtocol=https;AccountName=accountName;AccountKey=', listKeys(resourceId('shared-group', 'Microsoft.Storage/storageAccounts', 'accountName'), '2017-10-01').keys[0].value)]" - |} - "Invalid value for AzureWebJobsDashboard" + test "Publish as docker container" { + let f = functions { + name "func" + publish_as (DockerContainer(docker (new Uri("http://www.farmer.io")) "Robert Lewandowski" "do it")) } - test "Correctly supports unmanaged App Insights" { - let functionsApp = - functions { - name "func" - - link_to_unmanaged_app_insights ( - ResourceId.create ( - Farmer.Arm.Insights.components, - ResourceName "theName", - group = "shared-group" - ) - ) - } + let resources = (f :> IBuilder).BuildResources Location.WestEurope + let site = resources.[3] :?> Web.Site + let settings = Expect.wantSome site.AppSettings "AppSettings should be set" + Expect.equal settings.["DOCKER_REGISTRY_SERVER_URL"] (LiteralSetting "http://www.farmer.io/") "" + Expect.equal settings.["DOCKER_REGISTRY_SERVER_USERNAME"] (LiteralSetting "Robert Lewandowski") "" + + Expect.equal + settings.["DOCKER_REGISTRY_SERVER_PASSWORD"] + (LiteralSetting "[parameters('Robert Lewandowski-password')]") + "" + + Expect.equal site.AppCommandLine (Some "do it") "" + } + + test "Service plans support Elastic Premium functions" { + let sp = servicePlan { + name "test" + sku WebApp.Sku.EP2 + max_elastic_workers 25 + } - let template = arm { add_resource functionsApp } - let jsn = template.Template |> Writer.toJson - let jobj = jsn |> Newtonsoft.Json.Linq.JObject.Parse - - let appSettings = - jobj.SelectTokens - "$..resources[?(@type=='Microsoft.Web/sites')].properties.siteConfig.appSettings.[*]" - |> 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" + let resources = (sp :> IBuilder).BuildResources Location.WestEurope + let serverFarm = resources.[0] :?> Web.ServerFarm + + Expect.equal serverFarm.Sku (ElasticPremium "EP2") "Incorrect SKU" + Expect.equal serverFarm.Kind (Some "elastic") "Incorrect Kind" + Expect.equal serverFarm.MaximumElasticWorkerCount (Some 25) "Incorrect worker count" + } + + test "Supports health check" { + let f: Site = + functions { + name "test" + health_check_path "/status" + } + |> getResourceAtIndex 3 + + Expect.equal f.SiteConfig.HealthCheckPath "/status" "Health check path should be '/status'" + } + + test "Not setting the functions name causes an error" { + Expect.throws + (fun () -> functions { storage_account_name "foo" } |> ignore) + "Not setting functions name should throw" + } + + test "Sets ftp state correctly in builder" { + let f = + functions { + name "test" + ftp_state FTPState.Disabled + } + :> IBuilder + + let site = f.BuildResources Location.NorthEurope |> List.item 3 :?> Web.Site + Expect.equal site.FTPState (Some FTPState.Disabled) "Incorrect FTP state set" + } + + test "Sets ftp state correctly to 'disabled'" { + let f = + functions { + name "test" + ftp_state FTPState.Disabled + } + :> IBuilder + + let deployment = arm { add_resource f } + let jobj = Newtonsoft.Json.Linq.JObject.Parse(deployment.Template |> Writer.toJson) + + let ftpsStateValue = + jobj.SelectToken "resources[?(@.name=='test')].properties.siteConfig.ftpsState" + |> string + + Expect.equal + ftpsStateValue + "Disabled" + $"Incorrect value ('{ftpsStateValue}') set for ftpsState in generated template" + } + + test "Correctly supports unmanaged storage account" { + let functionsApp = functions { + name "func" + + link_to_unmanaged_storage_account ( + ResourceId.create ( + Farmer.Arm.Storage.storageAccounts, + ResourceName "accountName", + group = "shared-group" + ) + ) } - test "Function app correctly adds connection strings" { - let sa = storageAccount { name "foo" } + let template = arm { add_resource functionsApp } + let jsn = template.Template |> Writer.toJson + let jobj = jsn |> Newtonsoft.Json.Linq.JObject.Parse - let wa = - let resources = - functions { - name "test" - connection_string "a" - connection_string ("b", sa.Key) - } - |> getResources + let appSettings = + jobj.SelectTokens "$..resources[?(@type=='Microsoft.Web/sites')].properties.siteConfig.appSettings.[*]" + |> Seq.map (fun x -> x.ToObject<{| name: string; value: string |}>()) - resources |> getResource |> List.head + Expect.contains + appSettings + {| + name = "AzureWebJobsDashboard" + value = + "[concat('DefaultEndpointsProtocol=https;AccountName=accountName;AccountKey=', listKeys(resourceId('shared-group', 'Microsoft.Storage/storageAccounts', 'accountName'), '2017-10-01').keys[0].value)]" + |} + "Invalid value for AzureWebJobsDashboard" - let expected = - [ - "a", (ParameterSetting(SecureParameter "a"), Custom) - "b", (ExpressionSetting sa.Key, Custom) - ] + } - let parameters = wa :> IParameters + test "Correctly supports unmanaged App Insights" { + let functionsApp = functions { + name "func" - Expect.equal wa.ConnectionStrings (Map expected |> Some) "Missing connections" - Expect.equal parameters.SecureParameters [ SecureParameter "a" ] "Missing parameter" + link_to_unmanaged_app_insights ( + ResourceId.create (Farmer.Arm.Insights.components, ResourceName "theName", group = "shared-group") + ) } - test "Supports adding ip restriction" { - let ip = IPAddressCidr.parse "1.2.3.4/32" + let template = arm { add_resource functionsApp } + let jsn = template.Template |> Writer.toJson + let jobj = jsn |> Newtonsoft.Json.Linq.JObject.Parse - let resources = - functions { - name "test" - add_allowed_ip_restriction "test-rule" ip - } - |> getResources + let appSettings = + jobj.SelectTokens "$..resources[?(@type=='Microsoft.Web/sites')].properties.siteConfig.appSettings.[*]" + |> Seq.map (fun x -> x.ToObject<{| name: string; value: string |}>()) - let site = resources |> getResource |> List.head + Expect.contains + appSettings + {| + name = "APPINSIGHTS_INSTRUMENTATIONKEY" + value = + "[reference(resourceId('shared-group', 'Microsoft.Insights/components', 'theName'), '2014-04-01').InstrumentationKey]" + |} + "Invalid value for APPINSIGHTS_INSTRUMENTATIONKEY" + } - 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" + test "Function app correctly adds connection strings" { + let sa = storageAccount { name "foo" } + let wa = let resources = functions { name "test" - add_denied_ip_restriction "test-rule" ip + connection_string "a" + connection_string ("b", sa.Key) } |> getResources - let site = resources |> getResource |> List.head + resources |> getResource |> List.head - let expectedRestriction = IpSecurityRestriction.Create "test-rule" ip Deny + let expected = [ + "a", (ParameterSetting(SecureParameter "a"), Custom) + "b", (ExpressionSetting sa.Key, Custom) + ] - 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 parameters = wa :> IParameters - let resources = - functions { - name "test" - add_slot warmupSlot - add_allowed_ip_restriction "site-rule" siteIp - } - |> getResources + Expect.equal wa.ConnectionStrings (Map expected |> Some) "Missing connections" + Expect.equal parameters.SecureParameters [ SecureParameter "a" ] "Missing parameter" + } - let slot = - resources - |> getResource - |> List.filter (fun x -> x.ResourceType = Arm.Web.slots) - |> List.head + test "Supports adding ip restriction" { + let ip = IPAddressCidr.parse "1.2.3.4/32" - let site = resources |> getResource |> List.head + let resources = + functions { + name "test" + add_allowed_ip_restriction "test-rule" ip + } + |> getResources - let expectedSlotRestriction = IpSecurityRestriction.Create "slot-rule" slotIp Allow - let expectedSiteRestriction = IpSecurityRestriction.Create "site-rule" siteIp Allow + let site = resources |> getResource |> List.head - Expect.equal - slot.IpSecurityRestrictions - [ expectedSlotRestriction ] - "Slot should have correct allowed ip security restriction" + let expectedRestriction = IpSecurityRestriction.Create "test-rule" ip Allow - 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") + 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 asp = serverFarms.resourceId "my-asp" + let resources = + functions { + name "test" + add_denied_ip_restriction "test-rule" ip + } + |> getResources - let wa = - functions { - name "testApp" - link_to_unmanaged_service_plan asp - link_to_unmanaged_vnet subnetId - } + let site = resources |> getResource |> List.head + + let expectedRestriction = IpSecurityRestriction.Create "test-rule" ip Deny - 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" + 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 vnetConnections = resources |> getResource - Expect.hasLength vnetConnections 1 "incorrect number of Vnet connections" + let warmupSlot = appSlot { + name "warm-up" + add_allowed_ip_restriction "slot-rule" slotIp } - 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 = + 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" + 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" + 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" - } - ] + let vnetConnections = resources |> getResource + Expect.hasLength vnetConnections 1 "incorrect number of Vnet connections" + } + ] diff --git a/src/Tests/Gallery.fs b/src/Tests/Gallery.fs index 6ca6f469b..953fe9d88 100644 --- a/src/Tests/Gallery.fs +++ b/src/Tests/Gallery.fs @@ -7,126 +7,63 @@ open Farmer.Arm.Gallery open Newtonsoft.Json.Linq let tests = - testList - "Image Gallery" - [ - test "Builds basic image gallery" { - let deployment = - arm { - location Location.EastUS - - add_resources - [ - gallery { - name "mygallery" - description "Example Image Gallery" - } - ] - } - - let jobj = deployment.Template |> Writer.toJson |> JObject.Parse - let gallery = jobj.SelectToken "resources[?(@.name=='mygallery')]" - Expect.isNotNull gallery "Gallery is not included in deployment template" - let galleryDesc = gallery.SelectToken "properties.description" - Expect.equal galleryDesc (JValue "Example Image Gallery") "incorrect description" - } - test "Build community image gallery" { - let deployment = - arm { - location Location.EastUS - - add_resources - [ - gallery { - name "mygallery" - description "Example Community Image Gallery" - - sharing_profile ( - Community - { - Eula = "End User License Agreement goes here" - PublicNamePrefix = "farmages" - PublisherContact = "farmer.gallery@example.com" - PublisherUri = System.Uri "https://compositionalit.github.io/farmer" - } - ) - } - ] - } + testList "Image Gallery" [ + test "Builds basic image gallery" { + let deployment = arm { + location Location.EastUS - let jobj = deployment.Template |> Writer.toJson |> JObject.Parse - let gallery = jobj.SelectToken "resources[?(@.name=='mygallery')]" - let galleryPermissions = gallery.SelectToken "properties.sharingProfile.permissions" - Expect.equal galleryPermissions (JValue "Community") "incorrect permissions on community gallery" - - let galleryDesc = - gallery.SelectToken "properties.sharingProfile.communityGalleryInfo.publicNamePrefix" - - Expect.equal galleryDesc (JValue "farmages") "incorrect communityGalleryInfo.publicNamePrefix" - } - - test "Create basic image" { - let deployment = - arm { - add_resources - [ - galleryImage { - name "javaserver" - gallery_name "mygallery" - - gallery_image_identifier ( - { - GalleryImageIdentifier.Offer = "ubuntu-java" - Publisher = "farmages" - Sku = "ubuntu-20-java-17" - } - ) - - hyperv_generation Image.HyperVGeneration.V2 - os_state Image.OsState.Generalized - os_type OS.Linux - } - ] + add_resources [ + gallery { + name "mygallery" + description "Example Image Gallery" } - - let jobj = deployment.Template |> Writer.toJson |> JObject.Parse - let image = jobj.SelectToken "resources[?(@.name=='mygallery/javaserver')]" - Expect.isNotNull image "Image not found by gallery/image name" - Expect.isEmpty image.["dependsOn"] "Image should have no dependencies" - let imageProps = image.["properties"] - Expect.equal imageProps.["hyperVGeneration"] (JValue "V2") "Incorrect Hyper-V generation" - Expect.equal imageProps.["osState"] (JValue "Generalized") "Incorrect OS state" - Expect.equal imageProps.["osType"] (JValue "Linux") "Incorrect OS type" - let identifier = imageProps.["identifier"] - Expect.isNotNull identifier "Image properties missing 'identifier'" - Expect.equal identifier.["offer"] (JValue "ubuntu-java") "Incorrect identifier.offer" - Expect.equal identifier.["publisher"] (JValue "farmages") "Incorrect identifier.publisher" - Expect.equal identifier.["sku"] (JValue "ubuntu-20-java-17") "Incorrect identifier.sku" - let recommended = imageProps.["recommended"] - Expect.isNotNull recommended "properties.recommended is missing" - - Expect.equal - (recommended.SelectToken "memory.max") - (JValue 32) - "properties.recommended.memory.max incorrect" - - Expect.equal - (recommended.SelectToken "vCPUs.max") - (JValue 16) - "properties.recommended.vCPUs.max incorrect" + ] } - test "Create gallery and image" { - let myGallery = + let jobj = deployment.Template |> Writer.toJson |> JObject.Parse + let gallery = jobj.SelectToken "resources[?(@.name=='mygallery')]" + Expect.isNotNull gallery "Gallery is not included in deployment template" + let galleryDesc = gallery.SelectToken "properties.description" + Expect.equal galleryDesc (JValue "Example Image Gallery") "incorrect description" + } + test "Build community image gallery" { + let deployment = arm { + location Location.EastUS + + add_resources [ gallery { name "mygallery" - description "Example Private Gallery" + description "Example Community Image Gallery" + + sharing_profile ( + Community { + Eula = "End User License Agreement goes here" + PublicNamePrefix = "farmages" + PublisherContact = "farmer.gallery@example.com" + PublisherUri = System.Uri "https://compositionalit.github.io/farmer" + } + ) } + ] + } + + let jobj = deployment.Template |> Writer.toJson |> JObject.Parse + let gallery = jobj.SelectToken "resources[?(@.name=='mygallery')]" + let galleryPermissions = gallery.SelectToken "properties.sharingProfile.permissions" + Expect.equal galleryPermissions (JValue "Community") "incorrect permissions on community gallery" + + let galleryDesc = + gallery.SelectToken "properties.sharingProfile.communityGalleryInfo.publicNamePrefix" + + Expect.equal galleryDesc (JValue "farmages") "incorrect communityGalleryInfo.publicNamePrefix" + } - let myGalleryImage = + test "Create basic image" { + let deployment = arm { + add_resources [ galleryImage { - name "ubuntu-java-17-server" - gallery myGallery + name "javaserver" + gallery_name "mygallery" gallery_image_identifier ( { @@ -140,24 +77,72 @@ let tests = os_state Image.OsState.Generalized os_type OS.Linux } + ] + } - let deployment = - arm { - location Location.EastUS - add_resources [ myGallery; myGalleryImage ] - } + let jobj = deployment.Template |> Writer.toJson |> JObject.Parse + let image = jobj.SelectToken "resources[?(@.name=='mygallery/javaserver')]" + Expect.isNotNull image "Image not found by gallery/image name" + Expect.isEmpty image.["dependsOn"] "Image should have no dependencies" + let imageProps = image.["properties"] + Expect.equal imageProps.["hyperVGeneration"] (JValue "V2") "Incorrect Hyper-V generation" + Expect.equal imageProps.["osState"] (JValue "Generalized") "Incorrect OS state" + Expect.equal imageProps.["osType"] (JValue "Linux") "Incorrect OS type" + let identifier = imageProps.["identifier"] + Expect.isNotNull identifier "Image properties missing 'identifier'" + Expect.equal identifier.["offer"] (JValue "ubuntu-java") "Incorrect identifier.offer" + Expect.equal identifier.["publisher"] (JValue "farmages") "Incorrect identifier.publisher" + Expect.equal identifier.["sku"] (JValue "ubuntu-20-java-17") "Incorrect identifier.sku" + let recommended = imageProps.["recommended"] + Expect.isNotNull recommended "properties.recommended is missing" + + Expect.equal + (recommended.SelectToken "memory.max") + (JValue 32) + "properties.recommended.memory.max incorrect" + + Expect.equal (recommended.SelectToken "vCPUs.max") (JValue 16) "properties.recommended.vCPUs.max incorrect" + } + + test "Create gallery and image" { + let myGallery = gallery { + name "mygallery" + description "Example Private Gallery" + } - let jobj = deployment.Template |> Writer.toJson |> JObject.Parse + let myGalleryImage = galleryImage { + name "ubuntu-java-17-server" + gallery myGallery - let image = - jobj.SelectToken "resources[?(@.name=='mygallery/ubuntu-java-17-server')]" + gallery_image_identifier ( + { + GalleryImageIdentifier.Offer = "ubuntu-java" + Publisher = "farmages" + Sku = "ubuntu-20-java-17" + } + ) - Expect.isNotNull image "Image not found by gallery/image name" - Expect.hasLength image.["dependsOn"] 1 "Image should have 1 dependency" + hyperv_generation Image.HyperVGeneration.V2 + os_state Image.OsState.Generalized + os_type OS.Linux + } - Expect.equal - image.["dependsOn"].[0] - (JValue "[resourceId('Microsoft.Compute/galleries', 'mygallery')]") - "Image should depend on gallery" + let deployment = arm { + location Location.EastUS + add_resources [ myGallery; myGalleryImage ] } - ] + + let jobj = deployment.Template |> Writer.toJson |> JObject.Parse + + let image = + jobj.SelectToken "resources[?(@.name=='mygallery/ubuntu-java-17-server')]" + + Expect.isNotNull image "Image not found by gallery/image name" + Expect.hasLength image.["dependsOn"] 1 "Image should have 1 dependency" + + Expect.equal + image.["dependsOn"].[0] + (JValue "[resourceId('Microsoft.Compute/galleries', 'mygallery')]") + "Image should depend on gallery" + } + ] diff --git a/src/Tests/Helpers.fs b/src/Tests/Helpers.fs index 05fcdd911..c882de423 100644 --- a/src/Tests/Helpers.fs +++ b/src/Tests/Helpers.fs @@ -4,19 +4,17 @@ module TestHelpers open Farmer open Microsoft.Rest.Serialization -let createSimpleDeployment parameters = - { - Location = Location.NorthEurope - PostDeployTasks = [] - Template = - { - Outputs = [] - Parameters = parameters |> List.map SecureParameter - Resources = [] - } - RequiredResourceGroups = [] - Tags = Map.empty +let createSimpleDeployment parameters = { + Location = Location.NorthEurope + PostDeployTasks = [] + Template = { + Outputs = [] + Parameters = parameters |> List.map SecureParameter + Resources = [] } + RequiredResourceGroups = [] + Tags = Map.empty +} let convertTo<'T> = Serialization.toJson >> Serialization.ofJson<'T> @@ -52,11 +50,10 @@ let getFirstResourceOrFail (template: TypedArmTemplate<'ResourceType>) = template.Resources.[0] let toTemplate loc (d: IBuilder) = - let a = - arm { - location loc - add_resource d - } + let a = arm { + location loc + add_resource d + } a.Template diff --git a/src/Tests/Identity.fs b/src/Tests/Identity.fs index 5d5c57490..d06bcc4e9 100644 --- a/src/Tests/Identity.fs +++ b/src/Tests/Identity.fs @@ -6,84 +6,83 @@ open Farmer.Arm open Farmer.Identity let tests = - testList - "Identity" - [ - test "Can add two identities together" { - let systemOnly = - { ManagedIdentity.Empty with - SystemAssigned = Enabled - } - - let userOnlyA = - { ManagedIdentity.Empty with - UserAssigned = [ UserAssignedIdentity(userAssignedIdentities.resourceId "a") ] - } - - let userOnlyB = - { ManagedIdentity.Empty with - UserAssigned = [ UserAssignedIdentity(userAssignedIdentities.resourceId "b") ] - } - - Expect.isTrue (userOnlyA + systemOnly).SystemAssigned.AsBoolean "Should have System Assigned on" - - Expect.sequenceEqual - (userOnlyA + userOnlyB).UserAssigned - [ - UserAssignedIdentity(userAssignedIdentities.resourceId "a") - UserAssignedIdentity(userAssignedIdentities.resourceId "b") - ] - "User Assigned not added correctly" - - Expect.sequenceEqual - (userOnlyA + userOnlyA).UserAssigned - [ UserAssignedIdentity(userAssignedIdentities.resourceId "a") ] - "User Assigned duplicates exist" + testList "Identity" [ + test "Can add two identities together" { + let systemOnly = { + ManagedIdentity.Empty with + SystemAssigned = Enabled } - test "Creates ARM JSON correctly" { - let json = ManagedIdentity.Empty |> ManagedIdentity.toArmJson - Expect.equal json.``type`` "None" "Should be empty json" - Expect.isNull json.userAssignedIdentities "Should be empty json" - - let testIdentity = - userAssignedIdentities.resourceId "test" |> ManagedIdentity.create - - let json = testIdentity |> ManagedIdentity.toArmJson - Expect.equal json.``type`` "UserAssigned" "Should be user assigned" - - Expect.sequenceEqual - (json.userAssignedIdentities |> Seq.map (fun s -> s.Key)) - [ "[resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', 'test')]" ] - "Should be single UAI" - - Expect.equal - (json.userAssignedIdentities |> Seq.map (fun r -> r.Value.GetType()) |> Seq.head) - typeof - "Should be an object" - let json = - { - SystemAssigned = Enabled - UserAssigned = [] - } - |> ManagedIdentity.toArmJson - - Expect.equal json.``type`` "SystemAssigned" "Wrong type" - Expect.isNull json.userAssignedIdentities "Wrong identities" + let userOnlyA = { + ManagedIdentity.Empty with + UserAssigned = [ UserAssignedIdentity(userAssignedIdentities.resourceId "a") ] + } - let json = - let testIdentity2 = - userAssignedIdentities.resourceId "test2" |> ManagedIdentity.create + let userOnlyB = { + ManagedIdentity.Empty with + UserAssigned = [ UserAssignedIdentity(userAssignedIdentities.resourceId "b") ] + } - { ManagedIdentity.Empty with + Expect.isTrue (userOnlyA + systemOnly).SystemAssigned.AsBoolean "Should have System Assigned on" + + Expect.sequenceEqual + (userOnlyA + userOnlyB).UserAssigned + [ + UserAssignedIdentity(userAssignedIdentities.resourceId "a") + UserAssignedIdentity(userAssignedIdentities.resourceId "b") + ] + "User Assigned not added correctly" + + Expect.sequenceEqual + (userOnlyA + userOnlyA).UserAssigned + [ UserAssignedIdentity(userAssignedIdentities.resourceId "a") ] + "User Assigned duplicates exist" + } + test "Creates ARM JSON correctly" { + let json = ManagedIdentity.Empty |> ManagedIdentity.toArmJson + Expect.equal json.``type`` "None" "Should be empty json" + Expect.isNull json.userAssignedIdentities "Should be empty json" + + let testIdentity = + userAssignedIdentities.resourceId "test" |> ManagedIdentity.create + + let json = testIdentity |> ManagedIdentity.toArmJson + Expect.equal json.``type`` "UserAssigned" "Should be user assigned" + + Expect.sequenceEqual + (json.userAssignedIdentities |> Seq.map (fun s -> s.Key)) + [ "[resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', 'test')]" ] + "Should be single UAI" + + Expect.equal + (json.userAssignedIdentities |> Seq.map (fun r -> r.Value.GetType()) |> Seq.head) + typeof + "Should be an object" + + let json = + { + SystemAssigned = Enabled + UserAssigned = [] + } + |> ManagedIdentity.toArmJson + + Expect.equal json.``type`` "SystemAssigned" "Wrong type" + Expect.isNull json.userAssignedIdentities "Wrong identities" + + let json = + let testIdentity2 = + userAssignedIdentities.resourceId "test2" |> ManagedIdentity.create + + { + ManagedIdentity.Empty with SystemAssigned = Enabled - } - + testIdentity - + testIdentity2 - + testIdentity2 - |> ManagedIdentity.toArmJson - - Expect.equal json.``type`` "SystemAssigned, UserAssigned" "Wrong type" - Expect.hasLength json.userAssignedIdentities 2 "Wrong identities" - } - ] + } + + testIdentity + + testIdentity2 + + testIdentity2 + |> ManagedIdentity.toArmJson + + Expect.equal json.``type`` "SystemAssigned, UserAssigned" "Wrong type" + Expect.hasLength json.userAssignedIdentities 2 "Wrong identities" + } + ] diff --git a/src/Tests/ImageTemplate.fs b/src/Tests/ImageTemplate.fs index bb05ee9f6..8fc5a462d 100644 --- a/src/Tests/ImageTemplate.fs +++ b/src/Tests/ImageTemplate.fs @@ -7,185 +7,171 @@ open Farmer.Builders open Farmer.Arm.ImageTemplate let tests = - testList - "Image Template Tests" - [ - test "Builds basic customized image" { - let msi = createUserAssignedIdentity "imgbldr" - - let imageBuilder = + testList "Image Template Tests" [ + test "Builds basic customized image" { + let msi = createUserAssignedIdentity "imgbldr" + + let imageBuilder = { + Name = ResourceName "Ubuntu2004WithJava" + Location = Location.EastUS + Identity = { + SystemAssigned = Disabled + UserAssigned = [ msi.UserAssignedIdentity ] + } + Tags = Map.empty + Dependencies = Set.empty + BuildTimeoutInMinutes = None + Source = { - Name = ResourceName "Ubuntu2004WithJava" - Location = Location.EastUS - Identity = - { - SystemAssigned = Disabled - UserAssigned = [ msi.UserAssignedIdentity ] - } - Tags = Map.empty - Dependencies = Set.empty - BuildTimeoutInMinutes = None - Source = - { - PlanInfo = None - ImageIdentifier = - { - Publisher = "canonical" - Offer = "0001-com-ubuntu-server-focal" - Sku = "20_04-lts-gen2" - } - Version = null - } - |> ImageBuilderSource.Platform - Customize = - [ - { - Name = "install-jdk" - Inline = - [ - "set -eux" - "sudo apt-get update" - "sudo apt-get -y upgrade" - "sudo apt-get -y install openjdk-17-jre-headless" - ] - } - |> Customizer.Shell - ] - Distribute = - [ - { - RunOutputName = "testVhdRun" - ArtifactTags = Map.empty - } - |> Distibutor.VHD - ] + PlanInfo = None + ImageIdentifier = { + Publisher = "canonical" + Offer = "0001-com-ubuntu-server-focal" + Sku = "20_04-lts-gen2" + } + Version = null } - - let deployment = - arm { - location Location.EastUS - add_resource msi - add_resource imageBuilder + |> ImageBuilderSource.Platform + Customize = [ + { + Name = "install-jdk" + Inline = [ + "set -eux" + "sudo apt-get update" + "sudo apt-get -y upgrade" + "sudo apt-get -y install openjdk-17-jre-headless" + ] } + |> Customizer.Shell + ] + Distribute = [ + { + RunOutputName = "testVhdRun" + ArtifactTags = Map.empty + } + |> Distibutor.VHD + ] + } - let jobj = deployment.Template |> Writer.toJson |> JObject.Parse - let imageTemplate = jobj.SelectToken "resources[?(@.name == 'Ubuntu2004WithJava')]" - Expect.isNotNull imageTemplate "imageTemplate missing from deployment" - Expect.isNotNull (imageTemplate.SelectToken "identity") "imageTemplate.identity not set" - - let source = imageTemplate.SelectToken "properties.source" - Expect.equal source.["offer"] (JValue "0001-com-ubuntu-server-focal") "Incorrect source.offer" - Expect.equal source.["publisher"] (JValue "canonical") "Incorrect source.publisher" - Expect.equal source.["sku"] (JValue "20_04-lts-gen2") "Incorrect source.sku" - Expect.equal source.["type"] (JValue "PlatformImage") "Incorrect source.type" - Expect.equal source.["version"] (JValue "latest") "Incorrect source.version" - - let customize = imageTemplate.SelectToken "properties.customize" - Expect.hasLength customize 1 "customize length incorrect" - Expect.equal customize.[0].["type"] (JValue "Shell") "customize[0].type incorrect" - Expect.isNotNull customize.[0].["inline"] "Shell customization missing inline" - Expect.hasLength customize.[0].["inline"] 4 "Incorrect shell inline values" - - let distribute = imageTemplate.SelectToken "properties.distribute" - Expect.hasLength distribute 1 "distribute length incorrect" - Expect.equal distribute.[0].["runOutputName"] (JValue "testVhdRun") "Incorrect distribute.runOutputName" - Expect.equal distribute.[0].["type"] (JValue "VHD") "Incorrect distribute.type" - + let deployment = arm { + location Location.EastUS + add_resource msi + add_resource imageBuilder } - test "Customized image template builder" { - let msi = createUserAssignedIdentity "imgbldr" - - let imageBuilder = - imageTemplate { - name "Ubuntu2004WithJava" - add_identity msi - source_platform_image Vm.UbuntuServer_2004LTS - - add_customizers - [ - shellCustomizer { - name "install-jdk" - - inline_statements - [ - "set -eux" - "sudo apt-get update" - "sudo apt-get -y upgrade" - "sudo apt-get -y install openjdk-17-jre-headless" - ] - } - shellScriptCustomizer { script_uri "https://whatever.example.com/install.sh" } - ] - - add_distributors - [ - vhdDistributor { run_output_name "testVhdRun" } - sharedImageDistributor { - gallery_image_id ( - Farmer.Arm.Gallery.galleryImages.resourceId ( - ResourceName "my-image-gallery", - ResourceName "java-server-os" - ) - ) - - add_replication_regions [ Location.EastUS ] - add_tags [ "image-type", "java" ] - } - ] + let jobj = deployment.Template |> Writer.toJson |> JObject.Parse + let imageTemplate = jobj.SelectToken "resources[?(@.name == 'Ubuntu2004WithJava')]" + Expect.isNotNull imageTemplate "imageTemplate missing from deployment" + Expect.isNotNull (imageTemplate.SelectToken "identity") "imageTemplate.identity not set" + + let source = imageTemplate.SelectToken "properties.source" + Expect.equal source.["offer"] (JValue "0001-com-ubuntu-server-focal") "Incorrect source.offer" + Expect.equal source.["publisher"] (JValue "canonical") "Incorrect source.publisher" + Expect.equal source.["sku"] (JValue "20_04-lts-gen2") "Incorrect source.sku" + Expect.equal source.["type"] (JValue "PlatformImage") "Incorrect source.type" + Expect.equal source.["version"] (JValue "latest") "Incorrect source.version" + + let customize = imageTemplate.SelectToken "properties.customize" + Expect.hasLength customize 1 "customize length incorrect" + Expect.equal customize.[0].["type"] (JValue "Shell") "customize[0].type incorrect" + Expect.isNotNull customize.[0].["inline"] "Shell customization missing inline" + Expect.hasLength customize.[0].["inline"] 4 "Incorrect shell inline values" + + let distribute = imageTemplate.SelectToken "properties.distribute" + Expect.hasLength distribute 1 "distribute length incorrect" + Expect.equal distribute.[0].["runOutputName"] (JValue "testVhdRun") "Incorrect distribute.runOutputName" + Expect.equal distribute.[0].["type"] (JValue "VHD") "Incorrect distribute.type" + + } + + test "Customized image template builder" { + let msi = createUserAssignedIdentity "imgbldr" + + let imageBuilder = imageTemplate { + name "Ubuntu2004WithJava" + add_identity msi + source_platform_image Vm.UbuntuServer_2004LTS + + add_customizers [ + shellCustomizer { + name "install-jdk" + + inline_statements [ + "set -eux" + "sudo apt-get update" + "sudo apt-get -y upgrade" + "sudo apt-get -y install openjdk-17-jre-headless" + ] } - - let deployment = - arm { - location Location.EastUS - add_resource msi - add_resource imageBuilder + shellScriptCustomizer { script_uri "https://whatever.example.com/install.sh" } + ] + + add_distributors [ + vhdDistributor { run_output_name "testVhdRun" } + sharedImageDistributor { + gallery_image_id ( + Farmer.Arm.Gallery.galleryImages.resourceId ( + ResourceName "my-image-gallery", + ResourceName "java-server-os" + ) + ) + + add_replication_regions [ Location.EastUS ] + add_tags [ "image-type", "java" ] } + ] + } - let jobj = deployment.Template |> Writer.toJson |> JObject.Parse - let imageTemplate = jobj.SelectToken "resources[?(@.name == 'Ubuntu2004WithJava')]" - Expect.isNotNull imageTemplate "imageTemplate missing from deployment" - Expect.isNotNull (imageTemplate.SelectToken "identity") "imageTemplate.identity not set" - - let source = imageTemplate.SelectToken "properties.source" - Expect.equal source.["offer"] (JValue "0001-com-ubuntu-server-focal") "Incorrect source.offer" - Expect.equal source.["publisher"] (JValue "canonical") "Incorrect source.publisher" - Expect.equal source.["sku"] (JValue "20_04-lts-gen2") "Incorrect source.sku" - Expect.equal source.["type"] (JValue "PlatformImage") "Incorrect source.type" - Expect.equal source.["version"] (JValue "latest") "Incorrect source.version" - - let customize = imageTemplate.SelectToken "properties.customize" - Expect.hasLength customize 2 "customize length incorrect" - Expect.equal customize.[0].["type"] (JValue "Shell") "customize[0].type incorrect" - Expect.isNotNull customize.[0].["inline"] "Shell customization missing inline" - Expect.hasLength customize.[0].["inline"] 4 "Incorrect shell inline values" - Expect.equal customize.[1].["type"] (JValue "Shell") "customize[1].type incorrect" - - Expect.equal - customize.[1].["scriptUri"] - (JValue "https://whatever.example.com/install.sh") - "Incorrect shell scriptUri" - - let distribute = imageTemplate.SelectToken "properties.distribute" - Expect.hasLength distribute 2 "distribute length incorrect" - Expect.equal distribute.[0].["runOutputName"] (JValue "testVhdRun") "Incorrect distribute.runOutputName" - Expect.equal distribute.[0].["type"] (JValue "VHD") "Incorrect distribute.[0].type" - Expect.isNull (distribute.[0].SelectToken "artifactTags") "distrbute.[0] should not have 'artifactTags'" - Expect.equal distribute.[1].["type"] (JValue "SharedImage") "Incorrect distribute.[1].type" - - Expect.equal - (string distribute.[1].["galleryImageId"]) - ("[resourceId('Microsoft.Compute/galleries/images', 'my-image-gallery', 'java-server-os')]") - "Incorrect distribute.[1].galleryImageId" - - Expect.equal - distribute.[1].["runOutputName"] - (JValue "shared-image-run") - "Incorrect 'runOutputName' for shared iamge distributor" - - Expect.equal - (distribute.[1].SelectToken "artifactTags.image-type") - (JValue "java") - "distrbute.[1].artifactTags in correct" + let deployment = arm { + location Location.EastUS + add_resource msi + add_resource imageBuilder } - ] + + let jobj = deployment.Template |> Writer.toJson |> JObject.Parse + let imageTemplate = jobj.SelectToken "resources[?(@.name == 'Ubuntu2004WithJava')]" + Expect.isNotNull imageTemplate "imageTemplate missing from deployment" + Expect.isNotNull (imageTemplate.SelectToken "identity") "imageTemplate.identity not set" + + let source = imageTemplate.SelectToken "properties.source" + Expect.equal source.["offer"] (JValue "0001-com-ubuntu-server-focal") "Incorrect source.offer" + Expect.equal source.["publisher"] (JValue "canonical") "Incorrect source.publisher" + Expect.equal source.["sku"] (JValue "20_04-lts-gen2") "Incorrect source.sku" + Expect.equal source.["type"] (JValue "PlatformImage") "Incorrect source.type" + Expect.equal source.["version"] (JValue "latest") "Incorrect source.version" + + let customize = imageTemplate.SelectToken "properties.customize" + Expect.hasLength customize 2 "customize length incorrect" + Expect.equal customize.[0].["type"] (JValue "Shell") "customize[0].type incorrect" + Expect.isNotNull customize.[0].["inline"] "Shell customization missing inline" + Expect.hasLength customize.[0].["inline"] 4 "Incorrect shell inline values" + Expect.equal customize.[1].["type"] (JValue "Shell") "customize[1].type incorrect" + + Expect.equal + customize.[1].["scriptUri"] + (JValue "https://whatever.example.com/install.sh") + "Incorrect shell scriptUri" + + let distribute = imageTemplate.SelectToken "properties.distribute" + Expect.hasLength distribute 2 "distribute length incorrect" + Expect.equal distribute.[0].["runOutputName"] (JValue "testVhdRun") "Incorrect distribute.runOutputName" + Expect.equal distribute.[0].["type"] (JValue "VHD") "Incorrect distribute.[0].type" + Expect.isNull (distribute.[0].SelectToken "artifactTags") "distrbute.[0] should not have 'artifactTags'" + Expect.equal distribute.[1].["type"] (JValue "SharedImage") "Incorrect distribute.[1].type" + + Expect.equal + (string distribute.[1].["galleryImageId"]) + ("[resourceId('Microsoft.Compute/galleries/images', 'my-image-gallery', 'java-server-os')]") + "Incorrect distribute.[1].galleryImageId" + + Expect.equal + distribute.[1].["runOutputName"] + (JValue "shared-image-run") + "Incorrect 'runOutputName' for shared iamge distributor" + + Expect.equal + (distribute.[1].SelectToken "artifactTags.image-type") + (JValue "java") + "distrbute.[1].artifactTags in correct" + } + ] diff --git a/src/Tests/IotHub.fs b/src/Tests/IotHub.fs index 208c815fc..fefe9d5ea 100644 --- a/src/Tests/IotHub.fs +++ b/src/Tests/IotHub.fs @@ -19,52 +19,48 @@ let provisioningClient = new IotHubClient(Uri "http://management.azure.com", TokenCredentials "NotNullOrWhiteSpace") let tests = - testList - "IOT Hub" - [ - test "Can create a basic hub" { - let resource = - let hub = - iotHub { - name "isaacsuperhub" - sku B1 - capacity 2 - partition_count 2 - retention_days 3 - } + testList "IOT Hub" [ + test "Can create a basic hub" { + let resource = + let hub = iotHub { + name "isaacsuperhub" + sku B1 + capacity 2 + partition_count 2 + retention_days 3 + } - arm { add_resource hub } - |> findAzureResources iotClient.SerializationSettings - |> List.head + arm { add_resource hub } + |> findAzureResources iotClient.SerializationSettings + |> List.head - Expect.equal resource.Name "isaacsuperhub" "Hub name does not match" - Expect.equal resource.Sku.Name "B1" "Sku name is incorrect" - Expect.equal resource.Sku.Capacity (Nullable 2L) "Sku capacity is incorrect" + Expect.equal resource.Name "isaacsuperhub" "Hub name does not match" + Expect.equal resource.Sku.Name "B1" "Sku name is incorrect" + Expect.equal resource.Sku.Capacity (Nullable 2L) "Sku capacity is incorrect" - let events = resource.Properties.EventHubEndpoints.["events"] - Expect.equal events.PartitionCount (Nullable 2) "Partition count is incorrect" - Expect.equal events.RetentionTimeInDays (Nullable 3L) "Retention time is incorrect" - } + let events = resource.Properties.EventHubEndpoints.["events"] + Expect.equal events.PartitionCount (Nullable 2) "Partition count is incorrect" + Expect.equal events.RetentionTimeInDays (Nullable 3L) "Retention time is incorrect" + } - test "Creates a provisioning service" { - let resource = - let hub = - iotHub { - name "iothub" - enable_device_provisioning - } + test "Creates a provisioning service" { + let resource = + let hub = iotHub { + name "iothub" + enable_device_provisioning + } - let deployment = arm { add_resource hub } + let deployment = arm { add_resource hub } - deployment.Template.Resources.[1].JsonModel - |> Serialization.toJson - |> fun json -> - SafeJsonConvert.DeserializeObject( - json, - provisioningClient.SerializationSettings - ) + deployment.Template.Resources.[1].JsonModel + |> Serialization.toJson + |> fun json -> + SafeJsonConvert.DeserializeObject( + json, + provisioningClient.SerializationSettings + ) - Expect.equal resource.Sku.Capacity (Nullable 1L) "Sku capacity is incorrect" - Expect.equal resource.Sku.Name "S1" "Sku name capacity is incorrect" - } - ] + Expect.equal resource.Sku.Capacity (Nullable 1L) "Sku capacity is incorrect" + Expect.equal resource.Sku.Name "S1" "Sku name capacity is incorrect" + } + ] diff --git a/src/Tests/JsonRegression.fs b/src/Tests/JsonRegression.fs index e1dbabb86..399cdc975 100644 --- a/src/Tests/JsonRegression.fs +++ b/src/Tests/JsonRegression.fs @@ -77,287 +77,255 @@ let focusedDiffPrinter (numSurroundingLines: int) expected actual = (colourisedAndFocusedDiff diff.NewText.Lines) let tests = - testList - "ARM Writer Regression Tests" - [ - let compareDeploymentToJson (deployment: ResourceGroupConfig) jsonFile = - 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.equalWithDiffPrinter - (focusedDiffPrinter 8) - (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 = - let template = - arm { - location Location.NorthEurope - add_resources resources + testList "ARM Writer Regression Tests" [ + let compareDeploymentToJson (deployment: ResourceGroupConfig) jsonFile = + 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.equalWithDiffPrinter + (focusedDiffPrinter 8) + (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 = + let template = arm { + location Location.NorthEurope + add_resources resources + } + + compareDeploymentToJson template jsonFile + + test "Generates lots of resources" { + let number = string 1979 + + let sql = sqlServer { + name ("farmersql" + number) + admin_username "farmersqladmin" + + add_databases [ + sqlDb { + name "farmertestdb" + use_encryption } + ] - compareDeploymentToJson template jsonFile + enable_azure_firewall + } + + let storage = storageAccount { name ("farmerstorage" + number) } - test "Generates lots of resources" { - let number = string 1979 + let web = webApp { + name ("farmerwebapp" + number) + add_extension WebApp.Extensions.Logging + } - let sql = - sqlServer { - name ("farmersql" + number) - admin_username "farmersqladmin" + let fns = functions { name ("farmerfuncs" + number) } - add_databases - [ - sqlDb { - name "farmertestdb" - use_encryption - } - ] + let svcBus = serviceBus { + name ("farmerbus" + number) + sku ServiceBus.Sku.Standard + add_queues [ queue { name "queue1" } ] - enable_azure_firewall + add_topics [ + topic { + name "topic1" + add_subscriptions [ subscription { name "sub1" } ] } + ] + } - let storage = storageAccount { name ("farmerstorage" + number) } + let cdn = cdn { + name ("farmercdn" + number) - let web = - webApp { - name ("farmerwebapp" + number) - add_extension WebApp.Extensions.Logging - } + add_endpoints [ + endpoint { + name ("farmercdnendpoint" + number) + origin storage.WebsitePrimaryEndpointHost - let fns = functions { name ("farmerfuncs" + number) } - - let svcBus = - serviceBus { - name ("farmerbus" + number) - sku ServiceBus.Sku.Standard - add_queues [ queue { name "queue1" } ] - - add_topics - [ - topic { - name "topic1" - add_subscriptions [ subscription { name "sub1" } ] - } - ] - } + add_rule ( + cdnRule { + name ("farmerrule" + number) + order 1 - let cdn = - cdn { - name ("farmercdn" + number) - - add_endpoints - [ - endpoint { - name ("farmercdnendpoint" + number) - origin storage.WebsitePrimaryEndpointHost - - add_rule ( - cdnRule { - name ("farmerrule" + number) - order 1 - - when_device_type - DeliveryPolicy.EqualityOperator.Equals - DeliveryPolicy.DeviceType.Mobile - - url_rewrite "/pattern" "/destination" true - } - ) - } - ] - } + when_device_type DeliveryPolicy.EqualityOperator.Equals DeliveryPolicy.DeviceType.Mobile - let containerGroup = - containerGroup { - name ("farmeraci" + number) - - add_instances - [ - containerInstance { - name "webserver" - image "nginx:latest" - add_ports ContainerGroup.PublicPort [ 80us ] - add_volume_mount "source-code" "/src/farmer" - } - ] - - add_volumes - [ - volume_mount.git_repo - "source-code" - (System.Uri "https://github.com/CompositionalIT/farmer") - ] + url_rewrite "/pattern" "/destination" true + } + ) } + ] + } - let vm = - vm { - name "farmervm" - username "farmer-admin" + let containerGroup = containerGroup { + name ("farmeraci" + number) + + add_instances [ + containerInstance { + name "webserver" + image "nginx:latest" + add_ports ContainerGroup.PublicPort [ 80us ] + add_volume_mount "source-code" "/src/farmer" } + ] - let dockerFunction = - functions { - name "docker-func" - - publish_as ( - DockerContainer - { - Url = new Uri("http://www.farmer.io") - User = "Robert Lewandowski" - Password = SecureParameter "secure_pass_param" - StartupCommand = "do it" - } - ) + add_volumes [ + volume_mount.git_repo "source-code" (System.Uri "https://github.com/CompositionalIT/farmer") + ] + } - app_insights_off - } + let vm = vm { + name "farmervm" + username "farmer-admin" + } - let cosmos = - cosmosDb { - name "testdb" - account_name "testaccount" - throughput 400 - failover_policy CosmosDb.NoFailover - consistency_policy (CosmosDb.BoundedStaleness(500, 1000)) - - add_containers - [ - cosmosContainer { - name "myContainer" - partition_key [ "/id" ] CosmosDb.Hash - add_index "/path" [ CosmosDb.Number, CosmosDb.Hash ] - exclude_path "/excluded/*" - } - ] - } + let dockerFunction = functions { + name "docker-func" - let cosmosMongo = - cosmosDb { - name "testdbmongo" - account_name "testaccountmongo" - kind Mongo - throughput 400 - failover_policy CosmosDb.NoFailover - consistency_policy (CosmosDb.BoundedStaleness(500, 1000)) + publish_as ( + DockerContainer { + Url = new Uri("http://www.farmer.io") + User = "Robert Lewandowski" + Password = SecureParameter "secure_pass_param" + StartupCommand = "do it" } + ) - let nestedResourceGroup = - resourceGroup { - name "nested-resources" - deployment_name "nested-resources" - location Location.UKSouth - add_resources [ cosmos; cosmosMongo; vm ] - } + app_insights_off + } - let communicationServices = - communicationService { - name "test" - add_tags [ "a", "b" ] - data_location DataLocation.Australia + let cosmos = cosmosDb { + name "testdb" + account_name "testaccount" + throughput 400 + failover_policy CosmosDb.NoFailover + consistency_policy (CosmosDb.BoundedStaleness(500, 1000)) + + add_containers [ + cosmosContainer { + name "myContainer" + partition_key [ "/id" ] CosmosDb.Hash + add_index "/path" [ CosmosDb.Number, CosmosDb.Hash ] + exclude_path "/excluded/*" } + ] + } - compareResourcesToJson - [ - sql - storage - web - fns - svcBus - cdn - containerGroup - communicationServices - nestedResourceGroup - dockerFunction - ] - "lots-of-resources.json" + let cosmosMongo = cosmosDb { + name "testdbmongo" + account_name "testaccountmongo" + kind Mongo + throughput 400 + failover_policy CosmosDb.NoFailover + consistency_policy (CosmosDb.BoundedStaleness(500, 1000)) } - test "VM regression test" { - let myVm = - vm { - name "isaacsVM" - username "isaac" - vm_size Vm.Standard_A2 - operating_system Vm.WindowsServer_2012Datacenter - os_disk 128 Vm.StandardSSD_LRS - add_ssd_disk 128 - add_slow_disk 512 - diagnostics_support - } + let nestedResourceGroup = resourceGroup { + name "nested-resources" + deployment_name "nested-resources" + location Location.UKSouth + add_resources [ cosmos; cosmosMongo; vm ] + } - compareResourcesToJson [ myVm ] "vm.json" + let communicationServices = communicationService { + name "test" + add_tags [ "a", "b" ] + data_location DataLocation.Australia } - test "Storage, Event Hub, Log Analytics and Diagnostics" { - let data = storageAccount { name "isaacsuperdata" } - let hub = eventHub { name "isaacsuperhub" } - let logs = logAnalytics { name "isaacsuperlogs" } + compareResourcesToJson + [ + sql + storage + web + fns + svcBus + cdn + containerGroup + communicationServices + nestedResourceGroup + dockerFunction + ] + "lots-of-resources.json" + } + + test "VM regression test" { + let myVm = vm { + name "isaacsVM" + username "isaac" + vm_size Vm.Standard_A2 + operating_system Vm.WindowsServer_2012Datacenter + os_disk 128 Vm.StandardSSD_LRS + add_ssd_disk 128 + add_slow_disk 512 + diagnostics_support + } - let web = - webApp { - name "isaacdiagsuperweb" - app_insights_off - } + compareResourcesToJson [ myVm ] "vm.json" + } - let mydiagnosticSetting = - diagnosticSettings { - name "myDiagnosticSetting" - metrics_source web - - add_destination data - add_destination logs - add_destination hub - loganalytics_output_type Farmer.DiagnosticSettings.Dedicated - capture_metrics [ "AllMetrics" ] - - capture_logs - [ - Farmer.DiagnosticSettings.Logging.Web.Sites.AppServicePlatformLogs - Farmer.DiagnosticSettings.Logging.Web.Sites.AppServiceAntivirusScanAuditLogs - Farmer.DiagnosticSettings.Logging.Web.Sites.AppServiceAppLogs - Farmer.DiagnosticSettings.Logging.Web.Sites.AppServiceHTTPLogs - ] - } + test "Storage, Event Hub, Log Analytics and Diagnostics" { + let data = storageAccount { name "isaacsuperdata" } + let hub = eventHub { name "isaacsuperhub" } + let logs = logAnalytics { name "isaacsuperlogs" } - compareResourcesToJson [ data; web; hub; logs; mydiagnosticSetting ] "diagnostics.json" + let web = webApp { + name "isaacdiagsuperweb" + app_insights_off } - test "Event Grid" { - let storageSource = - storageAccount { - name "isaacgriddevprac" - add_private_container "data" - add_queue "todo" - } + let mydiagnosticSetting = diagnosticSettings { + name "myDiagnosticSetting" + metrics_source web + + add_destination data + add_destination logs + add_destination hub + loganalytics_output_type Farmer.DiagnosticSettings.Dedicated + capture_metrics [ "AllMetrics" ] + + capture_logs [ + Farmer.DiagnosticSettings.Logging.Web.Sites.AppServicePlatformLogs + Farmer.DiagnosticSettings.Logging.Web.Sites.AppServiceAntivirusScanAuditLogs + Farmer.DiagnosticSettings.Logging.Web.Sites.AppServiceAppLogs + Farmer.DiagnosticSettings.Logging.Web.Sites.AppServiceHTTPLogs + ] + } - let eventQueue = queue { name "events" } + compareResourcesToJson [ data; web; hub; logs; mydiagnosticSetting ] "diagnostics.json" + } - let sb = - serviceBus { - name "farmereventpubservicebusns" - add_queues [ eventQueue ] - } + test "Event Grid" { + let storageSource = storageAccount { + name "isaacgriddevprac" + add_private_container "data" + add_queue "todo" + } - let eventHubGrid = - eventGrid { - topic_name "newblobscreated" - source storageSource - add_queue_subscriber storageSource "todo" [ SystemEvents.Storage.BlobCreated ] - add_servicebus_queue_subscriber sb eventQueue [ SystemEvents.Storage.BlobCreated ] - } + let eventQueue = queue { name "events" } + + let sb = serviceBus { + name "farmereventpubservicebusns" + add_queues [ eventQueue ] + } - compareResourcesToJson [ storageSource; sb; eventHubGrid ] "event-grid.json" + let eventHubGrid = eventGrid { + topic_name "newblobscreated" + source storageSource + add_queue_subscriber storageSource "todo" [ SystemEvents.Storage.BlobCreated ] + add_servicebus_queue_subscriber sb eventQueue [ SystemEvents.Storage.BlobCreated ] } - test "Can parse JSON into an ARM template" { - let json = - """ { + compareResourcesToJson [ storageSource; sb; eventHubGrid ] "event-grid.json" + } + + test "Can parse JSON into an ARM template" { + let json = + """ { "apiVersion": "2019-06-01", "dependsOn": [], "kind": "StorageV2", @@ -372,232 +340,205 @@ let tests = } """ - let resource = - arm { add_resource (Resource.ofJson json) } |> Storage.getStorageResource + let resource = + arm { add_resource (Resource.ofJson json) } |> Storage.getStorageResource - Expect.equal resource.Name "jsontest" "Account name is wrong" - Expect.equal resource.Sku.Name "Standard_LRS" "SKU is wrong" - Expect.equal resource.Kind "StorageV2" "Kind" - } + Expect.equal resource.Name "jsontest" "Account name is wrong" + Expect.equal resource.Sku.Name "Standard_LRS" "SKU is wrong" + Expect.equal resource.Kind "StorageV2" "Kind" + } - test "ServiceBus" { - let svcBus = - serviceBus { - name "farmer-bus" - sku (ServiceBus.Sku.Premium MessagingUnits.OneUnit) - add_queues [ queue { name "queue1" } ] - - add_topics - [ - topic { - name "topic1" - - add_subscriptions - [ - subscription { - name "sub1" - - add_filters - [ - Rule.CreateCorrelationFilter( - "filter1", - [ "header1", "headervalue1" ] - ) - ] - } - ] - } - ] - } + test "ServiceBus" { + let svcBus = serviceBus { + name "farmer-bus" + sku (ServiceBus.Sku.Premium MessagingUnits.OneUnit) + add_queues [ queue { name "queue1" } ] - let topicWithUnmanagedNamespace = + add_topics [ topic { - name "unmanaged-topic" - link_to_unmanaged_namespace "farmer-bus" - - add_subscriptions - [ - subscription { - name "sub1" - - add_filters - [ Rule.CreateCorrelationFilter("filter1", [ "header1", "headervalue1" ]) ] - } - ] - } + name "topic1" - compareResourcesToJson [ svcBus; topicWithUnmanagedNamespace ] "service-bus.json" - } + add_subscriptions [ + subscription { + name "sub1" - test "VirtualWan" { - let vwan = - vwan { - name "farmer-vwan" - disable_vpn_encryption - allow_branch_to_branch_traffic - office_365_local_breakout_category Office365LocalBreakoutCategory.None - standard_vwan + add_filters [ Rule.CreateCorrelationFilter("filter1", [ "header1", "headervalue1" ]) ] + } + ] } - - compareResourcesToJson [ vwan ] "virtual-wan.json" + ] } - test "LoadBalancer" { - let myVnet = - vnet { - name "my-vnet" - add_address_spaces [ "10.0.1.0/24" ] - - add_subnets - [ - subnet { - name "my-services" - prefix "10.0.1.0/24" - add_delegations [ SubnetDelegationService.ContainerGroups ] - } - ] - } + let topicWithUnmanagedNamespace = topic { + name "unmanaged-topic" + link_to_unmanaged_namespace "farmer-bus" - let lb = - loadBalancer { - name "lb" - sku Farmer.LoadBalancer.Sku.Standard - - add_frontends - [ - frontend { - name "lb-frontend" - public_ip "lb-pip" - } - ] - - add_backend_pools - [ - backendAddressPool { - name "lb-backend" - vnet "my-vnet" - add_ip_addresses [ "10.0.1.4"; "10.0.1.5" ] - } - ] - - add_probes - [ - loadBalancerProbe { - name "httpGet" - protocol Farmer.LoadBalancer.LoadBalancerProbeProtocol.HTTP - port 8080 - request_path "/" - } - ] - - add_rules - [ - loadBalancingRule { - name "rule1" - frontend_ip_config "lb-frontend" - backend_address_pool "lb-backend" - frontend_port 80 - backend_port 8080 - protocol TransmissionProtocol.TCP - probe "httpGet" - } - ] + add_subscriptions [ + subscription { + name "sub1" + + add_filters [ Rule.CreateCorrelationFilter("filter1", [ "header1", "headervalue1" ]) ] } + ] + } - compareResourcesToJson [ myVnet; lb ] "load-balancer.json" + compareResourcesToJson [ svcBus; topicWithUnmanagedNamespace ] "service-bus.json" + } + + test "VirtualWan" { + let vwan = vwan { + name "farmer-vwan" + disable_vpn_encryption + allow_branch_to_branch_traffic + office_365_local_breakout_category Office365LocalBreakoutCategory.None + standard_vwan } - test "AzureFirewall" { - let vwan = - vwan { - name "farmer-vwan" - disable_vpn_encryption - allow_branch_to_branch_traffic - office_365_local_breakout_category Office365LocalBreakoutCategory.None - standard_vwan + compareResourcesToJson [ vwan ] "virtual-wan.json" + } + + test "LoadBalancer" { + let myVnet = vnet { + name "my-vnet" + add_address_spaces [ "10.0.1.0/24" ] + + add_subnets [ + subnet { + name "my-services" + prefix "10.0.1.0/24" + add_delegations [ SubnetDelegationService.ContainerGroups ] } + ] + } - let vhub = - vhub { - name "farmer_vhub" - address_prefix (IPAddressCidr.parse "100.73.255.0/24") - link_to_vwan vwan + let lb = loadBalancer { + name "lb" + sku Farmer.LoadBalancer.Sku.Standard + + add_frontends [ + frontend { + name "lb-frontend" + public_ip "lb-pip" } + ] - let firewall = - azureFirewall { - name "farmer_firewall" - sku SkuName.AZFW_Hub SkuTier.Standard - public_ip_reservation_count 2 - link_to_vhub vhub - availability_zones [ "1"; "2" ] - depends_on [ (vhub :> IBuilder).ResourceId ] + add_backend_pools [ + backendAddressPool { + name "lb-backend" + vnet "my-vnet" + add_ip_addresses [ "10.0.1.4"; "10.0.1.5" ] + } + ] + + add_probes [ + loadBalancerProbe { + name "httpGet" + protocol Farmer.LoadBalancer.LoadBalancerProbeProtocol.HTTP + port 8080 + request_path "/" } + ] + + add_rules [ + loadBalancingRule { + name "rule1" + frontend_ip_config "lb-frontend" + backend_address_pool "lb-backend" + frontend_port 80 + backend_port 8080 + protocol TransmissionProtocol.TCP + probe "httpGet" + } + ] + } - compareResourcesToJson [ firewall; vhub; vwan ] "azure-firewall.json" + compareResourcesToJson [ myVnet; lb ] "load-balancer.json" + } + + test "AzureFirewall" { + let vwan = vwan { + name "farmer-vwan" + disable_vpn_encryption + allow_branch_to_branch_traffic + office_365_local_breakout_category Office365LocalBreakoutCategory.None + standard_vwan } - test "AKS" { - let kubeletMsi = createUserAssignedIdentity "kubeletIdentity" - let clusterMsi = createUserAssignedIdentity "clusterIdentity" - - let assignMsiRoleNameExpr = - ArmExpression.create ( - $"guid(concat(resourceGroup().id, '{clusterMsi.ResourceId.Name.Value}', '{Roles.ManagedIdentityOperator.Id}'))" - ) - - let assignMsiRole = - { - Name = assignMsiRoleNameExpr.Eval() |> ResourceName - RoleDefinitionId = Roles.ManagedIdentityOperator - PrincipalId = clusterMsi.PrincipalId - PrincipalType = PrincipalType.ServicePrincipal - Scope = ResourceGroup - Dependencies = Set [ clusterMsi.ResourceId ] - } + let vhub = vhub { + name "farmer_vhub" + address_prefix (IPAddressCidr.parse "100.73.255.0/24") + link_to_vwan vwan + } - let myAcr = containerRegistry { name "farmercontainerregistry1234" } - let myAcrResId = (myAcr :> IBuilder).ResourceId - - let acrPullRoleNameExpr = - ArmExpression.create ( - $"guid(concat(resourceGroup().id, '{kubeletMsi.ResourceId.Name.Value}', '{Roles.AcrPull.Id}'))" - ) - - let acrPullRole = - { - Name = acrPullRoleNameExpr.Eval() |> ResourceName - RoleDefinitionId = Roles.AcrPull - PrincipalId = kubeletMsi.PrincipalId - PrincipalType = PrincipalType.ServicePrincipal - Scope = AssignmentScope.SpecificResource myAcrResId - Dependencies = Set [ kubeletMsi.ResourceId ] - } + let firewall = azureFirewall { + name "farmer_firewall" + sku SkuName.AZFW_Hub SkuTier.Standard + public_ip_reservation_count 2 + link_to_vhub vhub + availability_zones [ "1"; "2" ] + depends_on [ (vhub :> IBuilder).ResourceId ] + } - let myAks = - aks { - name "aks-cluster" - dns_prefix "aks-cluster-223d2976" - add_identity clusterMsi - service_principal_use_msi - kubelet_identity kubeletMsi - depends_on clusterMsi - depends_on myAcr - depends_on_expression assignMsiRoleNameExpr - depends_on_expression acrPullRoleNameExpr - } + compareResourcesToJson [ firewall; vhub; vwan ] "azure-firewall.json" + } + + test "AKS" { + let kubeletMsi = createUserAssignedIdentity "kubeletIdentity" + let clusterMsi = createUserAssignedIdentity "clusterIdentity" + + let assignMsiRoleNameExpr = + ArmExpression.create ( + $"guid(concat(resourceGroup().id, '{clusterMsi.ResourceId.Name.Value}', '{Roles.ManagedIdentityOperator.Id}'))" + ) + + let assignMsiRole = { + Name = assignMsiRoleNameExpr.Eval() |> ResourceName + RoleDefinitionId = Roles.ManagedIdentityOperator + PrincipalId = clusterMsi.PrincipalId + PrincipalType = PrincipalType.ServicePrincipal + Scope = ResourceGroup + Dependencies = Set [ clusterMsi.ResourceId ] + } - let template = - arm { - location Location.EastUS - add_resource kubeletMsi - add_resource clusterMsi - add_resource myAcr - add_resource myAks - add_resource assignMsiRole - add_resource acrPullRole - } + let myAcr = containerRegistry { name "farmercontainerregistry1234" } + let myAcrResId = (myAcr :> IBuilder).ResourceId + + let acrPullRoleNameExpr = + ArmExpression.create ( + $"guid(concat(resourceGroup().id, '{kubeletMsi.ResourceId.Name.Value}', '{Roles.AcrPull.Id}'))" + ) + + let acrPullRole = { + Name = acrPullRoleNameExpr.Eval() |> ResourceName + RoleDefinitionId = Roles.AcrPull + PrincipalId = kubeletMsi.PrincipalId + PrincipalType = PrincipalType.ServicePrincipal + Scope = AssignmentScope.SpecificResource myAcrResId + Dependencies = Set [ kubeletMsi.ResourceId ] + } + + let myAks = aks { + name "aks-cluster" + dns_prefix "aks-cluster-223d2976" + add_identity clusterMsi + service_principal_use_msi + kubelet_identity kubeletMsi + depends_on clusterMsi + depends_on myAcr + depends_on_expression assignMsiRoleNameExpr + depends_on_expression acrPullRoleNameExpr + } - compareDeploymentToJson template "aks-with-acr.json" + let template = arm { + location Location.EastUS + add_resource kubeletMsi + add_resource clusterMsi + add_resource myAcr + add_resource myAks + add_resource assignMsiRole + add_resource acrPullRole } - ] + + compareDeploymentToJson template "aks-with-acr.json" + } + ] diff --git a/src/Tests/KeyVault.fs b/src/Tests/KeyVault.fs index bbbd968a5..8f811f910 100644 --- a/src/Tests/KeyVault.fs +++ b/src/Tests/KeyVault.fs @@ -9,364 +9,341 @@ open Farmer open Newtonsoft.Json.Linq let tests = - testList - "KeyVault" - [ - test "Can create secrets without popping" { secret { name "test" } |> ignore } - test "Can create quick secrets" { - let vault = - keyVault { - name "test" - add_secret "test1" - add_secret (secret { name "test2" }) - } - - Expect.hasLength vault.Secrets 2 "Bad secrets" - } - test "Can create secrets with tags" { - let s = - secret { - name "test" - add_tag "foo" "bar" - add_tag "fizz" "buzz" - } - - Expect.hasLength s.Tags 2 "Incorrect number of tags on secret" - Expect.equal s.Tags.["foo"] "bar" "Incorrect value on secret tag 'foo'" - Expect.equal s.Tags.["fizz"] "buzz" "Incorrect value on secret tag 'fizz'" - } - test "Throws on empty inline secret" { - Expect.throws - (fun () -> - keyVault { - name "test" - add_secret "" - } - |> ignore) - "Empty secret should throw" - } - test "Throws on empty full secret" { - Expect.throws (fun () -> secret { name "" } |> ignore) "Empty secret should throw" - } - test "Default access policy settings is GET and LIST" { - let p = AccessPolicy.create (ObjectId Guid.Empty) - Expect.equal (set [ Secret.Get; Secret.List ]) p.Permissions.Secrets "Incorrect default secrets" - } - test "Creates key vault secrets correctly" { - let parameterSecret = SecretConfig.create "test" - Expect.equal parameterSecret.SecretName "test" "Invalid name of simple secret" - - Expect.equal - parameterSecret.Value - (ParameterSecret(SecureParameter "test")) - "Invalid value of parameter secret" - - let sa = storageAccount { name "storage" } - let expressionSecret = SecretConfig.create ("test", sa.Key) - Expect.equal expressionSecret.Value (ExpressionSecret sa.Key) "Invalid value of expression secret" - - Expect.sequenceEqual - expressionSecret.Dependencies - [ ResourceId.create (Farmer.Arm.Storage.storageAccounts, sa.Name.ResourceName) ] - "Missing storage account dependency" - - Expect.throws - (fun _ -> SecretConfig.create ("bad", (ArmExpression.literal "foo")) |> ignore) - "Should throw exception on expression with no owner" - } - - test "Works with identities" { - let a = webApp { name "test" } - - let v = - keyVault { add_access_policy (AccessPolicy.create a.SystemIdentity.PrincipalId) } :> IBuilder - - let vault = - v.BuildResources Location.NorthEurope |> List.head :?> Farmer.Arm.KeyVault.Vault - - Expect.sequenceEqual - vault.Dependencies - [ ResourceId.create (Arm.Web.sites, a.Name.ResourceName) ] - "Web App dependency" + testList "KeyVault" [ + test "Can create secrets without popping" { secret { name "test" } |> ignore } + test "Can create quick secrets" { + let vault = keyVault { + name "test" + add_secret "test1" + add_secret (secret { name "test2" }) } - test "Create a basic key vault" { - let kv = keyVault { name "my-test-kv-9876abcd" } - - let json = - let template = arm { add_resource kv } - template.Template |> Writer.toJson - - let jobj = JObject.Parse(json) - let kvName = jobj.SelectToken("resources[0].name") - - Expect.equal - kvName - (JValue.CreateString "my-test-kv-9876abcd" :> JToken) - "Incorrect name set on key vault" + Expect.hasLength vault.Secrets 2 "Bad secrets" + } + test "Can create secrets with tags" { + let s = secret { + name "test" + add_tag "foo" "bar" + add_tag "fizz" "buzz" } - test "Create a key vault with RBAC enabled" { - let kv = + Expect.hasLength s.Tags 2 "Incorrect number of tags on secret" + Expect.equal s.Tags.["foo"] "bar" "Incorrect value on secret tag 'foo'" + Expect.equal s.Tags.["fizz"] "buzz" "Incorrect value on secret tag 'fizz'" + } + test "Throws on empty inline secret" { + Expect.throws + (fun () -> keyVault { - name "my-test-kv-9876rbac" - enable_rbac - } - - let msi = createUserAssignedIdentity "kvUser" - - let roleAssignment = - { - Name = - ArmExpression - .create($"guid(concat(resourceGroup().id, '{Roles.KeyVaultReader.Id}'))") - .Eval() - |> ResourceName - RoleDefinitionId = Roles.KeyVaultReader - PrincipalId = msi.PrincipalId - PrincipalType = PrincipalType.ServicePrincipal - Scope = ResourceGroup - Dependencies = Set.empty + name "test" + add_secret "" } - - let json = - let template = - arm { - add_resource kv - add_resource msi - add_resource roleAssignment - } - - template.Template |> Writer.toJson - - let jobj = JObject.Parse(json) - let enableRbac = jobj.SelectToken("resources[0].properties.enableRbacAuthorization") - Expect.isTrue (enableRbac.Value()) "RBAC was not enabled on the key vault" + |> ignore) + "Empty secret should throw" + } + test "Throws on empty full secret" { + Expect.throws (fun () -> secret { name "" } |> ignore) "Empty secret should throw" + } + test "Default access policy settings is GET and LIST" { + let p = AccessPolicy.create (ObjectId Guid.Empty) + Expect.equal (set [ Secret.Get; Secret.List ]) p.Permissions.Secrets "Incorrect default secrets" + } + test "Creates key vault secrets correctly" { + let parameterSecret = SecretConfig.create "test" + Expect.equal parameterSecret.SecretName "test" "Invalid name of simple secret" + + Expect.equal + parameterSecret.Value + (ParameterSecret(SecureParameter "test")) + "Invalid value of parameter secret" + + let sa = storageAccount { name "storage" } + let expressionSecret = SecretConfig.create ("test", sa.Key) + Expect.equal expressionSecret.Value (ExpressionSecret sa.Key) "Invalid value of expression secret" + + Expect.sequenceEqual + expressionSecret.Dependencies + [ ResourceId.create (Farmer.Arm.Storage.storageAccounts, sa.Name.ResourceName) ] + "Missing storage account dependency" + + Expect.throws + (fun _ -> SecretConfig.create ("bad", (ArmExpression.literal "foo")) |> ignore) + "Should throw exception on expression with no owner" + } + + test "Works with identities" { + let a = webApp { name "test" } + + let v = + keyVault { add_access_policy (AccessPolicy.create a.SystemIdentity.PrincipalId) } :> IBuilder + + let vault = + v.BuildResources Location.NorthEurope |> List.head :?> Farmer.Arm.KeyVault.Vault + + Expect.sequenceEqual + vault.Dependencies + [ ResourceId.create (Arm.Web.sites, a.Name.ResourceName) ] + "Web App dependency" + } + + test "Create a basic key vault" { + let kv = keyVault { name "my-test-kv-9876abcd" } + + let json = + let template = arm { add_resource kv } + template.Template |> Writer.toJson + + let jobj = JObject.Parse(json) + let kvName = jobj.SelectToken("resources[0].name") + + Expect.equal kvName (JValue.CreateString "my-test-kv-9876abcd" :> JToken) "Incorrect name set on key vault" + } + + test "Create a key vault with RBAC enabled" { + let kv = keyVault { + name "my-test-kv-9876rbac" + enable_rbac } - test "Get Vault URI from output" { - let kv = keyVault { name "my-test-kv-9876" } - - let json = - let template = - arm { - add_resource kv - output "kv-uri" kv.VaultUri - } - - template.Template |> Writer.toJson - let jobj = JObject.Parse(json) - let kvUri = jobj.SelectToken("outputs.kv-uri.value") - - Expect.equal - (string kvUri) - "[reference(resourceId('Microsoft.KeyVault/vaults', 'my-test-kv-9876')).vaultUri]" - "Vault URI not set properly in output" + let msi = createUserAssignedIdentity "kvUser" + + let roleAssignment = { + Name = + ArmExpression + .create($"guid(concat(resourceGroup().id, '{Roles.KeyVaultReader.Id}'))") + .Eval() + |> ResourceName + RoleDefinitionId = Roles.KeyVaultReader + PrincipalId = msi.PrincipalId + PrincipalType = PrincipalType.ServicePrincipal + Scope = ResourceGroup + Dependencies = Set.empty } - test "Key Vault with purge protection emits correct value" { - let kv = - keyVault { - name "my-test-kv-9876" - enable_soft_delete_with_purge_protection - } - - let json = - let template = arm { add_resource kv } - template.Template |> Writer.toJson - let jobj = JObject.Parse(json) - - let purgeProtection = - jobj.SelectToken("resources[0].properties.enablePurgeProtection") - - Expect.equal (purgeProtection |> string |> Boolean.Parse) true "Purge protection not enabled" + let json = + let template = arm { + add_resource kv + add_resource msi + add_resource roleAssignment + } + + template.Template |> Writer.toJson + + let jobj = JObject.Parse(json) + let enableRbac = jobj.SelectToken("resources[0].properties.enableRbacAuthorization") + Expect.isTrue (enableRbac.Value()) "RBAC was not enabled on the key vault" + } + test "Get Vault URI from output" { + let kv = keyVault { name "my-test-kv-9876" } + + let json = + let template = arm { + add_resource kv + output "kv-uri" kv.VaultUri + } + + template.Template |> Writer.toJson + + let jobj = JObject.Parse(json) + let kvUri = jobj.SelectToken("outputs.kv-uri.value") + + Expect.equal + (string kvUri) + "[reference(resourceId('Microsoft.KeyVault/vaults', 'my-test-kv-9876')).vaultUri]" + "Vault URI not set properly in output" + } + test "Key Vault with purge protection emits correct value" { + let kv = keyVault { + name "my-test-kv-9876" + enable_soft_delete_with_purge_protection } - test "Add access policies on existing key vault" { - let additionalPolicies = - keyVaultAddPolicies { - key_vault (Farmer.Arm.KeyVault.vaults.resourceId "existing-vault") - - add_access_policies - [ - accessPolicy { - object_id (Guid "ad731a70-fd25-452f-b9d8-a0c0ae8033af") - application_id (Guid "12ef53f8-98a0-4513-b081-6b5e70db76e1") - certificate_permissions [ KeyVault.Certificate.List ] - secret_permissions KeyVault.Secret.All - key_permissions [ KeyVault.Key.List ] - } - ] - } - - let template = arm { add_resource additionalPolicies } - let jobj = JObject.Parse(template.Template |> Writer.toJson) - let name = jobj.SelectToken("resources[0].name") - Expect.equal (name |> string) "existing-vault/add" "Incorrect name for adding kv access policies" - let dependsOn = jobj.SelectToken("resources[0].dependsOn") :?> JArray - Expect.hasLength dependsOn 0 "Should have no dependencies" - let accessPolicies = - jobj.SelectToken("resources[0].properties.accessPolicies") :?> JArray + let json = + let template = arm { add_resource kv } + template.Template |> Writer.toJson - Expect.hasLength accessPolicies 1 "Should include one access policy to add to the key vault" + let jobj = JObject.Parse(json) - let tenant = - jobj.SelectToken("resources[0].properties.accessPolicies[0].tenantId") |> string + let purgeProtection = + jobj.SelectToken("resources[0].properties.enablePurgeProtection") - Expect.equal - tenant - "[subscription().tenantId]" - "If tenant was not specified, access policies default to target subscription's tenant" - } - test "Adding access policies on existing key vault without specifying the key vault fails" { - Expect.throws - (fun _ -> - let additionalPolicies = - keyVaultAddPolicies { - add_access_policies - [ - accessPolicy { - object_id (Guid "ad731a70-fd25-452f-b9d8-a0c0ae8033af") - application_id (Guid "12ef53f8-98a0-4513-b081-6b5e70db76e1") - certificate_permissions [ KeyVault.Certificate.List ] - secret_permissions KeyVault.Secret.All - key_permissions [ KeyVault.Key.List ] - } - ] - } + Expect.equal (purgeProtection |> string |> Boolean.Parse) true "Purge protection not enabled" + } + test "Add access policies on existing key vault" { + let additionalPolicies = keyVaultAddPolicies { + key_vault (Farmer.Arm.KeyVault.vaults.resourceId "existing-vault") - let template = arm { add_resource additionalPolicies } - template |> Writer.quickWrite |> ignore) - "Should have failed to build the key vault policy addition resource" - } - test "Standalone secret can be added as resource" { - let vaultId = (Arm.KeyVault.vaults.resourceId (ResourceName "my-kv")) - - let secret = - secret { - name "my-secret" - content_type "ConnectionString" - link_to_unmanaged_keyvault (Arm.KeyVault.vaults.resourceId "my-kv") + add_access_policies [ + accessPolicy { + object_id (Guid "ad731a70-fd25-452f-b9d8-a0c0ae8033af") + application_id (Guid "12ef53f8-98a0-4513-b081-6b5e70db76e1") + certificate_permissions [ KeyVault.Certificate.List ] + secret_permissions KeyVault.Secret.All + key_permissions [ KeyVault.Key.List ] } - - let resources = (arm { add_resource secret }).Template.Resources - Expect.hasLength resources 1 "Should only be one resource" - - match resources.[0] with - | :? Arm.KeyVault.Vaults.Secret as secret -> () - | x -> - failwith - $"resource was expected to be of type {typeof} but was {x.GetType()}" + ] } - test "Adding keys with key vault" { - let vault = - keyVault { - name "TestFarmVault" - tenant_id Subscription.TenantId - - add_keys - [ - key { - name "testKeyInlineRsa" - status Enabled - key_type KeyType.RSA_4096 - key_operations [ KeyOperation.Encrypt; KeyOperation.Sign; KeyOperation.Verify ] - } - key { - name "testKeyInlineEc" - status Disabled - key_type KeyType.EC_P256 - } - ] - } - - let deployment = arm { add_resource vault } - let jobj = JObject.Parse(deployment.Template |> Writer.toJson) - let rsaKey = - jobj.SelectToken("$.resources[?(@.name=='TestFarmVault/testKeyInlineRsa')]") - - Expect.isNotNull rsaKey "Unable to find 'testKeyInlineRsa'" - Expect.equal (int rsaKey.["properties"].["keySize"]) 4096 "Incorrect RSA key size" - Expect.equal (string rsaKey.["properties"].["kty"]) "RSA" "Incorrect RSA key type" - Expect.isNull rsaKey.["properties"].["curveName"] "RSA key should not have curveName" - - let ecKey = - jobj.SelectToken("$.resources[?(@.name=='TestFarmVault/testKeyInlineEc')]") + let template = arm { add_resource additionalPolicies } + let jobj = JObject.Parse(template.Template |> Writer.toJson) + let name = jobj.SelectToken("resources[0].name") + Expect.equal (name |> string) "existing-vault/add" "Incorrect name for adding kv access policies" + let dependsOn = jobj.SelectToken("resources[0].dependsOn") :?> JArray + Expect.hasLength dependsOn 0 "Should have no dependencies" + + let accessPolicies = + jobj.SelectToken("resources[0].properties.accessPolicies") :?> JArray + + Expect.hasLength accessPolicies 1 "Should include one access policy to add to the key vault" + + let tenant = + jobj.SelectToken("resources[0].properties.accessPolicies[0].tenantId") |> string + + Expect.equal + tenant + "[subscription().tenantId]" + "If tenant was not specified, access policies default to target subscription's tenant" + } + test "Adding access policies on existing key vault without specifying the key vault fails" { + Expect.throws + (fun _ -> + let additionalPolicies = keyVaultAddPolicies { + add_access_policies [ + accessPolicy { + object_id (Guid "ad731a70-fd25-452f-b9d8-a0c0ae8033af") + application_id (Guid "12ef53f8-98a0-4513-b081-6b5e70db76e1") + certificate_permissions [ KeyVault.Certificate.List ] + secret_permissions KeyVault.Secret.All + key_permissions [ KeyVault.Key.List ] + } + ] + } - Expect.isNotNull ecKey "Unable to find 'testKeyInlineEc'" - Expect.equal (string ecKey.["properties"].["kty"]) "EC" "Incorrect EC key type" - Expect.equal (string ecKey.["properties"].["curveName"]) "P-256" "Incorrect EC curveName" - Expect.isNull ecKey.["properties"].["keySize"] "Elliptic curve key should not have keySize" + let template = arm { add_resource additionalPolicies } + template |> Writer.quickWrite |> ignore) + "Should have failed to build the key vault policy addition resource" + } + test "Standalone secret can be added as resource" { + let vaultId = (Arm.KeyVault.vaults.resourceId (ResourceName "my-kv")) + + let secret = secret { + name "my-secret" + content_type "ConnectionString" + link_to_unmanaged_keyvault (Arm.KeyVault.vaults.resourceId "my-kv") } - test "Adding standalone keys to key vault" { - let vault = keyVault { name "TestFarmVault" } - let myKey = + let resources = (arm { add_resource secret }).Template.Resources + Expect.hasLength resources 1 "Should only be one resource" + + match resources.[0] with + | :? Arm.KeyVault.Vaults.Secret as secret -> () + | x -> + failwith + $"resource was expected to be of type {typeof} but was {x.GetType()}" + } + test "Adding keys with key vault" { + let vault = keyVault { + name "TestFarmVault" + tenant_id Subscription.TenantId + + add_keys [ key { - name "testKey" + name "testKeyInlineRsa" + status Enabled key_type KeyType.RSA_4096 - link_to_unmanaged_keyvault vault - key_operations [ KeyOperation.Encrypt ] + key_operations [ KeyOperation.Encrypt; KeyOperation.Sign; KeyOperation.Verify ] } - - let deployment = arm { add_resource myKey } - let jobj = JObject.Parse(deployment.Template |> Writer.toJson) - let key = jobj.SelectToken("$.resources[?(@.name=='TestFarmVault/testKey')]") - Expect.isNotNull key "Unable to find 'testKey'" - Expect.equal (int key.["properties"].["keySize"]) 4096 "Incorrect RSA key size" - Expect.equal (string key.["properties"].["kty"]) "RSA" "Incorrect RSA key type" - Expect.isEmpty key.["dependsOn"] "Standalone key should not have dependsOn" - } - - test "Should add vnet rule correctly to key vault" { - let vnetId = "/subscriptions/..." - - let vault = - keyVault { - name "TestFarmVault" - add_vnet_rule vnetId + key { + name "testKeyInlineEc" + status Disabled + key_type KeyType.EC_P256 } + ] + } - let deployment = arm { add_resource vault } - let jobj = JObject.Parse(deployment.Template |> Writer.toJson) - - let rules = - jobj.SelectToken("resources[0].properties.networkAcls.virtualNetworkRules") + let deployment = arm { add_resource vault } + let jobj = JObject.Parse(deployment.Template |> Writer.toJson) + + let rsaKey = + jobj.SelectToken("$.resources[?(@.name=='TestFarmVault/testKeyInlineRsa')]") + + Expect.isNotNull rsaKey "Unable to find 'testKeyInlineRsa'" + Expect.equal (int rsaKey.["properties"].["keySize"]) 4096 "Incorrect RSA key size" + Expect.equal (string rsaKey.["properties"].["kty"]) "RSA" "Incorrect RSA key type" + Expect.isNull rsaKey.["properties"].["curveName"] "RSA key should not have curveName" + + let ecKey = + jobj.SelectToken("$.resources[?(@.name=='TestFarmVault/testKeyInlineEc')]") + + Expect.isNotNull ecKey "Unable to find 'testKeyInlineEc'" + Expect.equal (string ecKey.["properties"].["kty"]) "EC" "Incorrect EC key type" + Expect.equal (string ecKey.["properties"].["curveName"]) "P-256" "Incorrect EC curveName" + Expect.isNull ecKey.["properties"].["keySize"] "Elliptic curve key should not have keySize" + } + test "Adding standalone keys to key vault" { + let vault = keyVault { name "TestFarmVault" } + + let myKey = key { + name "testKey" + key_type KeyType.RSA_4096 + link_to_unmanaged_keyvault vault + key_operations [ KeyOperation.Encrypt ] + } - Expect.isNonEmpty (rules :?> JArray) "Should have at least one network rule setup" - let ruleId = string rules.[0].["id"] - Expect.equal vnetId ruleId "The setup network rule should match the used vnetId" + let deployment = arm { add_resource myKey } + let jobj = JObject.Parse(deployment.Template |> Writer.toJson) + let key = jobj.SelectToken("$.resources[?(@.name=='TestFarmVault/testKey')]") + Expect.isNotNull key "Unable to find 'testKey'" + Expect.equal (int key.["properties"].["keySize"]) 4096 "Incorrect RSA key size" + Expect.equal (string key.["properties"].["kty"]) "RSA" "Incorrect RSA key type" + Expect.isEmpty key.["dependsOn"] "Standalone key should not have dependsOn" + } + + test "Should add vnet rule correctly to key vault" { + let vnetId = "/subscriptions/..." + + let vault = keyVault { + name "TestFarmVault" + add_vnet_rule vnetId } - test "Public network access can be toggled" { - let vault = - keyVault { - name "TestFarmVault" - disable_public_network_access - disable_public_network_access FeatureFlag.Disabled - } - let deployment = arm { add_resource vault } - let jobj = JObject.Parse(deployment.Template |> Writer.toJson) + let deployment = arm { add_resource vault } + let jobj = JObject.Parse(deployment.Template |> Writer.toJson) + + let rules = + jobj.SelectToken("resources[0].properties.networkAcls.virtualNetworkRules") + + Expect.isNonEmpty (rules :?> JArray) "Should have at least one network rule setup" + let ruleId = string rules.[0].["id"] + Expect.equal vnetId ruleId "The setup network rule should match the used vnetId" + } + test "Public network access can be toggled" { + let vault = keyVault { + name "TestFarmVault" + disable_public_network_access + disable_public_network_access FeatureFlag.Disabled + } - Expect.equal - (jobj.SelectToken("resources[0].properties.publicNetworkAccess").ToString()) - "Enabled" - "public network access should be enabled" + let deployment = arm { add_resource vault } + let jobj = JObject.Parse(deployment.Template |> Writer.toJson) + + Expect.equal + (jobj.SelectToken("resources[0].properties.publicNetworkAccess").ToString()) + "Enabled" + "public network access should be enabled" + } + test "Public network access can be disabled" { + let vault = keyVault { + name "TestFarmVault" + disable_public_network_access } - test "Public network access can be disabled" { - let vault = - keyVault { - name "TestFarmVault" - disable_public_network_access - } - let deployment = arm { add_resource vault } - let jobj = JObject.Parse(deployment.Template |> Writer.toJson) + let deployment = arm { add_resource vault } + let jobj = JObject.Parse(deployment.Template |> Writer.toJson) - Expect.equal - (jobj.SelectToken("resources[0].properties.publicNetworkAccess").ToString()) - "Disabled" - "public network access should be disabled" - } - ] + Expect.equal + (jobj.SelectToken("resources[0].properties.publicNetworkAccess").ToString()) + "Disabled" + "public network access should be disabled" + } + ] diff --git a/src/Tests/LoadBalancer.fs b/src/Tests/LoadBalancer.fs index d29efb4c0..6dcfc179a 100644 --- a/src/Tests/LoadBalancer.fs +++ b/src/Tests/LoadBalancer.fs @@ -12,252 +12,236 @@ let client = new NetworkManagementClient(Uri "http://management.azure.com", TokenCredentials "NotNullOrWhiteSpace") let tests = - testList - "Load Balancers" - [ - test "Empty basic load balancer" { - let lb = loadBalancer { name "lb" } - - let resource = - arm { add_resource lb } - |> findAzureResources - client.SerializationSettings - |> List.head - - Expect.equal resource.Name "lb" "Name did not match" - Expect.equal resource.Sku.Name "Basic" "Incorrect sku" + testList "Load Balancers" [ + test "Empty basic load balancer" { + let lb = loadBalancer { name "lb" } + + let resource = + arm { add_resource lb } + |> findAzureResources + client.SerializationSettings + |> List.head + + Expect.equal resource.Name "lb" "Name did not match" + Expect.equal resource.Sku.Name "Basic" "Incorrect sku" + } + + test "Empty standard load balancer" { + let lb = loadBalancer { + name "lb" + sku Sku.Standard } - test "Empty standard load balancer" { - let lb = - loadBalancer { - name "lb" - sku Sku.Standard - } + let resource = + arm { add_resource lb } + |> findAzureResources + client.SerializationSettings + |> List.head - let resource = - arm { add_resource lb } - |> findAzureResources - client.SerializationSettings - |> List.head + Expect.equal resource.Name "lb" "Name did not match" + Expect.equal resource.Sku.Name "Standard" "Incorrect sku" + } - Expect.equal resource.Name "lb" "Name did not match" - Expect.equal resource.Sku.Name "Standard" "Incorrect sku" + test "Empty standard load balancer with dependency" { + let lb = loadBalancer { + name "lb" + add_dependencies [ Farmer.Arm.Network.virtualNetworks.resourceId "existing-vnet" ] } - test "Empty standard load balancer with dependency" { - let lb = - loadBalancer { - name "lb" - add_dependencies [ Farmer.Arm.Network.virtualNetworks.resourceId "existing-vnet" ] - } + let deployment = arm { add_resource lb } + let json = deployment.Template |> Writer.toJson + let jobj = Newtonsoft.Json.Linq.JObject.Parse(json) - let deployment = arm { add_resource lb } - let json = deployment.Template |> Writer.toJson - let jobj = Newtonsoft.Json.Linq.JObject.Parse(json) + let expectedLoadBalancerDeps = + "[resourceId('Microsoft.Network/virtualNetworks', 'existing-vnet')]" - let expectedLoadBalancerDeps = - "[resourceId('Microsoft.Network/virtualNetworks', 'existing-vnet')]" + let dependsOn = jobj.SelectToken("resources[?(@.name=='lb')].dependsOn") + Expect.hasLength dependsOn 1 "load balancer has wrong number of dependencies" + let actualLbDeps = (dependsOn :?> Newtonsoft.Json.Linq.JArray).First.ToString() + Expect.equal actualLbDeps expectedLoadBalancerDeps "External dependency didn't match" + } - let dependsOn = jobj.SelectToken("resources[?(@.name=='lb')].dependsOn") - Expect.hasLength dependsOn 1 "load balancer has wrong number of dependencies" - let actualLbDeps = (dependsOn :?> Newtonsoft.Json.Linq.JArray).First.ToString() - Expect.equal actualLbDeps expectedLoadBalancerDeps "External dependency didn't match" - } + test "Load balancer with public ip generates public IP with dependency and matching sku" { + let lb = loadBalancer { + name "lb" + sku Sku.Standard - test "Load balancer with public ip generates public IP with dependency and matching sku" { - let lb = - loadBalancer { - name "lb" - sku Sku.Standard - - add_frontends - [ - frontend { - name "lb-frontend" - public_ip "lb-pip" - } - ] + add_frontends [ + frontend { + name "lb-frontend" + public_ip "lb-pip" } + ] + } - let deployment = arm { add_resource lb } - let json = deployment.Template |> Writer.toJson - let jobj = Newtonsoft.Json.Linq.JObject.Parse(json) - - let expectedLoadBalancerDeps = - "[resourceId('Microsoft.Network/publicIPAddresses', 'lb-pip')]" - - let dependsOn = jobj.SelectToken("resources[?(@.name=='lb')].dependsOn") - Expect.hasLength dependsOn 1 "load balancer has wrong number of dependencies" - let actualLbDeps = (dependsOn :?> Newtonsoft.Json.Linq.JArray).First.ToString() - Expect.equal actualLbDeps expectedLoadBalancerDeps "Public IP dependencies didn't match" - - let resource = - deployment - |> findAzureResources - client.SerializationSettings - |> List.tryFind (fun r -> r.Name = "lb-pip") + let deployment = arm { add_resource lb } + let json = deployment.Template |> Writer.toJson + let jobj = Newtonsoft.Json.Linq.JObject.Parse(json) - let pip = - Expect.wantSome resource "Unable to find generated IP address in resources" + let expectedLoadBalancerDeps = + "[resourceId('Microsoft.Network/publicIPAddresses', 'lb-pip')]" - Expect.equal pip.Name "lb-pip" "Incorrect name for generated public IP address" - Expect.equal pip.Sku.Name "Standard" "Incorrect sku for generated public IP address" - } + let dependsOn = jobj.SelectToken("resources[?(@.name=='lb')].dependsOn") + Expect.hasLength dependsOn 1 "load balancer has wrong number of dependencies" + let actualLbDeps = (dependsOn :?> Newtonsoft.Json.Linq.JArray).First.ToString() + Expect.equal actualLbDeps expectedLoadBalancerDeps "Public IP dependencies didn't match" - let completeLoadBalancer () = - loadBalancer { - name "lb" - sku Sku.Standard - - add_frontends - [ - frontend { - name "lb-frontend" - public_ip "lb-pip" - } - ] - - add_backend_pools - [ - backendAddressPool { - name "lb-backend" - link_to_vnet "my-vnet" - add_ip_addresses [ "10.0.1.4"; "10.0.1.5" ] - } - ] - - add_probes - [ - loadBalancerProbe { - name "httpGet" - protocol LoadBalancerProbeProtocol.HTTP - port 8080 - request_path "/" - } - ] - - add_rules - [ - loadBalancingRule { - name "rule1" - frontend_ip_config "lb-frontend" - backend_address_pool "lb-backend" - frontend_port 80 - backend_port 8080 - protocol TransmissionProtocol.TCP - probe "httpGet" - } - ] - - add_dependencies [ Farmer.Arm.Network.virtualNetworks.resourceId "my-vnet" ] - } + let resource = + deployment + |> findAzureResources + client.SerializationSettings + |> List.tryFind (fun r -> r.Name = "lb-pip") - test "Complete load balancer" { - let found = - arm { add_resource (completeLoadBalancer ()) } - |> findAzureResources - client.SerializationSettings - |> List.tryFind (fun r -> r.Name = "lb") + let pip = + Expect.wantSome resource "Unable to find generated IP address in resources" - let resource = Expect.wantSome found "No 'lb' resource found in template." - Expect.hasLength resource.BackendAddressPools 1 "Incorrect number of backend address pools" + Expect.equal pip.Name "lb-pip" "Incorrect name for generated public IP address" + Expect.equal pip.Sku.Name "Standard" "Incorrect sku for generated public IP address" + } - Expect.equal - resource.BackendAddressPools.[0].Name - "lb-backend" - "Incorrect name for backend address pool" - - Expect.hasLength resource.Probes 1 "Incorrect number of probes" - let probe = resource.Probes |> Seq.head - Expect.equal probe.Name "httpGet" "Incorrect name for httpGet probe" - Expect.equal probe.Protocol "Http" "Incorrect protocol for httpGet probe" - Expect.equal probe.Protocol "Http" "Incorrect protocol for httpGet probe" - Expect.equal probe.RequestPath "/" "Incorrect request path for httpGet probe" - Expect.equal probe.Port 8080 "Incorrect port for httpGet probe" - Expect.equal probe.IntervalInSeconds (Nullable 15) "Incorrect interval for httpGet probe" - Expect.equal probe.NumberOfProbes (Nullable 2) "Incorrect number of probes for httpGet probe" - Expect.hasLength resource.LoadBalancingRules 1 "Incorrect number of load balancing rules" - let rule = resource.LoadBalancingRules |> Seq.head - Expect.equal rule.Name "rule1" "Incorrect name for rule" - Expect.equal rule.FrontendPort 80 "Incorrect frontend port for rule" - Expect.equal rule.BackendPort (Nullable 8080) "Incorrect backend port for rule" - - let backendResourceId = - "[resourceId('Microsoft.Network/loadBalancers/backendAddressPools', 'lb', 'lb-backend')]" - - Expect.equal rule.BackendAddressPool.Id backendResourceId "Incorrect backend address pool for rule" - } + let completeLoadBalancer () = loadBalancer { + name "lb" + sku Sku.Standard - test "Complete load balancer backend pool" { - let found = - arm { add_resource (completeLoadBalancer ()) } - |> findAzureResources - client.SerializationSettings - |> List.tryFind (fun r -> r.Name = "lb/lb-backend") - - let resource = - Expect.wantSome found "No 'lb/lb-backend' resource found in template." + add_frontends [ + frontend { + name "lb-frontend" + public_ip "lb-pip" + } + ] - Expect.equal resource.Name "lb/lb-backend" "Incorrect name for backend address pool" + add_backend_pools [ + backendAddressPool { + name "lb-backend" + link_to_vnet "my-vnet" + add_ip_addresses [ "10.0.1.4"; "10.0.1.5" ] + } + ] + + add_probes [ + loadBalancerProbe { + name "httpGet" + protocol LoadBalancerProbeProtocol.HTTP + port 8080 + request_path "/" + } + ] + + add_rules [ + loadBalancingRule { + name "rule1" + frontend_ip_config "lb-frontend" + backend_address_pool "lb-backend" + frontend_port 80 + backend_port 8080 + protocol TransmissionProtocol.TCP + probe "httpGet" + } + ] + + add_dependencies [ Farmer.Arm.Network.virtualNetworks.resourceId "my-vnet" ] + } + + test "Complete load balancer" { + let found = + arm { add_resource (completeLoadBalancer ()) } + |> findAzureResources + client.SerializationSettings + |> List.tryFind (fun r -> r.Name = "lb") + + let resource = Expect.wantSome found "No 'lb' resource found in template." + Expect.hasLength resource.BackendAddressPools 1 "Incorrect number of backend address pools" + + Expect.equal resource.BackendAddressPools.[0].Name "lb-backend" "Incorrect name for backend address pool" + + Expect.hasLength resource.Probes 1 "Incorrect number of probes" + let probe = resource.Probes |> Seq.head + Expect.equal probe.Name "httpGet" "Incorrect name for httpGet probe" + Expect.equal probe.Protocol "Http" "Incorrect protocol for httpGet probe" + Expect.equal probe.Protocol "Http" "Incorrect protocol for httpGet probe" + Expect.equal probe.RequestPath "/" "Incorrect request path for httpGet probe" + Expect.equal probe.Port 8080 "Incorrect port for httpGet probe" + Expect.equal probe.IntervalInSeconds (Nullable 15) "Incorrect interval for httpGet probe" + Expect.equal probe.NumberOfProbes (Nullable 2) "Incorrect number of probes for httpGet probe" + Expect.hasLength resource.LoadBalancingRules 1 "Incorrect number of load balancing rules" + let rule = resource.LoadBalancingRules |> Seq.head + Expect.equal rule.Name "rule1" "Incorrect name for rule" + Expect.equal rule.FrontendPort 80 "Incorrect frontend port for rule" + Expect.equal rule.BackendPort (Nullable 8080) "Incorrect backend port for rule" + + let backendResourceId = + "[resourceId('Microsoft.Network/loadBalancers/backendAddressPools', 'lb', 'lb-backend')]" + + Expect.equal rule.BackendAddressPool.Id backendResourceId "Incorrect backend address pool for rule" + } + + test "Complete load balancer backend pool" { + let found = + arm { add_resource (completeLoadBalancer ()) } + |> findAzureResources + client.SerializationSettings + |> List.tryFind (fun r -> r.Name = "lb/lb-backend") + + let resource = + Expect.wantSome found "No 'lb/lb-backend' resource found in template." + + Expect.equal resource.Name "lb/lb-backend" "Incorrect name for backend address pool" + } + + test "Backend pool for existing vnet" { + let myVnet = vnet { name "my-vnet" } + + let backendPool = backendAddressPool { + name "backend-services" + load_balancer "existing-lb" + link_to_vnet myVnet + add_ip_addresses [ "10.0.1.4"; "10.0.1.5"; "10.0.1.6" ] } - test "Backend pool for existing vnet" { - let myVnet = vnet { name "my-vnet" } + let template = arm { add_resource backendPool } - let backendPool = - backendAddressPool { - name "backend-services" - load_balancer "existing-lb" - link_to_vnet myVnet - add_ip_addresses [ "10.0.1.4"; "10.0.1.5"; "10.0.1.6" ] - } + let pool = + template.Template.Resources |> Seq.head :?> Farmer.Arm.LoadBalancer.BackendAddressPool - let template = arm { add_resource backendPool } + Expect.equal pool.LoadBalancer (ResourceName "existing-lb") "Pool had incorrect load balancer" - let pool = - template.Template.Resources |> Seq.head :?> Farmer.Arm.LoadBalancer.BackendAddressPool + let expectedVnet = + Unmanaged(Farmer.Arm.Network.virtualNetworks.resourceId (ResourceName "my-vnet")) - Expect.equal pool.LoadBalancer (ResourceName "existing-lb") "Pool had incorrect load balancer" + Expect.hasLength pool.LoadBalancerBackendAddresses 3 "Pool should have 3 addresses" - let expectedVnet = - Unmanaged(Farmer.Arm.Network.virtualNetworks.resourceId (ResourceName "my-vnet")) + pool.LoadBalancerBackendAddresses + |> List.iter (fun addr -> + Expect.equal addr.VirtualNetwork (Some expectedVnet) "Pool did not have expected vnet") + } - Expect.hasLength pool.LoadBalancerBackendAddresses 3 "Pool should have 3 addresses" + test "Setting backend pool on VM NIC" { + let vm1 = vm { + name "webserver1" + vm_size Vm.Standard_B1ms + operating_system Vm.UbuntuServer_2004LTS + public_ip None + username "webserver" + diagnostics_support_managed + link_to_vnet "my-vnet" + subnet_name "my-webservers" - pool.LoadBalancerBackendAddresses - |> List.iter (fun addr -> - Expect.equal addr.VirtualNetwork (Some expectedVnet) "Pool did not have expected vnet") + link_to_backend_address_pool ( + Farmer.Arm.LoadBalancer.loadBalancerBackendAddressPools.resourceId "lb/lb-backend" + ) } - test "Setting backend pool on VM NIC" { - let vm1 = - vm { - name "webserver1" - vm_size Vm.Standard_B1ms - operating_system Vm.UbuntuServer_2004LTS - public_ip None - username "webserver" - diagnostics_support_managed - link_to_vnet "my-vnet" - subnet_name "my-webservers" - - link_to_backend_address_pool ( - Farmer.Arm.LoadBalancer.loadBalancerBackendAddressPools.resourceId "lb/lb-backend" - ) - } - - let template = arm { add_resource vm1 } + let template = arm { add_resource vm1 } - match template.Template.Resources with - | [ resource1; resource2 ] -> - let _ = resource1 :?> Farmer.Arm.Compute.VirtualMachine - let nic = resource2 :?> Farmer.Arm.Network.NetworkInterface + match template.Template.Resources with + | [ resource1; resource2 ] -> + let _ = resource1 :?> Farmer.Arm.Compute.VirtualMachine + let nic = resource2 :?> Farmer.Arm.Network.NetworkInterface - Expect.equal - (Farmer.Arm.LoadBalancer.loadBalancerBackendAddressPools.resourceId "lb/lb-backend") - nic.IpConfigs.[0].LoadBalancerBackendAddressPools.[0].ResourceId - "Backend ID didn't match" - | _ -> failwith "Only expecting two resources in the template." - } - ] + Expect.equal + (Farmer.Arm.LoadBalancer.loadBalancerBackendAddressPools.resourceId "lb/lb-backend") + nic.IpConfigs.[0].LoadBalancerBackendAddressPools.[0].ResourceId + "Backend ID didn't match" + | _ -> failwith "Only expecting two resources in the template." + } + ] diff --git a/src/Tests/LogAnalytics.fs b/src/Tests/LogAnalytics.fs index 1162b2d38..896f1e62f 100644 --- a/src/Tests/LogAnalytics.fs +++ b/src/Tests/LogAnalytics.fs @@ -22,40 +22,37 @@ let asAzureResource (ws: WorkspaceConfig) = r let tests = - testList - "Log analytics" - [ - test "Creates a log analytics workspace" { - let config = - logAnalytics { - name "myFarmer" - retention_period 30 - enable_query - enable_ingestion - } - - let workspace = asAzureResource config - - Expect.equal workspace.Location "westeurope" "Incorrect Location" - Expect.equal workspace.Name "myFarmer" "Incorrect Name" - Expect.equal workspace.PublicNetworkAccessForIngestion "Enabled" "Incorrect IngestionSupport" - Expect.equal workspace.PublicNetworkAccessForQuery "Enabled" "QuerySupport" - Expect.equal workspace.Sku.Name "PerGB2018" "Incorrect Sku" - Expect.equal workspace.RetentionInDays (Nullable 30) "Incorrect Retention In Days" + testList "Log analytics" [ + test "Creates a log analytics workspace" { + let config = logAnalytics { + name "myFarmer" + retention_period 30 + enable_query + enable_ingestion } - test "Ingestion and Query are disabled by default" { - let workspace = logAnalytics { name "" } |> asAzureResource - - Expect.equal workspace.RetentionInDays (Nullable()) "Retention Period should be off by default" - Expect.equal workspace.PublicNetworkAccessForQuery null "Query should be off by default" - Expect.equal workspace.PublicNetworkAccessForIngestion null "Ingestion should be off by default" - } - - test "Can't create log analytics with retention period outside 30 and 730 " { - for days in [ 29; 731 ] do - Expect.throws - (fun _ -> logAnalytics { retention_period days } |> ignore) - (sprintf "Should have thrown for %d" days) - } - ] + let workspace = asAzureResource config + + Expect.equal workspace.Location "westeurope" "Incorrect Location" + Expect.equal workspace.Name "myFarmer" "Incorrect Name" + Expect.equal workspace.PublicNetworkAccessForIngestion "Enabled" "Incorrect IngestionSupport" + Expect.equal workspace.PublicNetworkAccessForQuery "Enabled" "QuerySupport" + Expect.equal workspace.Sku.Name "PerGB2018" "Incorrect Sku" + Expect.equal workspace.RetentionInDays (Nullable 30) "Incorrect Retention In Days" + } + + test "Ingestion and Query are disabled by default" { + let workspace = logAnalytics { name "" } |> asAzureResource + + Expect.equal workspace.RetentionInDays (Nullable()) "Retention Period should be off by default" + Expect.equal workspace.PublicNetworkAccessForQuery null "Query should be off by default" + Expect.equal workspace.PublicNetworkAccessForIngestion null "Ingestion should be off by default" + } + + test "Can't create log analytics with retention period outside 30 and 730 " { + for days in [ 29; 731 ] do + Expect.throws + (fun _ -> logAnalytics { retention_period days } |> ignore) + (sprintf "Should have thrown for %d" days) + } + ] diff --git a/src/Tests/LogicApps.fs b/src/Tests/LogicApps.fs index cca04267c..65eecf4a6 100644 --- a/src/Tests/LogicApps.fs +++ b/src/Tests/LogicApps.fs @@ -23,20 +23,18 @@ let asAzureResource (lac: LogicAppConfig) = r let tests = - testList - "Logic Apps" - [ - test "Creates a logic app workflow" { - let config = logicApp { name "test-logic-app" } - let workflow = asAzureResource config + testList "Logic Apps" [ + test "Creates a logic app workflow" { + let config = logicApp { name "test-logic-app" } + let workflow = asAzureResource config - Expect.equal workflow.Name "test-logic-app" "Incorrect workflow name" - } - test "Populates a value-based logic app definition" { - // this is the required bare minimum for an empty logic app - // for it to not be set to "null" after parsing - let value = - """ + Expect.equal workflow.Name "test-logic-app" "Incorrect workflow name" + } + test "Populates a value-based logic app definition" { + // this is the required bare minimum for an empty logic app + // for it to not be set to "null" after parsing + let value = + """ { "definition": { "$schema": "https://schema.management.azure.com/providers/Microsoft.Logic/schemas/2016-06-01/workflowdefinition.json#", @@ -50,26 +48,24 @@ let tests = } """ - let config = - logicApp { - name "test-logic-app" - definition (ValueDefinition value) - } - - let workflow = asAzureResource config - - Expect.isNotNull workflow.Definition "Did not set logic app definition" + let config = logicApp { + name "test-logic-app" + definition (ValueDefinition value) } - test "Populates a file-based logic app definition" { - let path = "./test-data/blank-logic-app.json" - let config = - logicApp { - name "test-logic-app" - definition (FileDefinition path) - } + let workflow = asAzureResource config - let workflow = asAzureResource config - Expect.isNotNull workflow.Definition "Did not load definition from file" + Expect.isNotNull workflow.Definition "Did not set logic app definition" + } + test "Populates a file-based logic app definition" { + let path = "./test-data/blank-logic-app.json" + + let config = logicApp { + name "test-logic-app" + definition (FileDefinition path) } - ] + + let workflow = asAzureResource config + Expect.isNotNull workflow.Definition "Did not load definition from file" + } + ] diff --git a/src/Tests/Maps.fs b/src/Tests/Maps.fs index 2334b475d..007ab1f60 100644 --- a/src/Tests/Maps.fs +++ b/src/Tests/Maps.fs @@ -13,23 +13,20 @@ let client = new MapsManagementClient(Uri "http://management.azure.com", TokenCredentials "NotNullOrWhiteSpace") let tests = - testList - "Maps" - [ - test "Can create a basic maps account" { - let resource = - let account = - maps { - name "mymaps~@" - sku S0 - } + testList "Maps" [ + test "Can create a basic maps account" { + let resource = + let account = maps { + name "mymaps~@" + sku S0 + } - arm { add_resource account } - |> findAzureResources client.SerializationSettings - |> List.head + arm { add_resource account } + |> findAzureResources client.SerializationSettings + |> List.head - resource.Validate() - Expect.equal resource.Name "mymaps" "" - Expect.equal resource.Sku.Name "S0" "" - } - ] + resource.Validate() + Expect.equal resource.Name "mymaps" "" + Expect.equal resource.Sku.Name "S0" "" + } + ] diff --git a/src/Tests/Network.fs b/src/Tests/Network.fs index c81524468..5b5741364 100644 --- a/src/Tests/Network.fs +++ b/src/Tests/Network.fs @@ -23,809 +23,739 @@ let getPeeringResource = findAzureResources netClient.SerializationSettings let tests = - testList - "Network Tests" - [ - test "Basic vnet with subnets" { - let vnetName = "my-vnet" - let webServerSubnet = "web" - let databaseSubnet = "db" - - let myNet = - vnet { - name vnetName - add_address_spaces [ "10.100.200.0/23" ] - - add_subnets - [ - subnet { - name webServerSubnet - prefix "10.100.200.0/24" - } - subnet { - name databaseSubnet - prefix "10.100.201.0/24" - } - ] + testList "Network Tests" [ + test "Basic vnet with subnets" { + let vnetName = "my-vnet" + let webServerSubnet = "web" + let databaseSubnet = "db" + + let myNet = vnet { + name vnetName + add_address_spaces [ "10.100.200.0/23" ] + + add_subnets [ + subnet { + name webServerSubnet + prefix "10.100.200.0/24" } + subnet { + name databaseSubnet + prefix "10.100.201.0/24" + } + ] + } - let builtVnet = arm { add_resource myNet } |> getVnetResource - Expect.hasLength builtVnet.AddressSpace.AddressPrefixes 1 "Incorrect number of address spaces" - Expect.hasLength builtVnet.Subnets 2 "Incorrect number of subnets" + let builtVnet = arm { add_resource myNet } |> getVnetResource + Expect.hasLength builtVnet.AddressSpace.AddressPrefixes 1 "Incorrect number of address spaces" + Expect.hasLength builtVnet.Subnets 2 "Incorrect number of subnets" - Expect.containsAll - (builtVnet.Subnets |> Seq.map (fun s -> s.Name)) - [ webServerSubnet; databaseSubnet ] - "Incorrect set of subnets" + Expect.containsAll + (builtVnet.Subnets |> Seq.map (fun s -> s.Name)) + [ webServerSubnet; databaseSubnet ] + "Incorrect set of subnets" - Expect.equal builtVnet.Subnets.[0].Name webServerSubnet "Incorrect name for web server subnet" + Expect.equal builtVnet.Subnets.[0].Name webServerSubnet "Incorrect name for web server subnet" - Expect.equal - builtVnet.Subnets.[0].AddressPrefix - "10.100.200.0/24" - "Incorrect prefix for web server subnet" + Expect.equal builtVnet.Subnets.[0].AddressPrefix "10.100.200.0/24" "Incorrect prefix for web server subnet" - Expect.equal builtVnet.Subnets.[1].Name databaseSubnet "Incorrect name for database server subnet" + Expect.equal builtVnet.Subnets.[1].Name databaseSubnet "Incorrect name for database server subnet" - Expect.equal - builtVnet.Subnets.[1].AddressPrefix - "10.100.201.0/24" - "Incorrect prefix for database server subnet" + Expect.equal + builtVnet.Subnets.[1].AddressPrefix + "10.100.201.0/24" + "Incorrect prefix for database server subnet" - Expect.isNull - builtVnet.Subnets.[1].PrivateEndpointNetworkPolicies - "Incorrect PrivateEndpointNetworkPolicies" - } - test "Manually defined subnets with service endpoints" { - let vnetName = "my-vnet" - let servicesSubnet = "services" - let containerSubnet = "containers" + Expect.isNull + builtVnet.Subnets.[1].PrivateEndpointNetworkPolicies + "Incorrect PrivateEndpointNetworkPolicies" + } + test "Manually defined subnets with service endpoints" { + let vnetName = "my-vnet" + let servicesSubnet = "services" + let containerSubnet = "containers" - let myNet = - vnet { - name vnetName - add_address_spaces [ "10.28.0.0/16" ] - - add_subnets - [ - subnet { - name servicesSubnet - prefix "10.28.0.0/24" - - add_service_endpoints - [ - EndpointServiceType.Storage, - [ Location.EastUS; Location.EastUS2; Location.WestUS ] - ] - } - subnet { - name containerSubnet - prefix "10.28.1.0/24" - - add_service_endpoints - [ - EndpointServiceType.Storage, - [ Location.EastUS; Location.EastUS2; Location.WestUS ] - ] - - add_delegations [ SubnetDelegationService.ContainerGroups ] - } - ] - } + let myNet = vnet { + name vnetName + add_address_spaces [ "10.28.0.0/16" ] - let builtVnet = arm { add_resource myNet } |> getVnetResource - Expect.hasLength builtVnet.AddressSpace.AddressPrefixes 1 "Incorrect number of address spaces" - Expect.hasLength builtVnet.Subnets 2 "Incorrect number of subnets" - - Expect.containsAll - (builtVnet.Subnets |> Seq.map (fun s -> s.Name)) - [ servicesSubnet; containerSubnet ] - "Incorrect set of subnets" - - Expect.equal - builtVnet.Subnets.[0].ServiceEndpoints.[0].Service - "Microsoft.Storage" - "Incorrect MS.Storage service endpoint for services subnet" - - Expect.equal - builtVnet.Subnets.[1].ServiceEndpoints.[0].Service - "Microsoft.Storage" - "Incorrect MS.Storage service endpoint for containers subnet" - - Expect.equal - builtVnet.Subnets.[1].Delegations.[0].ServiceName - "Microsoft.ContainerInstance/containerGroups" - "Incorrect MS.ContainerGroups subnet delegation" - } - test "Automatically carved subnets with service endpoints" { - let vnetName = "my-vnet" - let servicesSubnet = "services" - let containerSubnet = "containers" + add_subnets [ + subnet { + name servicesSubnet + prefix "10.28.0.0/24" - let myNet = - vnet { - name vnetName - - build_address_spaces - [ - addressSpace { - space "10.28.0.0/16" - - subnets - [ - subnetSpec { - name servicesSubnet - size 24 - - add_service_endpoints - [ EndpointServiceType.Storage, [ Location.EastUS ] ] - } - subnetSpec { - name containerSubnet - size 24 - add_delegations [ SubnetDelegationService.ContainerGroups ] - - add_service_endpoints - [ EndpointServiceType.Storage, [ Location.EastUS ] ] - } - ] - } - ] + add_service_endpoints [ + EndpointServiceType.Storage, [ Location.EastUS; Location.EastUS2; Location.WestUS ] + ] } + subnet { + name containerSubnet + prefix "10.28.1.0/24" - let generatedVNet = arm { add_resource myNet } |> getVnetResource + add_service_endpoints [ + EndpointServiceType.Storage, [ Location.EastUS; Location.EastUS2; Location.WestUS ] + ] - Expect.containsAll - (generatedVNet.Subnets |> Seq.map (fun s -> s.Name)) - [ servicesSubnet; containerSubnet ] - "Incorrect set of subnets" + add_delegations [ SubnetDelegationService.ContainerGroups ] + } + ] + } - Expect.equal generatedVNet.Subnets.[0].Name servicesSubnet "Incorrect name for services subnet" + let builtVnet = arm { add_resource myNet } |> getVnetResource + Expect.hasLength builtVnet.AddressSpace.AddressPrefixes 1 "Incorrect number of address spaces" + Expect.hasLength builtVnet.Subnets 2 "Incorrect number of subnets" + + Expect.containsAll + (builtVnet.Subnets |> Seq.map (fun s -> s.Name)) + [ servicesSubnet; containerSubnet ] + "Incorrect set of subnets" + + Expect.equal + builtVnet.Subnets.[0].ServiceEndpoints.[0].Service + "Microsoft.Storage" + "Incorrect MS.Storage service endpoint for services subnet" + + Expect.equal + builtVnet.Subnets.[1].ServiceEndpoints.[0].Service + "Microsoft.Storage" + "Incorrect MS.Storage service endpoint for containers subnet" + + Expect.equal + builtVnet.Subnets.[1].Delegations.[0].ServiceName + "Microsoft.ContainerInstance/containerGroups" + "Incorrect MS.ContainerGroups subnet delegation" + } + test "Automatically carved subnets with service endpoints" { + let vnetName = "my-vnet" + let servicesSubnet = "services" + let containerSubnet = "containers" + + let myNet = vnet { + name vnetName + + build_address_spaces [ + addressSpace { + space "10.28.0.0/16" + + subnets [ + subnetSpec { + name servicesSubnet + size 24 + + add_service_endpoints [ EndpointServiceType.Storage, [ Location.EastUS ] ] + } + subnetSpec { + name containerSubnet + size 24 + add_delegations [ SubnetDelegationService.ContainerGroups ] - Expect.equal - generatedVNet.Subnets.[0].AddressPrefix - "10.28.0.0/24" - "Incorrect prefix for services subnet" + add_service_endpoints [ EndpointServiceType.Storage, [ Location.EastUS ] ] + } + ] + } + ] + } - Expect.equal - generatedVNet.Subnets.[0].ServiceEndpoints.[0].Service - "Microsoft.Storage" - "Incorrect MS.Storage service endpoint for services subnet" + let generatedVNet = arm { add_resource myNet } |> getVnetResource - Expect.equal generatedVNet.Subnets.[1].Name containerSubnet "Incorrect name for containers subnet" + Expect.containsAll + (generatedVNet.Subnets |> Seq.map (fun s -> s.Name)) + [ servicesSubnet; containerSubnet ] + "Incorrect set of subnets" - Expect.equal - generatedVNet.Subnets.[1].AddressPrefix - "10.28.1.0/24" - "Incorrect prefix for containers subnet" + Expect.equal generatedVNet.Subnets.[0].Name servicesSubnet "Incorrect name for services subnet" - Expect.equal - generatedVNet.Subnets.[1].ServiceEndpoints.[0].Service - "Microsoft.Storage" - "Incorrect MS.Storage service endpoint for containers subnet" + Expect.equal generatedVNet.Subnets.[0].AddressPrefix "10.28.0.0/24" "Incorrect prefix for services subnet" - Expect.equal - generatedVNet.Subnets.[1].Delegations.[0].ServiceName - "Microsoft.ContainerInstance/containerGroups" - "Incorrect MS.ContainerGroups subnet delegation" + Expect.equal + generatedVNet.Subnets.[0].ServiceEndpoints.[0].Service + "Microsoft.Storage" + "Incorrect MS.Storage service endpoint for services subnet" - Expect.isNull - generatedVNet.Subnets.[1].PrivateEndpointNetworkPolicies - "Incorrect PrivateEndpointNetworkPolicies" - } + Expect.equal generatedVNet.Subnets.[1].Name containerSubnet "Incorrect name for containers subnet" + Expect.equal generatedVNet.Subnets.[1].AddressPrefix "10.28.1.0/24" "Incorrect prefix for containers subnet" - test "Manually defined subnets with private endpoint support" { - let vnetName = "my-vnet" - let servicesSubnet = "services" - let containerSubnet = "containers" + Expect.equal + generatedVNet.Subnets.[1].ServiceEndpoints.[0].Service + "Microsoft.Storage" + "Incorrect MS.Storage service endpoint for containers subnet" - let myNet = - vnet { - name vnetName - add_address_spaces [ "10.28.0.0/16" ] - - add_subnets - [ - subnet { - name servicesSubnet - prefix "10.28.0.0/24" - allow_private_endpoints Enabled - private_link_service_network_policies Disabled - } - ] - } + Expect.equal + generatedVNet.Subnets.[1].Delegations.[0].ServiceName + "Microsoft.ContainerInstance/containerGroups" + "Incorrect MS.ContainerGroups subnet delegation" - let builtVnet = arm { add_resource myNet } |> getVnetResource - Expect.hasLength builtVnet.AddressSpace.AddressPrefixes 1 "Incorrect number of address spaces" - Expect.hasLength builtVnet.Subnets 1 "Incorrect number of subnets" + Expect.isNull + generatedVNet.Subnets.[1].PrivateEndpointNetworkPolicies + "Incorrect PrivateEndpointNetworkPolicies" + } - Expect.equal - builtVnet.Subnets.[0].PrivateEndpointNetworkPolicies - "Disabled" - "Incorrect PrivateEndpointNetworkPolicies" - Expect.equal - builtVnet.Subnets.[0].PrivateLinkServiceNetworkPolicies - "Disabled" - "PrivateLinkServiceNetworkPolicies should be enabled" - } - test "Automatically carved subnets with private endpoint support" { - let vnetName = "my-vnet" - let servicesSubnet = "services" - let containerSubnet = "containers" + test "Manually defined subnets with private endpoint support" { + let vnetName = "my-vnet" + let servicesSubnet = "services" + let containerSubnet = "containers" - let myNet = - vnet { - name vnetName - - build_address_spaces - [ - addressSpace { - space "10.28.0.0/16" - - subnets - [ - subnetSpec { - name servicesSubnet - size 24 - allow_private_endpoints Enabled - private_link_service_network_policies Disabled - } - ] - } - ] - } - - let generatedVNet = arm { add_resource myNet } |> getVnetResource - Expect.equal generatedVNet.Subnets.[0].Name servicesSubnet "Incorrect name for services subnet" - - Expect.equal - generatedVNet.Subnets.[0].AddressPrefix - "10.28.0.0/24" - "Incorrect prefix for services subnet" + let myNet = vnet { + name vnetName + add_address_spaces [ "10.28.0.0/16" ] - Expect.equal - generatedVNet.Subnets.[0].PrivateEndpointNetworkPolicies - "Disabled" - "Incorrect PrivateEndpointNetworkPolicies" - - Expect.equal - generatedVNet.Subnets.[0].PrivateLinkServiceNetworkPolicies - "Disabled" - "Incorrect PrivateEndpointNetworkPolicies" + add_subnets [ + subnet { + name servicesSubnet + prefix "10.28.0.0/24" + allow_private_endpoints Enabled + private_link_service_network_policies Disabled + } + ] } - test "Two VNets with bidirectional peering" { - let vnet1 = vnet { name "vnet1" } - let vnet2 = - vnet { - name "vnet2" - add_peering vnet1 + let builtVnet = arm { add_resource myNet } |> getVnetResource + Expect.hasLength builtVnet.AddressSpace.AddressPrefixes 1 "Incorrect number of address spaces" + Expect.hasLength builtVnet.Subnets 1 "Incorrect number of subnets" + + Expect.equal + builtVnet.Subnets.[0].PrivateEndpointNetworkPolicies + "Disabled" + "Incorrect PrivateEndpointNetworkPolicies" + + Expect.equal + builtVnet.Subnets.[0].PrivateLinkServiceNetworkPolicies + "Disabled" + "PrivateLinkServiceNetworkPolicies should be enabled" + } + test "Automatically carved subnets with private endpoint support" { + let vnetName = "my-vnet" + let servicesSubnet = "services" + let containerSubnet = "containers" + + let myNet = vnet { + name vnetName + + build_address_spaces [ + addressSpace { + space "10.28.0.0/16" + + subnets [ + subnetSpec { + name servicesSubnet + size 24 + allow_private_endpoints Enabled + private_link_service_network_policies Disabled + } + ] } + ] + } - let peerings = - arm { add_resources [ vnet1; vnet2 ] } - |> getPeeringResource - |> List.filter (fun x -> x.Name.Contains("/peering-")) + let generatedVNet = arm { add_resource myNet } |> getVnetResource + Expect.equal generatedVNet.Subnets.[0].Name servicesSubnet "Incorrect name for services subnet" - Expect.hasLength peerings 2 "Incorrect peering count" + Expect.equal generatedVNet.Subnets.[0].AddressPrefix "10.28.0.0/24" "Incorrect prefix for services subnet" - Expect.equal - peerings.[0].RemoteVirtualNetwork.Id - ((virtualNetworks.resourceId (ResourceName "vnet1")).ArmExpression.Eval()) - "remote VNet incorrect" + Expect.equal + generatedVNet.Subnets.[0].PrivateEndpointNetworkPolicies + "Disabled" + "Incorrect PrivateEndpointNetworkPolicies" - Expect.equal - peerings.[1].RemoteVirtualNetwork.Id - ((virtualNetworks.resourceId (ResourceName "vnet2")).ArmExpression.Eval()) - "remote VNet incorrect" + Expect.equal + generatedVNet.Subnets.[0].PrivateLinkServiceNetworkPolicies + "Disabled" + "Incorrect PrivateEndpointNetworkPolicies" + } + test "Two VNets with bidirectional peering" { + let vnet1 = vnet { name "vnet1" } - Expect.equal - (Nullable false) - peerings.[0].AllowGatewayTransit - "Gateway transit should be disabled by default" + let vnet2 = vnet { + name "vnet2" + add_peering vnet1 + } - Expect.equal - (Nullable false) - peerings.[1].AllowGatewayTransit - "Gateway transit should be disabled by default" + let peerings = + arm { add_resources [ vnet1; vnet2 ] } + |> getPeeringResource + |> List.filter (fun x -> x.Name.Contains("/peering-")) + + Expect.hasLength peerings 2 "Incorrect peering count" + + Expect.equal + peerings.[0].RemoteVirtualNetwork.Id + ((virtualNetworks.resourceId (ResourceName "vnet1")).ArmExpression.Eval()) + "remote VNet incorrect" + + Expect.equal + peerings.[1].RemoteVirtualNetwork.Id + ((virtualNetworks.resourceId (ResourceName "vnet2")).ArmExpression.Eval()) + "remote VNet incorrect" + + Expect.equal + (Nullable false) + peerings.[0].AllowGatewayTransit + "Gateway transit should be disabled by default" + + Expect.equal + (Nullable false) + peerings.[1].AllowGatewayTransit + "Gateway transit should be disabled by default" + } + test "Two VNets with one-directional peering" { + let vnet1 = vnet { name "vnet1" } + + let peering = vnetPeering { + remote_vnet vnet1 + direction OneWayToRemote + access AccessOnly + transit UseRemoteGateway } - test "Two VNets with one-directional peering" { - let vnet1 = vnet { name "vnet1" } - - let peering = - vnetPeering { - remote_vnet vnet1 - direction OneWayToRemote - access AccessOnly - transit UseRemoteGateway - } - let vnet2 = - vnet { - name "vnet2" - add_peering peering - } + let vnet2 = vnet { + name "vnet2" + add_peering peering + } - let foundPeerings = - arm { add_resources [ vnet1; vnet2 ] } - |> getPeeringResource - |> List.filter (fun x -> x.Name.Contains("/peering-")) + let foundPeerings = + arm { add_resources [ vnet1; vnet2 ] } + |> getPeeringResource + |> List.filter (fun x -> x.Name.Contains("/peering-")) + + Expect.hasLength foundPeerings 1 "Incorrect peering count" + + Expect.equal + foundPeerings.[0].RemoteVirtualNetwork.Id + ((virtualNetworks.resourceId (ResourceName "vnet1")).ArmExpression.Eval()) + "remote VNet incorrect" + + Expect.equal foundPeerings.[0].AllowVirtualNetworkAccess (Nullable true) "incorrect network access" + Expect.equal foundPeerings.[0].AllowForwardedTraffic (Nullable false) "incorrect forwarding" + Expect.equal foundPeerings.[0].AllowGatewayTransit (Nullable true) "incorrect transit" + Expect.equal foundPeerings.[0].UseRemoteGateways (Nullable true) "incorrect gateway" + } + test "Two VNets with one-directional reverse peering" { + let vnet1 = vnet { name "vnet1" } + + let peering = vnetPeering { + remote_vnet vnet1 + direction OneWayFromRemote + access AccessOnly + transit UseRemoteGateway + } - Expect.hasLength foundPeerings 1 "Incorrect peering count" + let vnet2 = vnet { + name "vnet2" + add_peering peering + } - Expect.equal - foundPeerings.[0].RemoteVirtualNetwork.Id - ((virtualNetworks.resourceId (ResourceName "vnet1")).ArmExpression.Eval()) - "remote VNet incorrect" + let foundPeerings = + arm { add_resources [ vnet1; vnet2 ] } + |> getPeeringResource + |> List.filter (fun x -> x.Name.Contains("/peering-")) + + Expect.hasLength foundPeerings 1 "Incorrect peering count" + + Expect.equal + foundPeerings.[0].RemoteVirtualNetwork.Id + ((virtualNetworks.resourceId (ResourceName "vnet2")).ArmExpression.Eval()) + "remote VNet incorrect" + + Expect.equal foundPeerings.[0].AllowVirtualNetworkAccess (Nullable true) "incorrect network access" + Expect.equal foundPeerings.[0].AllowForwardedTraffic (Nullable false) "incorrect forwarding" + Expect.equal foundPeerings.[0].AllowGatewayTransit (Nullable true) "incorrect transit" + Expect.equal foundPeerings.[0].UseRemoteGateways (Nullable false) "incorrect gateway" + } + test "Automatically carved subnets with network security group support" { + let webPolicy = securityRule { + name "web-servers" + description "Public web server access" + services [ "http", 80; "https", 443 ] + add_source_tag NetworkSecurity.TCP "Internet" + add_destination_network "10.28.0.0/24" + } - Expect.equal foundPeerings.[0].AllowVirtualNetworkAccess (Nullable true) "incorrect network access" - Expect.equal foundPeerings.[0].AllowForwardedTraffic (Nullable false) "incorrect forwarding" - Expect.equal foundPeerings.[0].AllowGatewayTransit (Nullable true) "incorrect transit" - Expect.equal foundPeerings.[0].UseRemoteGateways (Nullable true) "incorrect gateway" + let appPolicy = securityRule { + name "app-servers" + description "Internal app server access" + services [ "http", 8080 ] + add_source_network NetworkSecurity.TCP "10.28.0.0/24" + add_destination_network "10.28.1.0/24" } - test "Two VNets with one-directional reverse peering" { - let vnet1 = vnet { name "vnet1" } - - let peering = - vnetPeering { - remote_vnet vnet1 - direction OneWayFromRemote - access AccessOnly - transit UseRemoteGateway - } - let vnet2 = - vnet { - name "vnet2" - add_peering peering - } + let myNsg = nsg { + name "my-nsg" + add_rules [ webPolicy; appPolicy ] + } - let foundPeerings = - arm { add_resources [ vnet1; vnet2 ] } - |> getPeeringResource - |> List.filter (fun x -> x.Name.Contains("/peering-")) + let vnetName = "my-vnet" + let webSubnet = "web" + let appsSubnet = "apps" + let noNsgSubnet = "no-nsg" - Expect.hasLength foundPeerings 1 "Incorrect peering count" + let myNet = vnet { + name vnetName - Expect.equal - foundPeerings.[0].RemoteVirtualNetwork.Id - ((virtualNetworks.resourceId (ResourceName "vnet2")).ArmExpression.Eval()) - "remote VNet incorrect" + build_address_spaces [ + addressSpace { + space "10.28.0.0/16" - Expect.equal foundPeerings.[0].AllowVirtualNetworkAccess (Nullable true) "incorrect network access" - Expect.equal foundPeerings.[0].AllowForwardedTraffic (Nullable false) "incorrect forwarding" - Expect.equal foundPeerings.[0].AllowGatewayTransit (Nullable true) "incorrect transit" - Expect.equal foundPeerings.[0].UseRemoteGateways (Nullable false) "incorrect gateway" - } - test "Automatically carved subnets with network security group support" { - let webPolicy = - securityRule { - name "web-servers" - description "Public web server access" - services [ "http", 80; "https", 443 ] - add_source_tag NetworkSecurity.TCP "Internet" - add_destination_network "10.28.0.0/24" + subnets [ + subnetSpec { + name webSubnet + size 24 + network_security_group myNsg + } + subnetSpec { + name appsSubnet + size 24 + network_security_group myNsg + } + subnetSpec { + name noNsgSubnet + size 24 + } + ] } + ] + } - let appPolicy = - securityRule { - name "app-servers" - description "Internal app server access" - services [ "http", 8080 ] - add_source_network NetworkSecurity.TCP "10.28.0.0/24" - add_destination_network "10.28.1.0/24" - } + let template = arm { add_resources [ myNet; myNsg ] } + let jobj = template.Template |> Writer.toJson |> Newtonsoft.Json.Linq.JObject.Parse - let myNsg = - nsg { - name "my-nsg" - add_rules [ webPolicy; appPolicy ] - } + let dependencies = + jobj.SelectToken "resources[?(@.type=='Microsoft.Network/virtualNetworks')].dependsOn" + :?> Newtonsoft.Json.Linq.JArray - let vnetName = "my-vnet" - let webSubnet = "web" - let appsSubnet = "apps" - let noNsgSubnet = "no-nsg" + Expect.isNotNull dependencies "vnet missing dependency for nsg" + Expect.hasLength dependencies 1 "Incorrect number of dependencies for vnet" - let myNet = - vnet { - name vnetName - - build_address_spaces - [ - addressSpace { - space "10.28.0.0/16" - - subnets - [ - subnetSpec { - name webSubnet - size 24 - network_security_group myNsg - } - subnetSpec { - name appsSubnet - size 24 - network_security_group myNsg - } - subnetSpec { - name noNsgSubnet - size 24 - } - ] - } - ] - } + Expect.equal + (dependencies.[0].ToString()) + "[resourceId('Microsoft.Network/networkSecurityGroups', 'my-nsg')]" + "Incorrect vnet dependencies" - let template = arm { add_resources [ myNet; myNsg ] } - let jobj = template.Template |> Writer.toJson |> Newtonsoft.Json.Linq.JObject.Parse + let vnet = template |> getVnetResource + Expect.isNotNull vnet.Subnets.[0].NetworkSecurityGroup "First subnet missing NSG" - let dependencies = - jobj.SelectToken "resources[?(@.type=='Microsoft.Network/virtualNetworks')].dependsOn" - :?> Newtonsoft.Json.Linq.JArray + Expect.equal + vnet.Subnets.[0].NetworkSecurityGroup.Id + "[resourceId('Microsoft.Network/networkSecurityGroups', 'my-nsg')]" + "Incorrect security group for first subnet" - Expect.isNotNull dependencies "vnet missing dependency for nsg" - Expect.hasLength dependencies 1 "Incorrect number of dependencies for vnet" + Expect.isNotNull vnet.Subnets.[0].NetworkSecurityGroup "Second subnet missing NSG" - Expect.equal - (dependencies.[0].ToString()) - "[resourceId('Microsoft.Network/networkSecurityGroups', 'my-nsg')]" - "Incorrect vnet dependencies" + Expect.equal + vnet.Subnets.[1].NetworkSecurityGroup.Id + "[resourceId('Microsoft.Network/networkSecurityGroups', 'my-nsg')]" + "Incorrect security group for second subnet" - let vnet = template |> getVnetResource - Expect.isNotNull vnet.Subnets.[0].NetworkSecurityGroup "First subnet missing NSG" + Expect.isNull vnet.Subnets.[2].NetworkSecurityGroup "Third subnet should not have NSG" + } + test "Vnet with linked network security group doesn't add dependsOn" { + let vnetName = "my-vnet" + let webSubnet = "web" - Expect.equal - vnet.Subnets.[0].NetworkSecurityGroup.Id - "[resourceId('Microsoft.Network/networkSecurityGroups', 'my-nsg')]" - "Incorrect security group for first subnet" + let myNet = vnet { + name vnetName - Expect.isNotNull vnet.Subnets.[0].NetworkSecurityGroup "Second subnet missing NSG" + build_address_spaces [ + addressSpace { + space "10.28.0.0/16" - Expect.equal - vnet.Subnets.[1].NetworkSecurityGroup.Id - "[resourceId('Microsoft.Network/networkSecurityGroups', 'my-nsg')]" - "Incorrect security group for second subnet" + subnets [ + subnetSpec { + name webSubnet + size 24 - Expect.isNull vnet.Subnets.[2].NetworkSecurityGroup "Third subnet should not have NSG" + link_to_network_security_group (networkSecurityGroups.resourceId "my-nsg") + } + ] + } + ] } - test "Vnet with linked network security group doesn't add dependsOn" { - let vnetName = "my-vnet" - let webSubnet = "web" - let myNet = - vnet { - name vnetName - - build_address_spaces - [ - addressSpace { - space "10.28.0.0/16" - - subnets - [ - subnetSpec { - name webSubnet - size 24 - - link_to_network_security_group ( - networkSecurityGroups.resourceId "my-nsg" - ) - } - ] - } - ] - } + let template = arm { add_resources [ myNet ] } + let jobj = template.Template |> Writer.toJson |> Newtonsoft.Json.Linq.JObject.Parse - let template = arm { add_resources [ myNet ] } - let jobj = template.Template |> Writer.toJson |> Newtonsoft.Json.Linq.JObject.Parse + let dependencies = + jobj.SelectToken "resources[?(@.type=='Microsoft.Network/virtualNetworks')].dependsOn" + :?> Newtonsoft.Json.Linq.JArray - let dependencies = - jobj.SelectToken "resources[?(@.type=='Microsoft.Network/virtualNetworks')].dependsOn" - :?> Newtonsoft.Json.Linq.JArray + Expect.hasLength dependencies 0 "Should be no vnet dependencies when linking to nsg" - Expect.hasLength dependencies 0 "Should be no vnet dependencies when linking to nsg" + let vnet = template |> getVnetResource + Expect.isNotNull vnet.Subnets.[0].NetworkSecurityGroup "Subnet missing NSG" - let vnet = template |> getVnetResource - Expect.isNotNull vnet.Subnets.[0].NetworkSecurityGroup "Subnet missing NSG" + Expect.equal + vnet.Subnets.[0].NetworkSecurityGroup.Id + "[resourceId('Microsoft.Network/networkSecurityGroups', 'my-nsg')]" + "Incorrect security group for subnet" + } + test "Add subnet linked to managed vnet" { + let vnetName = "my-vnet" + let servicesSubnet = "services" - Expect.equal - vnet.Subnets.[0].NetworkSecurityGroup.Id - "[resourceId('Microsoft.Network/networkSecurityGroups', 'my-nsg')]" - "Incorrect security group for subnet" + let subnetResource = subnet { + name servicesSubnet + link_to_vnet (virtualNetworks.resourceId vnetName) + prefix "10.28.0.0/24" } - test "Add subnet linked to managed vnet" { - let vnetName = "my-vnet" - let servicesSubnet = "services" - let subnetResource = - subnet { - name servicesSubnet - link_to_vnet (virtualNetworks.resourceId vnetName) - prefix "10.28.0.0/24" - } + Expect.equal + ((subnetResource :> IBuilder).ResourceId.Eval()) + "[resourceId('Microsoft.Network/virtualNetworks/subnets', 'my-vnet', 'services')]" + "Incorrect resourceId on subnet" - Expect.equal - ((subnetResource :> IBuilder).ResourceId.Eval()) - "[resourceId('Microsoft.Network/virtualNetworks/subnets', 'my-vnet', 'services')]" - "Incorrect resourceId on subnet" + let template = arm { add_resources [ subnetResource ] } + let jobj = template.Template |> Writer.toJson |> Newtonsoft.Json.Linq.JObject.Parse - let template = arm { add_resources [ subnetResource ] } - let jobj = template.Template |> Writer.toJson |> Newtonsoft.Json.Linq.JObject.Parse + let dependsOn = + jobj.SelectToken "resources[?(@.type=='Microsoft.Network/virtualNetworks/subnets')].dependsOn" + :?> Newtonsoft.Json.Linq.JArray - let dependsOn = - jobj.SelectToken "resources[?(@.type=='Microsoft.Network/virtualNetworks/subnets')].dependsOn" - :?> Newtonsoft.Json.Linq.JArray + Expect.hasLength dependsOn 1 "Linking to managed vnet should have dependency on the vnet" - Expect.hasLength dependsOn 1 "Linking to managed vnet should have dependency on the vnet" + let subnet = + jobj.SelectToken "resources[?(@.type=='Microsoft.Network/virtualNetworks/subnets')].name" - let subnet = - jobj.SelectToken "resources[?(@.type=='Microsoft.Network/virtualNetworks/subnets')].name" + Expect.equal (string subnet) "my-vnet/services" "Incorrect name on subnet" + } + test "Add subnet linked to existing (unmanaged) vnet" { + let vnetName = "my-vnet" + let servicesSubnet = "services" - Expect.equal (string subnet) "my-vnet/services" "Incorrect name on subnet" + let subnetResource = subnet { + name servicesSubnet + link_to_unmanaged_vnet (virtualNetworks.resourceId vnetName) + prefix "10.28.0.0/24" } - test "Add subnet linked to existing (unmanaged) vnet" { - let vnetName = "my-vnet" - let servicesSubnet = "services" - let subnetResource = - subnet { - name servicesSubnet - link_to_unmanaged_vnet (virtualNetworks.resourceId vnetName) - prefix "10.28.0.0/24" + Expect.equal + ((subnetResource :> IBuilder).ResourceId.Eval()) + "[resourceId('Microsoft.Network/virtualNetworks/subnets', 'my-vnet', 'services')]" + "Incorrect resourceId on subnet" + + let template = arm { add_resources [ subnetResource ] } + let jobj = template.Template |> Writer.toJson |> Newtonsoft.Json.Linq.JObject.Parse + + let dependsOn = + jobj.SelectToken "resources[?(@.type=='Microsoft.Network/virtualNetworks/subnets')].dependsOn" + :?> Newtonsoft.Json.Linq.JArray + + Expect.isNull dependsOn "Linking to unmanaged vnet should have no dependencies" + + let subnet = + jobj.SelectToken "resources[?(@.type=='Microsoft.Network/virtualNetworks/subnets')].name" + + Expect.equal (string subnet) "my-vnet/services" "Incorrect name on subnet" + } + test "Standalone subnet without linked vnet not allowed" { + Expect.throws + (fun _ -> + let template = arm { + add_resources [ + subnet { + name "foo" + prefix "10.28.0.0/24" + } + ] } - Expect.equal - ((subnetResource :> IBuilder).ResourceId.Eval()) - "[resourceId('Microsoft.Network/virtualNetworks/subnets', 'my-vnet', 'services')]" - "Incorrect resourceId on subnet" - - let template = arm { add_resources [ subnetResource ] } - let jobj = template.Template |> Writer.toJson |> Newtonsoft.Json.Linq.JObject.Parse + template.Template |> Writer.toJson |> ignore) + "Adding a subnet resource without linking to a vnet is not allowed" + } + test "Creates basic NAT gateway" { + let deployment = arm { + location Location.EastUS - let dependsOn = - jobj.SelectToken "resources[?(@.type=='Microsoft.Network/virtualNetworks/subnets')].dependsOn" - :?> Newtonsoft.Json.Linq.JArray + add_resources [ + natGateway { name "my-nat-gateway" } + vnet { + name "my-net" + add_address_spaces [ "10.100.0.0/16" ] - Expect.isNull dependsOn "Linking to unmanaged vnet should have no dependencies" + add_subnets [ + subnet { + name "my-services" + prefix "10.100.12.0/24" + nat_gateway (natGateways.resourceId "my-nat-gateway") + } + ] + } + ] + } - let subnet = - jobj.SelectToken "resources[?(@.type=='Microsoft.Network/virtualNetworks/subnets')].name" + let jobj = deployment.Template |> Writer.toJson |> JObject.Parse - Expect.equal (string subnet) "my-vnet/services" "Incorrect name on subnet" - } - test "Standalone subnet without linked vnet not allowed" { - Expect.throws - (fun _ -> - let template = - arm { - add_resources - [ - subnet { - name "foo" - prefix "10.28.0.0/24" - } - ] - } + let natGateway = + jobj.SelectToken "resources[?(@.type=='Microsoft.Network/natGateways')]" - template.Template |> Writer.toJson |> ignore) - "Adding a subnet resource without linking to a vnet is not allowed" - } - test "Creates basic NAT gateway" { - let deployment = - arm { - location Location.EastUS - - add_resources - [ - natGateway { name "my-nat-gateway" } - vnet { - name "my-net" - add_address_spaces [ "10.100.0.0/16" ] - - add_subnets - [ - subnet { - name "my-services" - prefix "10.100.12.0/24" - nat_gateway (natGateways.resourceId "my-nat-gateway") - } - ] - } - ] - } + let dependencies = natGateway.["dependsOn"] :?> JArray - let jobj = deployment.Template |> Writer.toJson |> JObject.Parse + Expect.contains + dependencies + (JValue "[resourceId('Microsoft.Network/publicIPAddresses', 'my-nat-gateway-publicip-1')]") + "Missing dependency for public IP" - let natGateway = - jobj.SelectToken "resources[?(@.type=='Microsoft.Network/natGateways')]" + let natGwProps = natGateway.["properties"] + let idleTimeout = natGwProps.["idleTimeoutInMinutes"] + Expect.equal (int idleTimeout) 4 "Incorrect default value for idle timeout" + let ipRefs = natGwProps.["publicIpAddresses"] - let dependencies = natGateway.["dependsOn"] :?> JArray + Expect.equal + (string ipRefs.[0].["id"]) + "[resourceId('Microsoft.Network/publicIPAddresses', 'my-nat-gateway-publicip-1')]" + "IP Addresses did not match" - Expect.contains - dependencies - (JValue "[resourceId('Microsoft.Network/publicIPAddresses', 'my-nat-gateway-publicip-1')]") - "Missing dependency for public IP" + let publicIp = + jobj.SelectToken "resources[?(@.type=='Microsoft.Network/publicIPAddresses')]" - let natGwProps = natGateway.["properties"] - let idleTimeout = natGwProps.["idleTimeoutInMinutes"] - Expect.equal (int idleTimeout) 4 "Incorrect default value for idle timeout" - let ipRefs = natGwProps.["publicIpAddresses"] + Expect.isNotNull publicIp "Public IP should have been generated for the NAT gateway." + } + test "Creates route table with two routes" { + let deployment = arm { + location Location.EastUS - Expect.equal - (string ipRefs.[0].["id"]) - "[resourceId('Microsoft.Network/publicIPAddresses', 'my-nat-gateway-publicip-1')]" - "IP Addresses did not match" + add_resources [ + routeTable { + name "myroutetable" - let publicIp = - jobj.SelectToken "resources[?(@.type=='Microsoft.Network/publicIPAddresses')]" + add_routes [ + route { + name "myroute" + addressPrefix "10.10.90.0/24" + nextHopIpAddress "10.10.67.5" + } + route { + name "myroute2" + addressPrefix "10.10.80.0/24" + } + route { + name "myroute3" + addressPrefix "10.2.31.0/24" + nextHopType (Route.HopType.VirtualAppliance None) + } + route { + name "myroute4" + addressPrefix "10.2.31.0/24" - Expect.isNotNull publicIp "Public IP should have been generated for the NAT gateway." + nextHopType ( + Route.HopType.VirtualAppliance(Some(System.Net.IPAddress.Parse "10.2.31.2")) + ) + } + ] + } + ] } - test "Creates route table with two routes" { - let deployment = - arm { - location Location.EastUS - - add_resources - [ - routeTable { - name "myroutetable" - - add_routes - [ - route { - name "myroute" - addressPrefix "10.10.90.0/24" - nextHopIpAddress "10.10.67.5" - } - route { - name "myroute2" - addressPrefix "10.10.80.0/24" - } - route { - name "myroute3" - addressPrefix "10.2.31.0/24" - nextHopType (Route.HopType.VirtualAppliance None) - } - route { - name "myroute4" - addressPrefix "10.2.31.0/24" - - nextHopType ( - Route.HopType.VirtualAppliance( - Some(System.Net.IPAddress.Parse "10.2.31.2") - ) - ) - } - ] - } - ] + + let jobj = deployment.Template |> Writer.toJson |> JObject.Parse + let a = jobj.ToString() + + let routeTable = + jobj.SelectToken "resources[?(@.type=='Microsoft.Network/routeTables')]" + + let routeTableProps = routeTable.["properties"] + + let disableBgp: bool = + JToken.op_Explicit routeTableProps.["disableBgpRoutePropagation"] + + Expect.equal disableBgp false "Incorrect default value for disableBgpRoutePropagation" + let routes = routeTableProps.["routes"] :?> JArray + Expect.isNotNull routes "Routes should have been generated for the route table" + Expect.equal (string routes.[0].["name"]) "myroute" "route 1 should be named 'myroute'" + Expect.equal (string routes.[1].["name"]) "myroute2" "route 2 should be named 'myroute2'" + Expect.isNull routes.[0].["apiVersion"] "Embedded routes should not have 'apiVersion'." + let routeProps = routes.[0].["properties"] + let route2Props = routes.[1].["properties"] + let route3Props = routes.[2].["properties"] + let route4Props = routes.[3].["properties"] + + Expect.equal + (string routeProps.["nextHopType"]) + "VirtualAppliance" + "route 1 should have a hop type of 'VirtualAppliance'" + + Expect.equal + (string routeProps.["addressPrefix"]) + "10.10.90.0/24" + "route 1 should have an address prefix of '10.10.90.0/24'" + + Expect.isNull route2Props.["nextHopIpAddress"] "route 2 should not have a next hop ip address" + Expect.isNull route3Props.["nextHopIpAddress"] "route 3 should not have a next hop ip address" + + Expect.equal + (string route2Props.["nextHopType"]) + "None" + "route 2 should have the default set to None for nextHopType" + + Expect.equal + (string route4Props.["nextHopIpAddress"]) + "10.2.31.2" + "route 4 should have the next hop ip address set to 10.2.31.2" + } + test "Create private endpoint" { + let myNet = vnet { + name "my-net" + add_address_spaces [ "10.40.0.0/16" ] + + add_subnets [ + subnet { + name "priv-endpoints" + prefix "10.40.255.0/24" + allow_private_endpoints Enabled } + ] + } - let jobj = deployment.Template |> Writer.toJson |> JObject.Parse - let a = jobj.ToString() - - let routeTable = - jobj.SelectToken "resources[?(@.type=='Microsoft.Network/routeTables')]" - - let routeTableProps = routeTable.["properties"] - - let disableBgp: bool = - JToken.op_Explicit routeTableProps.["disableBgpRoutePropagation"] - - Expect.equal disableBgp false "Incorrect default value for disableBgpRoutePropagation" - let routes = routeTableProps.["routes"] :?> JArray - Expect.isNotNull routes "Routes should have been generated for the route table" - Expect.equal (string routes.[0].["name"]) "myroute" "route 1 should be named 'myroute'" - Expect.equal (string routes.[1].["name"]) "myroute2" "route 2 should be named 'myroute2'" - Expect.isNull routes.[0].["apiVersion"] "Embedded routes should not have 'apiVersion'." - let routeProps = routes.[0].["properties"] - let route2Props = routes.[1].["properties"] - let route3Props = routes.[2].["properties"] - let route4Props = routes.[3].["properties"] - - Expect.equal - (string routeProps.["nextHopType"]) - "VirtualAppliance" - "route 1 should have a hop type of 'VirtualAppliance'" - - Expect.equal - (string routeProps.["addressPrefix"]) - "10.10.90.0/24" - "route 1 should have an address prefix of '10.10.90.0/24'" - - Expect.isNull route2Props.["nextHopIpAddress"] "route 2 should not have a next hop ip address" - Expect.isNull route3Props.["nextHopIpAddress"] "route 3 should not have a next hop ip address" - - Expect.equal - (string route2Props.["nextHopType"]) - "None" - "route 2 should have the default set to None for nextHopType" - - Expect.equal - (string route4Props.["nextHopIpAddress"]) - "10.2.31.2" - "route 4 should have the next hop ip address set to 10.2.31.2" + let existingPrivateLinkId = { + PrivateLink.privateLinkServices.resourceId "pls" with + ResourceGroup = Some "farmer-pls" } - test "Create private endpoint" { - let myNet = - vnet { - name "my-net" - add_address_spaces [ "10.40.0.0/16" ] - - add_subnets - [ - subnet { - name "priv-endpoints" - prefix "10.40.255.0/24" - allow_private_endpoints Enabled - } - ] - } - let existingPrivateLinkId = - { PrivateLink.privateLinkServices.resourceId "pls" with - ResourceGroup = Some "farmer-pls" - } + let pe1 = privateEndpoint { + name "pe1" + custom_nic_name "pe1-nic" + link_to_subnet (subnets.resourceId (ResourceName "my-net", ResourceName "priv-endpoints")) + resource (Unmanaged existingPrivateLinkId) + } + + let myDnsZone = dnsZone { + name "farmer.com" + zone_type Dns.Public - let pe1 = - privateEndpoint { + add_records [ + Farmer.Builders.Dns.aRecord { name "pe1" - custom_nic_name "pe1-nic" - link_to_subnet (subnets.resourceId (ResourceName "my-net", ResourceName "priv-endpoints")) - resource (Unmanaged existingPrivateLinkId) - } + ttl 600 - let myDnsZone = - dnsZone { - name "farmer.com" - zone_type Dns.Public - - add_records - [ - Farmer.Builders.Dns.aRecord { - name "pe1" - ttl 600 - - add_ipv4_addresses - [ - pe1.CustomNicFirstEndpointIP - |> Option.map ArmExpression.Eval - |> Option.toObj - ] - } - ] + add_ipv4_addresses [ + pe1.CustomNicFirstEndpointIP |> Option.map ArmExpression.Eval |> Option.toObj + ] } + ] + } - let deployment = - arm { - add_resources - [ - myNet - pe1 - resourceGroup { - name "[resourceGroup().name]" - depends_on pe1 - add_resource myDnsZone - } - ] + let deployment = arm { + add_resources [ + myNet + pe1 + resourceGroup { + name "[resourceGroup().name]" + depends_on pe1 + add_resource myDnsZone } - - let jobj = deployment.Template |> Writer.toJson |> JObject.Parse - let peProps = jobj.SelectToken "resources[?(@.name=='pe1')].properties" - Expect.equal (string peProps.["customNetworkInterfaceName"]) "pe1-nic" "Incorrect custom nic name" - - Expect.equal - (string peProps.["subnet"].["id"]) - "[resourceId('Microsoft.Network/virtualNetworks/subnets', 'my-net', 'priv-endpoints')]" - "Incorrect subnet id" - - Expect.equal - (string peProps.["privateLinkServiceConnections"].[0].["properties"].["privateLinkServiceId"]) - "[resourceId('farmer-pls', 'Microsoft.Network/privateLinkServices', 'pls')]" - "Incorrect private link service ID" + ] } - ] + + let jobj = deployment.Template |> Writer.toJson |> JObject.Parse + let peProps = jobj.SelectToken "resources[?(@.name=='pe1')].properties" + Expect.equal (string peProps.["customNetworkInterfaceName"]) "pe1-nic" "Incorrect custom nic name" + + Expect.equal + (string peProps.["subnet"].["id"]) + "[resourceId('Microsoft.Network/virtualNetworks/subnets', 'my-net', 'priv-endpoints')]" + "Incorrect subnet id" + + Expect.equal + (string peProps.["privateLinkServiceConnections"].[0].["properties"].["privateLinkServiceId"]) + "[resourceId('farmer-pls', 'Microsoft.Network/privateLinkServices', 'pls')]" + "Incorrect private link service ID" + } + ] diff --git a/src/Tests/NetworkSecurityGroup.fs b/src/Tests/NetworkSecurityGroup.fs index cf4ff39ad..cf7d65d6c 100644 --- a/src/Tests/NetworkSecurityGroup.fs +++ b/src/Tests/NetworkSecurityGroup.fs @@ -14,213 +14,200 @@ let client = new NetworkManagementClient(Uri "http://management.azure.com", TokenCredentials "NotNullOrWhiteSpace") let tests = - testList - "NetworkSecurityGroup" - [ - test "Can create a network security group in an ARM template" { - let resource = - let nsg = - { - Name = ResourceName "my-nsg" - Location = Location.WestEurope - SecurityRules = [] - Tags = Map.empty - } - - arm { add_resource nsg } - |> findAzureResources client.SerializationSettings - |> List.head - - Expect.equal resource.Name "my-nsg" "" + testList "NetworkSecurityGroup" [ + test "Can create a network security group in an ARM template" { + let resource = + let nsg = { + Name = ResourceName "my-nsg" + Location = Location.WestEurope + SecurityRules = [] + Tags = Map.empty + } + + arm { add_resource nsg } + |> findAzureResources client.SerializationSettings + |> List.head + + Expect.equal resource.Name "my-nsg" "" + } + test "Can create a network security group with rules in an ARM template" { + let rules = + let nsg = { + Name = ResourceName "my-nsg" + Location = Location.WestEurope + SecurityRules = [] + Tags = Map.empty + } + + let acceptRule = { + Name = ResourceName "accept-web" + Description = Some(sprintf "Rule created on %s" (DateTimeOffset.Now.Date.ToShortDateString())) + SecurityGroup = nsg.Name + Protocol = TCP + SourcePorts = Set [ AnyPort ] + DestinationPorts = Set [ Port 80us; Port 443us ] + SourceAddresses = [ AnyEndpoint ] + DestinationAddresses = [ Network(IPAddressCidr.parse "10.100.30.0/24") ] + Access = Allow + Direction = Inbound + Priority = 100 + } + + arm { + add_resource nsg + add_resource acceptRule + } + |> findAzureResources client.SerializationSettings + + match rules with + | [ _; rule1 ] -> + rule1.Validate() + + Expect.equal rule1.Name "my-nsg/accept-web" "" + Expect.equal rule1.Access "Allow" "" + Expect.equal rule1.DestinationAddressPrefixes.[0] "10.100.30.0/24" "" + Expect.equal rule1.DestinationPortRanges.[0] "80" "" + Expect.equal rule1.DestinationPortRanges.[1] "443" "" + Expect.equal rule1.Direction "Inbound" "" + Expect.equal rule1.Protocol "Tcp" "" + Expect.equal rule1.Priority (Nullable 100) "" + Expect.equal rule1.SourceAddressPrefix "*" "" + Expect.equal rule1.SourcePortRange "*" "" + Expect.equal rule1.SourcePortRanges.Count 0 "" + rule1.Validate() + | _ -> raiseFarmer "Unexpected number of resources in template." + } + test "Policy converted to security rules" { + let webPolicy = securityRule { + name "web-servers" + description "Public web server access" + services [ "http", 80; "https", 443 ] + add_source_tag TCP "Internet" + add_destination_network "10.100.30.0/24" } - test "Can create a network security group with rules in an ARM template" { - let rules = - let nsg = - { - Name = ResourceName "my-nsg" - Location = Location.WestEurope - SecurityRules = [] - Tags = Map.empty - } - - let acceptRule = - { - Name = ResourceName "accept-web" - Description = - Some(sprintf "Rule created on %s" (DateTimeOffset.Now.Date.ToShortDateString())) - SecurityGroup = nsg.Name - Protocol = TCP - SourcePorts = Set [ AnyPort ] - DestinationPorts = Set [ Port 80us; Port 443us ] - SourceAddresses = [ AnyEndpoint ] - DestinationAddresses = [ Network(IPAddressCidr.parse "10.100.30.0/24") ] - Access = Allow - Direction = Inbound - Priority = 100 - } - - arm { - add_resource nsg - add_resource acceptRule - } - |> findAzureResources client.SerializationSettings - - match rules with - | [ _; rule1 ] -> - rule1.Validate() - - Expect.equal rule1.Name "my-nsg/accept-web" "" - Expect.equal rule1.Access "Allow" "" - Expect.equal rule1.DestinationAddressPrefixes.[0] "10.100.30.0/24" "" - Expect.equal rule1.DestinationPortRanges.[0] "80" "" - Expect.equal rule1.DestinationPortRanges.[1] "443" "" - Expect.equal rule1.Direction "Inbound" "" - Expect.equal rule1.Protocol "Tcp" "" - Expect.equal rule1.Priority (Nullable 100) "" - Expect.equal rule1.SourceAddressPrefix "*" "" - Expect.equal rule1.SourcePortRange "*" "" - Expect.equal rule1.SourcePortRanges.Count 0 "" - rule1.Validate() - | _ -> raiseFarmer "Unexpected number of resources in template." + + let myNsg = nsg { + name "my-nsg" + add_rules [ webPolicy ] + } + + let nsg = + arm { add_resource myNsg } + |> findAzureResources client.SerializationSettings + + match nsg.Head.SecurityRules |> List.ofSeq with + | [ rule1 ] -> + rule1.Validate() + Expect.equal rule1.Name "web-servers" "" + Expect.equal rule1.Access "Allow" "" + Expect.equal rule1.DestinationAddressPrefixes.[0] "10.100.30.0/24" "" + Expect.equal rule1.DestinationPortRanges.[0] "80" "" + Expect.equal rule1.DestinationPortRanges.[1] "443" "" + Expect.equal rule1.Direction "Inbound" "" + Expect.equal rule1.Protocol "Tcp" "" + Expect.equal rule1.Priority (Nullable 100) "" + Expect.equal rule1.SourceAddressPrefix "Internet" "" + Expect.equal rule1.SourcePortRange "*" "" + Expect.equal rule1.SourcePortRanges.Count 0 "" + | _ -> raiseFarmer "Unexpected number of resources in template." + } + test "Multitier Policy converted to security rules" { + let appNet = "10.100.31.0/24" + let dbNet = "10.100.32.0/24" + + let webPolicy = securityRule { // Web servers - accessible from anything + name "web-servers" + description "Public web server access" + services [ "http", 80; "https", 443 ] + add_source_tag TCP "Internet" + add_destination_network "10.100.30.0/24" + } + + let appPolicy = securityRule { // Only accessible by web servers + name "app-servers" + description "Internal app server access" + services [ "http", 8080 ] + add_source_network TCP "10.100.30.0/24" + add_destination_network appNet } - test "Policy converted to security rules" { - let webPolicy = - securityRule { - name "web-servers" - description "Public web server access" - services [ "http", 80; "https", 443 ] - add_source_tag TCP "Internet" - add_destination_network "10.100.30.0/24" - } - - let myNsg = - nsg { - name "my-nsg" - add_rules [ webPolicy ] - } - - let nsg = - arm { add_resource myNsg } - |> findAzureResources client.SerializationSettings - - match nsg.Head.SecurityRules |> List.ofSeq with - | [ rule1 ] -> - rule1.Validate() - Expect.equal rule1.Name "web-servers" "" - Expect.equal rule1.Access "Allow" "" - Expect.equal rule1.DestinationAddressPrefixes.[0] "10.100.30.0/24" "" - Expect.equal rule1.DestinationPortRanges.[0] "80" "" - Expect.equal rule1.DestinationPortRanges.[1] "443" "" - Expect.equal rule1.Direction "Inbound" "" - Expect.equal rule1.Protocol "Tcp" "" - Expect.equal rule1.Priority (Nullable 100) "" - Expect.equal rule1.SourceAddressPrefix "Internet" "" - Expect.equal rule1.SourcePortRange "*" "" - Expect.equal rule1.SourcePortRanges.Count 0 "" - | _ -> raiseFarmer "Unexpected number of resources in template." + + let dbPolicy = securityRule { // DB servers - not accessible by web, only by app servers + name "db-servers" + description "Internal database server access" + services [ "postgres", 5432 ] + add_source_network TCP appNet + add_destination_network dbNet } - test "Multitier Policy converted to security rules" { - let appNet = "10.100.31.0/24" - let dbNet = "10.100.32.0/24" - - let webPolicy = - securityRule { // Web servers - accessible from anything - name "web-servers" - description "Public web server access" - services [ "http", 80; "https", 443 ] - add_source_tag TCP "Internet" - add_destination_network "10.100.30.0/24" - } - - let appPolicy = - securityRule { // Only accessible by web servers - name "app-servers" - description "Internal app server access" - services [ "http", 8080 ] - add_source_network TCP "10.100.30.0/24" - add_destination_network appNet - } - - let dbPolicy = - securityRule { // DB servers - not accessible by web, only by app servers - name "db-servers" - description "Internal database server access" - services [ "postgres", 5432 ] - add_source_network TCP appNet - add_destination_network dbNet - } - - let blockOutbound = - securityRule { - name "no-internet" - description "Block traffic out to internet" - add_source_network AnyProtocol appNet - add_source_network AnyProtocol dbNet - add_destination_tag "Internet" - direction Outbound - deny_traffic - } - - let myNsg = - nsg { - name "my-nsg" - add_rules [ webPolicy; appPolicy; dbPolicy; blockOutbound ] - initial_rule_priority 1000 - priority_incr 50 - } - - let nsg = - arm { add_resource myNsg } - |> findAzureResources client.SerializationSettings - - match nsg.Head.SecurityRules |> List.ofSeq with - | [ rule1; rule2; rule3; rule4 ] -> - // Web server access - rule1.Validate() - Expect.equal rule1.Name "web-servers" "" - Expect.equal rule1.Access "Allow" "" - Expect.equal rule1.DestinationAddressPrefixes.[0] "10.100.30.0/24" "" - Expect.equal rule1.DestinationPortRanges.[0] "80" "" - Expect.equal rule1.DestinationPortRanges.[1] "443" "" - Expect.equal rule1.Direction "Inbound" "" - Expect.equal rule1.Protocol "Tcp" "" - Expect.equal rule1.Priority (Nullable 1000) "" - Expect.equal rule1.SourceAddressPrefix "Internet" "" - Expect.equal rule1.SourceAddressPrefixes.Count 0 "" - Expect.equal rule1.SourcePortRange "*" "" - Expect.equal rule1.SourcePortRanges.Count 0 "" - // App server access - rule2.Validate() - Expect.equal rule2.Name "app-servers" "" - Expect.equal rule2.Access "Allow" "" - Expect.equal rule2.DestinationAddressPrefixes.[0] "10.100.31.0/24" "" - Expect.equal rule2.DestinationPortRanges.[0] "8080" "" - Expect.equal rule2.Direction "Inbound" "" - Expect.equal rule2.Protocol "Tcp" "" - Expect.equal rule2.Priority (Nullable 1050) "" - Expect.equal rule2.SourceAddressPrefixes.[0] "10.100.30.0/24" "" - Expect.equal rule2.SourcePortRanges.Count 0 "" - // DB server access - rule3.Validate() - Expect.equal rule3.Name "db-servers" "" - Expect.equal rule3.Access "Allow" "" - Expect.equal rule3.DestinationAddressPrefixes.[0] "10.100.32.0/24" "" - Expect.equal rule3.DestinationPortRanges.[0] "5432" "" - Expect.equal rule3.Direction "Inbound" "" - Expect.equal rule3.Protocol "Tcp" "" - Expect.equal rule3.Priority (Nullable 1100) "" - Expect.equal rule3.SourceAddressPrefixes.[0] "10.100.31.0/24" "" - Expect.equal rule3.SourcePortRanges.Count 0 "" - // Block Internet access - rule4.Validate() - Expect.equal rule4.Name "no-internet" "" - Expect.equal rule4.Access "Deny" "" - Expect.equal rule4.DestinationAddressPrefix "Internet" "" - Expect.equal rule4.DestinationPortRange "*" "" - Expect.equal rule4.Direction "Outbound" "" - Expect.equal rule4.Protocol "*" "" - Expect.equal rule4.Priority (Nullable 1150) "" - Expect.containsAll rule4.SourceAddressPrefixes [ "10.100.31.0/24"; "10.100.32.0/24" ] "" - | _ -> raiseFarmer "Unexpected number of resources in template." + + let blockOutbound = securityRule { + name "no-internet" + description "Block traffic out to internet" + add_source_network AnyProtocol appNet + add_source_network AnyProtocol dbNet + add_destination_tag "Internet" + direction Outbound + deny_traffic } - ] + + let myNsg = nsg { + name "my-nsg" + add_rules [ webPolicy; appPolicy; dbPolicy; blockOutbound ] + initial_rule_priority 1000 + priority_incr 50 + } + + let nsg = + arm { add_resource myNsg } + |> findAzureResources client.SerializationSettings + + match nsg.Head.SecurityRules |> List.ofSeq with + | [ rule1; rule2; rule3; rule4 ] -> + // Web server access + rule1.Validate() + Expect.equal rule1.Name "web-servers" "" + Expect.equal rule1.Access "Allow" "" + Expect.equal rule1.DestinationAddressPrefixes.[0] "10.100.30.0/24" "" + Expect.equal rule1.DestinationPortRanges.[0] "80" "" + Expect.equal rule1.DestinationPortRanges.[1] "443" "" + Expect.equal rule1.Direction "Inbound" "" + Expect.equal rule1.Protocol "Tcp" "" + Expect.equal rule1.Priority (Nullable 1000) "" + Expect.equal rule1.SourceAddressPrefix "Internet" "" + Expect.equal rule1.SourceAddressPrefixes.Count 0 "" + Expect.equal rule1.SourcePortRange "*" "" + Expect.equal rule1.SourcePortRanges.Count 0 "" + // App server access + rule2.Validate() + Expect.equal rule2.Name "app-servers" "" + Expect.equal rule2.Access "Allow" "" + Expect.equal rule2.DestinationAddressPrefixes.[0] "10.100.31.0/24" "" + Expect.equal rule2.DestinationPortRanges.[0] "8080" "" + Expect.equal rule2.Direction "Inbound" "" + Expect.equal rule2.Protocol "Tcp" "" + Expect.equal rule2.Priority (Nullable 1050) "" + Expect.equal rule2.SourceAddressPrefixes.[0] "10.100.30.0/24" "" + Expect.equal rule2.SourcePortRanges.Count 0 "" + // DB server access + rule3.Validate() + Expect.equal rule3.Name "db-servers" "" + Expect.equal rule3.Access "Allow" "" + Expect.equal rule3.DestinationAddressPrefixes.[0] "10.100.32.0/24" "" + Expect.equal rule3.DestinationPortRanges.[0] "5432" "" + Expect.equal rule3.Direction "Inbound" "" + Expect.equal rule3.Protocol "Tcp" "" + Expect.equal rule3.Priority (Nullable 1100) "" + Expect.equal rule3.SourceAddressPrefixes.[0] "10.100.31.0/24" "" + Expect.equal rule3.SourcePortRanges.Count 0 "" + // Block Internet access + rule4.Validate() + Expect.equal rule4.Name "no-internet" "" + Expect.equal rule4.Access "Deny" "" + Expect.equal rule4.DestinationAddressPrefix "Internet" "" + Expect.equal rule4.DestinationPortRange "*" "" + Expect.equal rule4.Direction "Outbound" "" + Expect.equal rule4.Protocol "*" "" + Expect.equal rule4.Priority (Nullable 1150) "" + Expect.containsAll rule4.SourceAddressPrefixes [ "10.100.31.0/24"; "10.100.32.0/24" ] "" + | _ -> raiseFarmer "Unexpected number of resources in template." + } + ] diff --git a/src/Tests/OperationsManagement.fs b/src/Tests/OperationsManagement.fs index 4f59cacaf..113029263 100644 --- a/src/Tests/OperationsManagement.fs +++ b/src/Tests/OperationsManagement.fs @@ -7,84 +7,79 @@ open Farmer.Builders open Newtonsoft.Json.Linq let tests = - testList - "Operations Management Solution" - [ - test "Generates an operations management soluution" { - - let sentinelWorkspace = - logAnalytics { - name "my-sentinel-workspace" - retention_period 30 - enable_query - daily_cap 5 - } + testList "Operations Management Solution" [ + test "Generates an operations management soluution" { + + let sentinelWorkspace = logAnalytics { + name "my-sentinel-workspace" + retention_period 30 + enable_query + daily_cap 5 + } - let omsName = $"SecurityInsights({sentinelWorkspace.Name.Value})" + let omsName = $"SecurityInsights({sentinelWorkspace.Name.Value})" - let sentinelSolution = - oms { - name omsName + let sentinelSolution = oms { + name omsName - plan ( - omsPlan { - name omsName - publisher "Microsoft" - product "OMSGallery/SecurityInsights" - } - ) - - properties (omsProperties { workspace sentinelWorkspace }) + plan ( + omsPlan { + name omsName + publisher "Microsoft" + product "OMSGallery/SecurityInsights" } + ) - let deployment = - arm { - location Location.EastUS - add_resources [ sentinelWorkspace; sentinelSolution ] - } + properties (omsProperties { workspace sentinelWorkspace }) + } - let jobj = deployment.Template |> Writer.toJson |> JObject.Parse + let deployment = arm { + location Location.EastUS + add_resources [ sentinelWorkspace; sentinelSolution ] + } - let workspaceResource = - jobj.SelectToken("resources[?(@.name=='my-sentinel-workspace')]") + let jobj = deployment.Template |> Writer.toJson |> JObject.Parse - Expect.equal - (string (workspaceResource.["type"])) - "Microsoft.OperationalInsights/workspaces" - "Incorrect type for OMS workspace" + let workspaceResource = + jobj.SelectToken("resources[?(@.name=='my-sentinel-workspace')]") - let workspaceProperties = workspaceResource.["properties"] + Expect.equal + (string (workspaceResource.["type"])) + "Microsoft.OperationalInsights/workspaces" + "Incorrect type for OMS workspace" - Expect.equal - (string (workspaceProperties.["publicNetworkAccessForQuery"])) - "Enabled" - "Incorrect public network access" + let workspaceProperties = workspaceResource.["properties"] - Expect.equal (int (workspaceProperties.["retentionInDays"])) 30 "Incorrect retention" + Expect.equal + (string (workspaceProperties.["publicNetworkAccessForQuery"])) + "Enabled" + "Incorrect public network access" - let solutionResource = - jobj.SelectToken("resources[?(@.name=='SecurityInsights(my-sentinel-workspace)')]") + Expect.equal (int (workspaceProperties.["retentionInDays"])) 30 "Incorrect retention" - Expect.equal - (string (solutionResource.["type"])) - "Microsoft.OperationsManagement/solutions" - "Incorrect type for OMS solution" + let solutionResource = + jobj.SelectToken("resources[?(@.name=='SecurityInsights(my-sentinel-workspace)')]") - Expect.hasLength - (solutionResource.["dependsOn"] :?> JArray) - 1 - "oms solution has incorrect number of dependencies" + Expect.equal + (string (solutionResource.["type"])) + "Microsoft.OperationsManagement/solutions" + "Incorrect type for OMS solution" - Expect.equal - (string (solutionResource.["dependsOn"].[0])) - "[resourceId('Microsoft.OperationalInsights/workspaces', 'my-sentinel-workspace')]" - "oms solution has incorrect dependency" + Expect.hasLength + (solutionResource.["dependsOn"] :?> JArray) + 1 + "oms solution has incorrect number of dependencies" - let solutionProperties = solutionResource.["properties"] + Expect.equal + (string (solutionResource.["dependsOn"].[0])) + "[resourceId('Microsoft.OperationalInsights/workspaces', 'my-sentinel-workspace')]" + "oms solution has incorrect dependency" - Expect.equal - (string (solutionProperties.["workspaceResourceId"])) - "[resourceId('Microsoft.OperationalInsights/workspaces', 'my-sentinel-workspace')]" - "Incorrect solution workspace resource Id" - } - ] + let solutionProperties = solutionResource.["properties"] + + Expect.equal + (string (solutionProperties.["workspaceResourceId"])) + "[resourceId('Microsoft.OperationalInsights/workspaces', 'my-sentinel-workspace')]" + "Incorrect solution workspace resource Id" + } + ] diff --git a/src/Tests/PostgreSQL.fs b/src/Tests/PostgreSQL.fs index f4d2c735a..56ee91182 100644 --- a/src/Tests/PostgreSQL.fs +++ b/src/Tests/PostgreSQL.fs @@ -8,76 +8,71 @@ open Farmer.PostgreSQL open Farmer.Builders open Farmer.Arm -type PostgresSku = - { - name: string - family: string - capacity: int - tier: string - size: string - } - - -type StorageProfile = - { - backupRetentionDays: int - geoRedundantBackup: string - storageAutoGrow: string - storageMB: int - } - -type Properties = - { - administratorLogin: string - administratorLoginPassword: string - version: string - storageProfile: StorageProfile - } - - -type PostgresTemplate = - { - name: string - ``type``: string - apiVersion: string - sku: PostgresSku - location: string - geoRedundantBackup: string - resources: obj array - properties: Properties - } +type PostgresSku = { + name: string + family: string + capacity: int + tier: string + size: string +} + + +type StorageProfile = { + backupRetentionDays: int + geoRedundantBackup: string + storageAutoGrow: string + storageMB: int +} + +type Properties = { + administratorLogin: string + administratorLoginPassword: string + version: string + storageProfile: StorageProfile +} + + +type PostgresTemplate = { + name: string + ``type``: string + apiVersion: string + sku: PostgresSku + location: string + geoRedundantBackup: string + resources: obj array + properties: Properties +} type Dependencies = string array -type DatabaseResource = - { - name: string - ``type``: string - apiVersion: string - properties: {| collation: string; charset: string |} - dependsOn: string array - } - -type FirewallResource = - { - name: string - apiVersion: string - ``type``: string - dependsOn: string array - properties: {| endIpAddress: string - startIpAddress: string |} - location: string - } - -type VnetResource = - { - name: string - apiVersion: string - ``type``: string - dependsOn: string array - properties: {| virtualNetworkSubnetId: string |} - location: string - } +type DatabaseResource = { + name: string + ``type``: string + apiVersion: string + properties: {| collation: string; charset: string |} + dependsOn: string array +} + +type FirewallResource = { + name: string + apiVersion: string + ``type``: string + dependsOn: string array + properties: {| + endIpAddress: string + startIpAddress: string + |} + location: string +} + +type VnetResource = { + name: string + apiVersion: string + ``type``: string + dependsOn: string array + properties: {| virtualNetworkSubnetId: string |} + location: string +} let runBuilder<'T> = toTypedTemplate<'T> Location.NorthEurope @@ -95,407 +90,378 @@ module Expect = | Some msg -> failtestf "%s. Expected f to not throw, but it did. Exception message: %s" message msg let tests = - testList - "PostgreSQL Database Service" - [ - test "Server settings are correct" { - let actual = - runBuilder - <| postgreSQL { - name "testdb" - admin_username "myadminuser" - server_version VS_10 - storage_size 50 - backup_retention 17 - capacity 4 - tier Sku.GeneralPurpose - enable_geo_redundant_backup - disable_storage_autogrow - } - - Expect.equal actual.apiVersion "2017-12-01" "apiVersion" - Expect.equal actual.``type`` "Microsoft.DBforPostgreSQL/servers" "type" - Expect.equal actual.sku.name "GP_Gen5_4" "sku name" - Expect.equal actual.sku.family "Gen5" "sku family" - Expect.equal actual.sku.capacity 4 "sku capacity" - Expect.equal actual.sku.tier "GeneralPurpose" "sku tier" - Expect.equal actual.sku.size "51200" "sku size" - Expect.equal actual.properties.administratorLogin "myadminuser" "Admin user prop" - - Expect.equal - actual.properties.administratorLoginPassword - "[parameters('password-for-testdb')]" - "Admin password prop" - - Expect.equal actual.properties.version "10" "server version" - Expect.equal actual.properties.storageProfile.geoRedundantBackup "Enabled" "geo backup" - Expect.equal actual.properties.storageProfile.storageAutoGrow "Disabled" "storage autogrow" - Expect.equal actual.properties.storageProfile.backupRetentionDays 17 "backup retention" + testList "PostgreSQL Database Service" [ + test "Server settings are correct" { + let actual = + runBuilder + <| postgreSQL { + name "testdb" + admin_username "myadminuser" + server_version VS_10 + storage_size 50 + backup_retention 17 + capacity 4 + tier Sku.GeneralPurpose + enable_geo_redundant_backup + disable_storage_autogrow + } + + Expect.equal actual.apiVersion "2017-12-01" "apiVersion" + Expect.equal actual.``type`` "Microsoft.DBforPostgreSQL/servers" "type" + Expect.equal actual.sku.name "GP_Gen5_4" "sku name" + Expect.equal actual.sku.family "Gen5" "sku family" + Expect.equal actual.sku.capacity 4 "sku capacity" + Expect.equal actual.sku.tier "GeneralPurpose" "sku tier" + Expect.equal actual.sku.size "51200" "sku size" + Expect.equal actual.properties.administratorLogin "myadminuser" "Admin user prop" + + Expect.equal + actual.properties.administratorLoginPassword + "[parameters('password-for-testdb')]" + "Admin password prop" + + Expect.equal actual.properties.version "10" "server version" + Expect.equal actual.properties.storageProfile.geoRedundantBackup "Enabled" "geo backup" + Expect.equal actual.properties.storageProfile.storageAutoGrow "Disabled" "storage autogrow" + Expect.equal actual.properties.storageProfile.backupRetentionDays 17 "backup retention" + } + + test "Database settings are correct" { + let db = postgreSQLDb { + name "my_db" + collation "de_DE" + charset "ASCII" } - test "Database settings are correct" { - let db = - postgreSQLDb { - name "my_db" - collation "de_DE" - charset "ASCII" - } - - let actual = - postgreSQL { - name "testdb" - admin_username "myadminuser" - add_database db - } - - let actual = - actual - |> toTemplate Location.NorthEurope - |> Writer.toJson - |> Serialization.ofJson> - |> fun r -> r.Resources - |> Seq.find (fun r -> r.name = "testdb/my_db") - - let expectedDbRes = - { - name = "testdb/my_db" - apiVersion = "2017-12-01" - ``type`` = "Microsoft.DBforPostgreSQL/servers/databases" - properties = - {| - collation = "de_DE" - charset = "ASCII" - |} - dependsOn = [| "[resourceId('Microsoft.DBforPostgreSQL/servers', 'testdb')]" |] - } - - Expect.equal actual expectedDbRes "database resource" + let actual = postgreSQL { + name "testdb" + admin_username "myadminuser" + add_database db } - test "Firewall rules are correctly set" { - let actual = - postgreSQL { - name "testdb" - admin_username "myadminuser" - enable_azure_firewall - } - - let actual = - actual - |> toTemplate Location.NorthEurope - |> Writer.toJson - |> Serialization.ofJson> - |> fun r -> r.Resources - |> Seq.find (fun r -> r.name = "testdb/allow-azure-services") - - let expectedFwRuleRes: FirewallResource = - { - name = "testdb/allow-azure-services" - ``type`` = "Microsoft.DBforPostgreSQL/servers/firewallrules" - apiVersion = "2017-12-01" - dependsOn = [| "[resourceId('Microsoft.DBforPostgreSQL/servers', 'testdb')]" |] - location = "northeurope" - properties = - {| - startIpAddress = "0.0.0.0" - endIpAddress = "0.0.0.0" - |} - } - - Expect.equal actual expectedFwRuleRes "Firewall is incorrect" + let actual = + actual + |> toTemplate Location.NorthEurope + |> Writer.toJson + |> Serialization.ofJson> + |> fun r -> r.Resources + |> Seq.find (fun r -> r.name = "testdb/my_db") + + let expectedDbRes = { + name = "testdb/my_db" + apiVersion = "2017-12-01" + ``type`` = "Microsoft.DBforPostgreSQL/servers/databases" + properties = {| + collation = "de_DE" + charset = "ASCII" + |} + dependsOn = [| "[resourceId('Microsoft.DBforPostgreSQL/servers', 'testdb')]" |] } - test "Vnet rule are correctly set" { - let subscriptionId = "sid-subid" - let resourceGroup = "rg-abc" - let vnetName = "vnetid" - let subnetName = "default" - - let networkResourceId = - { - Type = subnets - ResourceGroup = Some resourceGroup - Subscription = Some subscriptionId - Name = ResourceName vnetName - Segments = [ ResourceName subnetName ] - } - - let networkResourceIdString = networkResourceId.Eval() - let vnetRuleName = "vnet-rule-name" - - let actual = - postgreSQL { - name "testdb" - admin_username "myadminuser" - add_vnet_rule vnetRuleName networkResourceId - } - - let actual = - actual - |> toTemplate Location.NorthEurope - |> Writer.toJson - |> Serialization.ofJson> - |> fun r -> r.Resources - |> Seq.find (fun r -> r.name = $"testdb/%s{vnetRuleName}") - - let expectedVnetRuleResult: VnetResource = - { - name = $"testdb/%s{vnetRuleName}" - ``type`` = "Microsoft.DBforPostgreSQL/servers/virtualNetworkRules" - apiVersion = "2017-12-01" - dependsOn = [| "[resourceId('Microsoft.DBforPostgreSQL/servers', 'testdb')]" |] - location = "northeurope" - properties = - {| - virtualNetworkSubnetId = networkResourceIdString - |} - } - - Expect.equal actual expectedVnetRuleResult "Vnet is incorrect" - } + Expect.equal actual expectedDbRes "database resource" + } - test "Vnet rules are correctly set" { - let subscriptionId = "sid-subid" - let resourceGroup = "rg-abc" - - let vnetName1 = "vnetid1" - let subnetName1 = "default1" - - let networkResourceId1 = - { - Type = subnets - ResourceGroup = Some resourceGroup - Subscription = Some subscriptionId - Name = ResourceName vnetName1 - Segments = [ ResourceName subnetName1 ] - } - - let networkResourceId1String = networkResourceId1.Eval() - let vnetRuleName1 = "vnet-rule-name1" - - let vnetName2 = "vnetid2" - let subnetName2 = "default2" - - let networkResourceId2 = - { - Type = subnets - ResourceGroup = Some resourceGroup - Subscription = Some subscriptionId - Name = ResourceName vnetName2 - Segments = [ ResourceName subnetName2 ] - } - - let networkResourceId2String = networkResourceId2.Eval() - let vnetRuleName2 = "vnet-rule-name2" - - let actual = - postgreSQL { - name "testdb" - admin_username "myadminuser" - add_vnet_rules [ vnetRuleName1, networkResourceId1; vnetRuleName2, networkResourceId2 ] - } - - let actual = - actual - |> toTemplate Location.NorthEurope - |> Writer.toJson - |> Serialization.ofJson> - |> fun r -> r.Resources - - let actual1 = actual |> Seq.find (fun r -> r.name = $"testdb/%s{vnetRuleName1}") - let actual2 = actual |> Seq.find (fun r -> r.name = $"testdb/%s{vnetRuleName2}") - - let expectedVnetRuleResult1: VnetResource = - { - name = $"testdb/%s{vnetRuleName1}" - ``type`` = "Microsoft.DBforPostgreSQL/servers/virtualNetworkRules" - apiVersion = "2017-12-01" - dependsOn = [| "[resourceId('Microsoft.DBforPostgreSQL/servers', 'testdb')]" |] - location = "northeurope" - properties = - {| - virtualNetworkSubnetId = networkResourceId1String - |} - } - - let expectedVnetRuleResult2: VnetResource = - { - name = $"testdb/%s{vnetRuleName2}" - ``type`` = "Microsoft.DBforPostgreSQL/servers/virtualNetworkRules" - apiVersion = "2017-12-01" - dependsOn = [| "[resourceId('Microsoft.DBforPostgreSQL/servers', 'testdb')]" |] - location = "northeurope" - properties = - {| - virtualNetworkSubnetId = networkResourceId2String - |} - } - - Expect.equal actual1 expectedVnetRuleResult1 "Vnet is incorrect" - Expect.equal actual2 expectedVnetRuleResult2 "Vnet is incorrect" + test "Firewall rules are correctly set" { + let actual = postgreSQL { + name "testdb" + admin_username "myadminuser" + enable_azure_firewall } - test "Server endpoint configuration member correct" { - let db = - postgreSQLDb { - name "my_db" - collation "de_DE" - charset "ASCII" - } - - let server = - postgreSQL { - name "testdb" - admin_username "myadminuser" - add_database db - } - - let deployment = - arm { - add_resources [ server ] - output "serverfqdn" server.FullyQualifiedDomainName - } - - let jobj = - deployment.Template |> Writer.toJson |> Newtonsoft.Json.Linq.JObject.Parse - - let fqdnExpression = jobj.SelectToken "outputs.serverfqdn.value" - - Expect.equal - (string fqdnExpression) - "[reference(resourceId('Microsoft.DBforPostgreSQL/servers', 'testdb')).fullyQualifiedDomainName]" - "Incorrect fqdn output" + let actual = + actual + |> toTemplate Location.NorthEurope + |> Writer.toJson + |> Serialization.ofJson> + |> fun r -> r.Resources + |> Seq.find (fun r -> r.name = "testdb/allow-azure-services") + + let expectedFwRuleRes: FirewallResource = { + name = "testdb/allow-azure-services" + ``type`` = "Microsoft.DBforPostgreSQL/servers/firewallrules" + apiVersion = "2017-12-01" + dependsOn = [| "[resourceId('Microsoft.DBforPostgreSQL/servers', 'testdb')]" |] + location = "northeurope" + properties = {| + startIpAddress = "0.0.0.0" + endIpAddress = "0.0.0.0" + |} } - test "Server name must be given" { - Expect.throws - (fun () -> runBuilder <| postgreSQL { admin_username "adminuser" } |> ignore) - "Missing server name" + Expect.equal actual expectedFwRuleRes "Firewall is incorrect" + } + + test "Vnet rule are correctly set" { + let subscriptionId = "sid-subid" + let resourceGroup = "rg-abc" + let vnetName = "vnetid" + let subnetName = "default" + + let networkResourceId = { + Type = subnets + ResourceGroup = Some resourceGroup + Subscription = Some subscriptionId + Name = ResourceName vnetName + Segments = [ ResourceName subnetName ] } - test "Admin username must be given" { - Expect.throws - (fun () -> runBuilder <| postgreSQL { name "servername" } |> ignore) - "Missing admin username" - } + let networkResourceIdString = networkResourceId.Eval() + let vnetRuleName = "vnet-rule-name" - test "server_name is validated when set" { - Expect.throws (fun () -> postgreSQL { name "123bad" } |> ignore) "Bad server name" + let actual = postgreSQL { + name "testdb" + admin_username "myadminuser" + add_vnet_rule vnetRuleName networkResourceId } - test "admin_username is validated when set" { - Expect.throws (fun () -> postgreSQL { admin_username "123bad" } |> ignore) "Bad admin username" + let actual = + actual + |> toTemplate Location.NorthEurope + |> Writer.toJson + |> Serialization.ofJson> + |> fun r -> r.Resources + |> Seq.find (fun r -> r.name = $"testdb/%s{vnetRuleName}") + + let expectedVnetRuleResult: VnetResource = { + name = $"testdb/%s{vnetRuleName}" + ``type`` = "Microsoft.DBforPostgreSQL/servers/virtualNetworkRules" + apiVersion = "2017-12-01" + dependsOn = [| "[resourceId('Microsoft.DBforPostgreSQL/servers', 'testdb')]" |] + location = "northeurope" + properties = {| + virtualNetworkSubnetId = networkResourceIdString + |} } - test "backup_retention is validated when set" { - Expect.throws (fun () -> postgreSQL { backup_retention 2 } |> ignore) "Bad backup retention" - } + Expect.equal actual expectedVnetRuleResult "Vnet is incorrect" + } - test "storage_size is validated when set" { - Expect.throws (fun () -> postgreSQL { storage_size 1 } |> ignore) "Bad backup retention" - } + test "Vnet rules are correctly set" { + let subscriptionId = "sid-subid" + let resourceGroup = "rg-abc" - test "capacity is validated when set" { - Expect.throws (fun () -> postgreSQL { capacity 6 } |> ignore) "Bad capacity" - } + let vnetName1 = "vnetid1" + let subnetName1 = "default1" - test "Username can be validated" { - let validate c = fun () -> Validate.username "u" c + let networkResourceId1 = { + Type = subnets + ResourceGroup = Some resourceGroup + Subscription = Some subscriptionId + Name = ResourceName vnetName1 + Segments = [ ResourceName subnetName1 ] + } - let badNames = - [ - (null, "Null username") - ("", "Empty username") - (" \t ", "Blank username") - (String('a', 64), "Username too long") - ("Ædmin", "Bad chars in username") - ("123abc", "Can not begin with number") - ("admin_123", "More bad chars in username") - ] + let networkResourceId1String = networkResourceId1.Eval() + let vnetRuleName1 = "vnet-rule-name1" - for (candidate, label) in badNames do - Expect.throws (validate candidate) label + let vnetName2 = "vnetid2" + let subnetName2 = "default2" - Validate.reservedUsernames - |> List.iter (fun candidate -> - Expect.throws (validate candidate) (sprintf "Reserved name '%s'" candidate)) + let networkResourceId2 = { + Type = subnets + ResourceGroup = Some resourceGroup + Subscription = Some subscriptionId + Name = ResourceName vnetName2 + Segments = [ ResourceName subnetName2 ] + } - let goodNames = [ "a"; "abd23"; (String('a', 63)) ] + let networkResourceId2String = networkResourceId2.Eval() + let vnetRuleName2 = "vnet-rule-name2" - for candidate in goodNames do - Expect.throwsNot (validate candidate) (sprintf "'%s' should work" candidate) + let actual = postgreSQL { + name "testdb" + admin_username "myadminuser" + add_vnet_rules [ vnetRuleName1, networkResourceId1; vnetRuleName2, networkResourceId2 ] } - test "Servername can be validated" { - let validate c = fun () -> Validate.servername c - - let badNames = - [ - (" \t ", "Blank servername") - (null, "Null servername") - ("", "Empty servername") - (String('a', 64), "servername too long") - ("ab", "servername too short") - ("aBcd", "uppercase char in servername") - ("-server", "Beginning hyphen") - ("server-", "Ending hyphen") - ("særver", "Bad chars in servername") - ("123abc", "Can not begin with number") - ] - - for candidate, label in badNames do - Expect.throws (validate candidate) label - - let goodNames = [ "abc"; "abd-23"; (String('a', 63)) ] - - for candidate in goodNames do - Expect.throwsNot (validate candidate) (sprintf "'%s' should work" candidate) + let actual = + actual + |> toTemplate Location.NorthEurope + |> Writer.toJson + |> Serialization.ofJson> + |> fun r -> r.Resources + + let actual1 = actual |> Seq.find (fun r -> r.name = $"testdb/%s{vnetRuleName1}") + let actual2 = actual |> Seq.find (fun r -> r.name = $"testdb/%s{vnetRuleName2}") + + let expectedVnetRuleResult1: VnetResource = { + name = $"testdb/%s{vnetRuleName1}" + ``type`` = "Microsoft.DBforPostgreSQL/servers/virtualNetworkRules" + apiVersion = "2017-12-01" + dependsOn = [| "[resourceId('Microsoft.DBforPostgreSQL/servers', 'testdb')]" |] + location = "northeurope" + properties = {| + virtualNetworkSubnetId = networkResourceId1String + |} } - test "Database name can be validated" { - let validate c = fun () -> Validate.dbname c - - let badNames = - [ - (null, "Null dbname") - ("", "Empty dbname") - (" \t ", "Blank dbname") - (String('a', 64), "dbname too long") - ("123abc", "Can not begin with number") - ] - - for candidate, label in badNames do - Expect.throws (validate candidate) label - - let goodNames = [ "abc"; "abd-23"; (String('a', 63)) ] - - for candidate in goodNames do - Expect.throwsNot (validate candidate) (sprintf "'%s' should work" candidate) + let expectedVnetRuleResult2: VnetResource = { + name = $"testdb/%s{vnetRuleName2}" + ``type`` = "Microsoft.DBforPostgreSQL/servers/virtualNetworkRules" + apiVersion = "2017-12-01" + dependsOn = [| "[resourceId('Microsoft.DBforPostgreSQL/servers', 'testdb')]" |] + location = "northeurope" + properties = {| + virtualNetworkSubnetId = networkResourceId2String + |} } - test "Storage size can be validated" { - Expect.throws (fun () -> Validate.storageSize 4) "Storage size too small" - Expect.throws (fun () -> Validate.storageSize 1025) "Storage size too large" - Expect.throwsNot (fun () -> Validate.storageSize 5) "Storage size just right, min" - Expect.throwsNot (fun () -> Validate.storageSize 50) "Storage size just right" - Expect.throwsNot (fun () -> Validate.storageSize 1024) "Storage size just right, max" - } + Expect.equal actual1 expectedVnetRuleResult1 "Vnet is incorrect" + Expect.equal actual2 expectedVnetRuleResult2 "Vnet is incorrect" + } - test "Backup retention can be validated" { - Expect.throws (fun () -> Validate.backupRetention 4) "Backup retention too small" - Expect.throws (fun () -> Validate.backupRetention 1000) "Backup retention too large" - Expect.throwsNot (fun () -> Validate.backupRetention 21) "Backup retention just right" + test "Server endpoint configuration member correct" { + let db = postgreSQLDb { + name "my_db" + collation "de_DE" + charset "ASCII" } - test "Capacity can be validated" { - Expect.throws (fun () -> Validate.capacity 0) "Capacity too small" - Expect.throws (fun () -> Validate.capacity 128) "Capacity too large" - Expect.throws (fun () -> Validate.capacity 13) "Capacity not a power of two" - Expect.throwsNot (fun () -> Validate.capacity 16) "Capacity just right" + let server = postgreSQL { + name "testdb" + admin_username "myadminuser" + add_database db } - test "Family name should not include type name" { - Expect.equal PostgreSQLFamily.Gen5.AsArmValue "Gen5" "Wrong value for Gen5 family" - Expect.equal (PostgreSQLFamily.Gen5.ToString()) "Gen5" "Wrong value for Gen5 family" + let deployment = arm { + add_resources [ server ] + output "serverfqdn" server.FullyQualifiedDomainName } - ] + + let jobj = + deployment.Template |> Writer.toJson |> Newtonsoft.Json.Linq.JObject.Parse + + let fqdnExpression = jobj.SelectToken "outputs.serverfqdn.value" + + Expect.equal + (string fqdnExpression) + "[reference(resourceId('Microsoft.DBforPostgreSQL/servers', 'testdb')).fullyQualifiedDomainName]" + "Incorrect fqdn output" + } + + test "Server name must be given" { + Expect.throws + (fun () -> runBuilder <| postgreSQL { admin_username "adminuser" } |> ignore) + "Missing server name" + } + + test "Admin username must be given" { + Expect.throws (fun () -> runBuilder <| postgreSQL { name "servername" } |> ignore) "Missing admin username" + } + + test "server_name is validated when set" { + Expect.throws (fun () -> postgreSQL { name "123bad" } |> ignore) "Bad server name" + } + + test "admin_username is validated when set" { + Expect.throws (fun () -> postgreSQL { admin_username "123bad" } |> ignore) "Bad admin username" + } + + test "backup_retention is validated when set" { + Expect.throws (fun () -> postgreSQL { backup_retention 2 } |> ignore) "Bad backup retention" + } + + test "storage_size is validated when set" { + Expect.throws (fun () -> postgreSQL { storage_size 1 } |> ignore) "Bad backup retention" + } + + test "capacity is validated when set" { + Expect.throws (fun () -> postgreSQL { capacity 6 } |> ignore) "Bad capacity" + } + + test "Username can be validated" { + let validate c = fun () -> Validate.username "u" c + + let badNames = [ + (null, "Null username") + ("", "Empty username") + (" \t ", "Blank username") + (String('a', 64), "Username too long") + ("Ædmin", "Bad chars in username") + ("123abc", "Can not begin with number") + ("admin_123", "More bad chars in username") + ] + + for (candidate, label) in badNames do + Expect.throws (validate candidate) label + + Validate.reservedUsernames + |> List.iter (fun candidate -> Expect.throws (validate candidate) (sprintf "Reserved name '%s'" candidate)) + + let goodNames = [ "a"; "abd23"; (String('a', 63)) ] + + for candidate in goodNames do + Expect.throwsNot (validate candidate) (sprintf "'%s' should work" candidate) + } + + test "Servername can be validated" { + let validate c = fun () -> Validate.servername c + + let badNames = [ + (" \t ", "Blank servername") + (null, "Null servername") + ("", "Empty servername") + (String('a', 64), "servername too long") + ("ab", "servername too short") + ("aBcd", "uppercase char in servername") + ("-server", "Beginning hyphen") + ("server-", "Ending hyphen") + ("særver", "Bad chars in servername") + ("123abc", "Can not begin with number") + ] + + for candidate, label in badNames do + Expect.throws (validate candidate) label + + let goodNames = [ "abc"; "abd-23"; (String('a', 63)) ] + + for candidate in goodNames do + Expect.throwsNot (validate candidate) (sprintf "'%s' should work" candidate) + } + + test "Database name can be validated" { + let validate c = fun () -> Validate.dbname c + + let badNames = [ + (null, "Null dbname") + ("", "Empty dbname") + (" \t ", "Blank dbname") + (String('a', 64), "dbname too long") + ("123abc", "Can not begin with number") + ] + + for candidate, label in badNames do + Expect.throws (validate candidate) label + + let goodNames = [ "abc"; "abd-23"; (String('a', 63)) ] + + for candidate in goodNames do + Expect.throwsNot (validate candidate) (sprintf "'%s' should work" candidate) + } + + test "Storage size can be validated" { + Expect.throws (fun () -> Validate.storageSize 4) "Storage size too small" + Expect.throws (fun () -> Validate.storageSize 1025) "Storage size too large" + Expect.throwsNot (fun () -> Validate.storageSize 5) "Storage size just right, min" + Expect.throwsNot (fun () -> Validate.storageSize 50) "Storage size just right" + Expect.throwsNot (fun () -> Validate.storageSize 1024) "Storage size just right, max" + } + + test "Backup retention can be validated" { + Expect.throws (fun () -> Validate.backupRetention 4) "Backup retention too small" + Expect.throws (fun () -> Validate.backupRetention 1000) "Backup retention too large" + Expect.throwsNot (fun () -> Validate.backupRetention 21) "Backup retention just right" + } + + test "Capacity can be validated" { + Expect.throws (fun () -> Validate.capacity 0) "Capacity too small" + Expect.throws (fun () -> Validate.capacity 128) "Capacity too large" + Expect.throws (fun () -> Validate.capacity 13) "Capacity not a power of two" + Expect.throwsNot (fun () -> Validate.capacity 16) "Capacity just right" + } + + test "Family name should not include type name" { + Expect.equal PostgreSQLFamily.Gen5.AsArmValue "Gen5" "Wrong value for Gen5 family" + Expect.equal (PostgreSQLFamily.Gen5.ToString()) "Gen5" "Wrong value for Gen5 family" + } + ] diff --git a/src/Tests/PrivateLink.fs b/src/Tests/PrivateLink.fs index 01cc93ca9..7e001e751 100644 --- a/src/Tests/PrivateLink.fs +++ b/src/Tests/PrivateLink.fs @@ -6,147 +6,126 @@ open Farmer.Builders open Newtonsoft.Json.Linq let tests = - testList - "Private Link" - [ - test "Creates a private link service for a load balancer" { - let vnet = - vnet { - name "private-net" - add_address_spaces [ "10.100.0.0/16" ] - - add_subnets - [ - subnet { - name "default" - prefix "10.100.0.0/24" - } - subnet { - name "backend-services" - prefix "10.100.1.0/24" - } - subnet { - name "private-endpoints" - prefix "10.100.255.0/24" - private_link_service_network_policies Disabled - } - ] + testList "Private Link" [ + test "Creates a private link service for a load balancer" { + let vnet = vnet { + name "private-net" + add_address_spaces [ "10.100.0.0/16" ] + + add_subnets [ + subnet { + name "default" + prefix "10.100.0.0/24" } + subnet { + name "backend-services" + prefix "10.100.1.0/24" + } + subnet { + name "private-endpoints" + prefix "10.100.255.0/24" + private_link_service_network_policies Disabled + } + ] + } + + let lb = loadBalancer { + name "lb" + sku LoadBalancer.Sku.Standard + depends_on vnet + + add_frontends [ + frontend { + name "lb-frontend" + private_ip_allocation_method AllocationMethod.DynamicPrivateIp - let lb = - loadBalancer { - name "lb" - sku LoadBalancer.Sku.Standard - depends_on vnet - - add_frontends - [ - frontend { - name "lb-frontend" - private_ip_allocation_method AllocationMethod.DynamicPrivateIp - - link_to_subnet ( - ResourceId.create ( - Farmer.Arm.Network.subnets, - vnet.Name, - ResourceName "default" - ) - ) - } - ] - - add_backend_pools [ backendAddressPool { name "lb-backend" } ] - - add_probes - [ - loadBalancerProbe { - name "httpGet" - protocol Farmer.LoadBalancer.LoadBalancerProbeProtocol.HTTP - port 80 - request_path "/" - } - ] - - add_rules - [ - loadBalancingRule { - name "rule1" - frontend_ip_config "lb-frontend" - backend_address_pool "lb-backend" - frontend_port 80 - backend_port 80 - protocol TransmissionProtocol.TCP - probe "httpGet" - } - ] + link_to_subnet ( + ResourceId.create (Farmer.Arm.Network.subnets, vnet.Name, ResourceName "default") + ) } + ] - let pls = - privateLink { - name "pls" - depends_on lb - add_auto_approved_subscriptions [ System.Guid.NewGuid() ] - - add_load_balancer_frontend_ids - [ - ResourceId.create ( - Farmer.Arm.LoadBalancer.loadBalancerFrontendIPConfigurations, - lb.Name, - ResourceName "lb-frontend" - ) - ] - - add_ip_configs - [ - privateLinkIpConfig { - link_to_subnet ( - ResourceId.create ( - Farmer.Arm.Network.subnets, - vnet.Name, - ResourceName "private-endpoints" - ) - ) - } - ] + add_backend_pools [ backendAddressPool { name "lb-backend" } ] + + add_probes [ + loadBalancerProbe { + name "httpGet" + protocol Farmer.LoadBalancer.LoadBalancerProbeProtocol.HTTP + port 80 + request_path "/" + } + ] + + add_rules [ + loadBalancingRule { + name "rule1" + frontend_ip_config "lb-frontend" + backend_address_pool "lb-backend" + frontend_port 80 + backend_port 80 + protocol TransmissionProtocol.TCP + probe "httpGet" } + ] + } - let deployment = arm { add_resources [ lb; vnet; pls ] } - let json = deployment.Template |> Writer.toJson |> JToken.Parse - let privateLinkProps = json.SelectToken("resources[?(@.name=='pls')].properties") - let ipconfigs = privateLinkProps.SelectToken("ipConfigurations") :?> JArray - let ipconfig = ipconfigs.[0] - Expect.equal (string ipconfig.["name"]) "private-net-private-endpoints" "Incorrect name for ipconfig" - let ipconfigProps = ipconfig.["properties"] - Expect.equal ipconfigProps.["primary"] (JValue false) "Incorrect value for ipconfig.properties.primary" - - Expect.equal - ipconfigProps.["privateIPAddressVersion"] - (JValue "IPv4") - "Incorrect value for ipconfig.properties.privateIPAddressVersion" - - Expect.equal - ipconfigProps.["privateIPAllocationMethod"] - (JValue "Dynamic") - "Incorrect value for ipconfig.properties.privateIPAllocationMethod" - - Expect.equal - ipconfigProps.["subnet"].["id"] - (JValue - "[resourceId('Microsoft.Network/virtualNetworks/subnets', 'private-net', 'private-endpoints')]") - "Incorrect value for ipconfig.properties.subnet.id" - - Expect.hasLength ipconfigs 1 "Incorrect number of ip configurations" - - let frontendIpConfigs = - privateLinkProps.SelectToken("loadBalancerFrontendIpConfigurations") :?> JArray - - Expect.hasLength frontendIpConfigs 1 "Incorrect number of lb frontend ip configurations" - let frontendIpConfigId = frontendIpConfigs.[0].["id"] - - Expect.equal - frontendIpConfigId - (JValue - "[resourceId('Microsoft.Network/loadBalancers/frontendIPConfigurations', 'lb', 'lb-frontend')]") - "Incorrect expression for frontend IP config" + let pls = privateLink { + name "pls" + depends_on lb + add_auto_approved_subscriptions [ System.Guid.NewGuid() ] + + add_load_balancer_frontend_ids [ + ResourceId.create ( + Farmer.Arm.LoadBalancer.loadBalancerFrontendIPConfigurations, + lb.Name, + ResourceName "lb-frontend" + ) + ] + + add_ip_configs [ + privateLinkIpConfig { + link_to_subnet ( + ResourceId.create (Farmer.Arm.Network.subnets, vnet.Name, ResourceName "private-endpoints") + ) + } + ] } - ] + + let deployment = arm { add_resources [ lb; vnet; pls ] } + let json = deployment.Template |> Writer.toJson |> JToken.Parse + let privateLinkProps = json.SelectToken("resources[?(@.name=='pls')].properties") + let ipconfigs = privateLinkProps.SelectToken("ipConfigurations") :?> JArray + let ipconfig = ipconfigs.[0] + Expect.equal (string ipconfig.["name"]) "private-net-private-endpoints" "Incorrect name for ipconfig" + let ipconfigProps = ipconfig.["properties"] + Expect.equal ipconfigProps.["primary"] (JValue false) "Incorrect value for ipconfig.properties.primary" + + Expect.equal + ipconfigProps.["privateIPAddressVersion"] + (JValue "IPv4") + "Incorrect value for ipconfig.properties.privateIPAddressVersion" + + Expect.equal + ipconfigProps.["privateIPAllocationMethod"] + (JValue "Dynamic") + "Incorrect value for ipconfig.properties.privateIPAllocationMethod" + + Expect.equal + ipconfigProps.["subnet"].["id"] + (JValue "[resourceId('Microsoft.Network/virtualNetworks/subnets', 'private-net', 'private-endpoints')]") + "Incorrect value for ipconfig.properties.subnet.id" + + Expect.hasLength ipconfigs 1 "Incorrect number of ip configurations" + + let frontendIpConfigs = + privateLinkProps.SelectToken("loadBalancerFrontendIpConfigurations") :?> JArray + + Expect.hasLength frontendIpConfigs 1 "Incorrect number of lb frontend ip configurations" + let frontendIpConfigId = frontendIpConfigs.[0].["id"] + + Expect.equal + frontendIpConfigId + (JValue "[resourceId('Microsoft.Network/loadBalancers/frontendIPConfigurations', 'lb', 'lb-frontend')]") + "Incorrect expression for frontend IP config" + } + ] diff --git a/src/Tests/ResourceGroup.fs b/src/Tests/ResourceGroup.fs index 39c4bf3c1..dd7945082 100644 --- a/src/Tests/ResourceGroup.fs +++ b/src/Tests/ResourceGroup.fs @@ -6,55 +6,51 @@ open Farmer.Arm.ResourceGroup open Farmer.Builders let tests = - testList - "Resource Group" - [ - test "Creates a resource group" { - let rg = createResourceGroup "myRg" Location.EastUS - Expect.equal rg.Name.Value "myRg" "Incorrect name on resource group" - Expect.equal rg.Location Location.EastUS "Incorrect location on resource group" - Expect.equal rg.Dependencies Set.empty "Resource group should have no dependencies" - Expect.equal rg.Tags Map.empty "Resource group should have no tags" - } - test "Supports multiple nested deployments to the same resource group" { - let nestedRgs = - [ 1..3 ] - |> List.map (fun i -> - resourceGroup { - name "target-rg" - add_resource (storageAccount { name $"stg{i}" }) - } - :> IBuilder) - - let rg = + testList "Resource Group" [ + test "Creates a resource group" { + let rg = createResourceGroup "myRg" Location.EastUS + Expect.equal rg.Name.Value "myRg" "Incorrect name on resource group" + Expect.equal rg.Location Location.EastUS "Incorrect location on resource group" + Expect.equal rg.Dependencies Set.empty "Resource group should have no dependencies" + Expect.equal rg.Tags Map.empty "Resource group should have no tags" + } + test "Supports multiple nested deployments to the same resource group" { + let nestedRgs = + [ 1..3 ] + |> List.map (fun i -> resourceGroup { - name "outer-rg" - add_resources nestedRgs + name "target-rg" + add_resource (storageAccount { name $"stg{i}" }) } + :> IBuilder) + + let rg = resourceGroup { + name "outer-rg" + add_resources nestedRgs + } - Expect.hasLength rg.Template.Resources 3 "all three resource groups should be added" + Expect.hasLength rg.Template.Resources 3 "all three resource groups should be added" - let nestedResources = - rg.Template.Resources - |> List.map (fun x -> x :?> ResourceGroupDeployment) - |> List.collect (fun x -> x.Resources) - |> List.map (fun x -> x.ResourceId.Name.Value) + let nestedResources = + rg.Template.Resources + |> List.map (fun x -> x :?> ResourceGroupDeployment) + |> List.collect (fun x -> x.Resources) + |> List.map (fun x -> x.ResourceId.Name.Value) - Expect.equal nestedResources [ "stg1"; "stg2"; "stg3" ] "all three storage accounts should be nested" + Expect.equal nestedResources [ "stg1"; "stg2"; "stg3" ] "all three storage accounts should be nested" + } + test "zip_deploy should be performed when declared in a nested resource" { + let webApp = webApp { + name "webapp" + zip_deploy "deploy" } - test "zip_deploy should be performed when declared in a nested resource" { - let webApp = - webApp { - name "webapp" - zip_deploy "deploy" - } - let oneNestedLevel = resourceGroup { add_resource webApp } - let twoNestedLevels = resourceGroup { add_resource oneNestedLevel } - let threeNestedLevels = arm { add_resource twoNestedLevels } + let oneNestedLevel = resourceGroup { add_resource webApp } + let twoNestedLevels = resourceGroup { add_resource oneNestedLevel } + let threeNestedLevels = arm { add_resource twoNestedLevels } - Expect.isNonEmpty - (threeNestedLevels :> IDeploymentSource).Deployment.PostDeployTasks - "The zip_deploy should create a post deployment task" - } - ] + Expect.isNonEmpty + (threeNestedLevels :> IDeploymentSource).Deployment.PostDeployTasks + "The zip_deploy should create a post deployment task" + } + ] diff --git a/src/Tests/RoleAssignment.fs b/src/Tests/RoleAssignment.fs index 3144f3bdd..e61cdd865 100644 --- a/src/Tests/RoleAssignment.fs +++ b/src/Tests/RoleAssignment.fs @@ -5,23 +5,20 @@ open Farmer open Farmer.Arm let tests = - testList - "RoleAssignment" - [ - test "Produces opaque resource scope" { - let actual: IArmResource = - { - Name = ResourceName "assignment" - RoleDefinitionId = Roles.Contributor - PrincipalId = ArmExpression.create "1" |> PrincipalId - PrincipalType = PrincipalType.User - Scope = privateClouds.resourceId "mySDDC" |> UnmanagedResource - Dependencies = Set.empty - } - - "Expected matching scope" - |> Expect.stringContains - (Newtonsoft.Json.JsonConvert.SerializeObject actual.JsonModel) - "\"scope\":\"[resourceId('Microsoft.AVS/privateClouds', 'mySDDC')]\"" + testList "RoleAssignment" [ + test "Produces opaque resource scope" { + let actual: IArmResource = { + Name = ResourceName "assignment" + RoleDefinitionId = Roles.Contributor + PrincipalId = ArmExpression.create "1" |> PrincipalId + PrincipalType = PrincipalType.User + Scope = privateClouds.resourceId "mySDDC" |> UnmanagedResource + Dependencies = Set.empty } - ] + + "Expected matching scope" + |> Expect.stringContains + (Newtonsoft.Json.JsonConvert.SerializeObject actual.JsonModel) + "\"scope\":\"[resourceId('Microsoft.AVS/privateClouds', 'mySDDC')]\"" + } + ] diff --git a/src/Tests/ServiceBus.fs b/src/Tests/ServiceBus.fs index 164ec13b3..437b210c0 100644 --- a/src/Tests/ServiceBus.fs +++ b/src/Tests/ServiceBus.fs @@ -49,912 +49,847 @@ let parseTemplate (arm: ResourceGroupConfig) = Newtonsoft.Json.Linq.JObject.Parse(json) let tests = - testList - "Service Bus Tests" - [ - test "Namespace is correctly created" { - let sbNs = - arm { - add_resource ( - serviceBus { - name "serviceBus" - sku Standard - } - ) - } - |> findAzureResources dummyClient.SerializationSettings - |> List.head + testList "Service Bus Tests" [ + test "Namespace is correctly created" { + let sbNs = + arm { + add_resource ( + serviceBus { + name "serviceBus" + sku Standard + } + ) + } + |> findAzureResources dummyClient.SerializationSettings + |> List.head + + sbNs.Validate() + + Expect.equal sbNs.Name "serviceBus" "Invalid namespace name" + Expect.equal sbNs.Sku.Name SkuName.Standard "Invalid Sku" + } + + test "Namespace validation is respected" { + Expect.throws (fun _ -> serviceBus { name "myns" } |> ignore) "Namespace length is too small" + + Expect.throws + (fun _ -> serviceBus { name (String.replicate 51 "x") } |> ignore) + "Namespace length is too long" + + Expect.throws (fun _ -> serviceBus { name "-abcdefghijk" } |> ignore) "Namespace starts with a dash" + Expect.throws (fun _ -> serviceBus { name "abcdefghijk-" } |> ignore) "Namespace ends with a dash" + Expect.throws (fun _ -> serviceBus { name "1abcdefghijk" } |> ignore) "Namespace starts with a number" + Expect.throws (fun _ -> serviceBus { name "abcdefghijk-sb" } |> ignore) "Namespace ends with -sb" + + Expect.throws + (fun _ -> serviceBus { name "abcdefghijk-mgmt" } |> ignore) + "Namespace ends with management postifx" + + Expect.throws + (fun _ -> serviceBus { name "c347834e-3f04-409c-b26b-c5ed702dea0b" } |> ignore) + "Namespace is a guid" + } + + test "Public network access can be disabled" { + let resourceGroup = arm { + add_resource ( + serviceBus { + name "serviceBus" + sku Standard + disable_public_network_access + } + ) + } + + let jobj = parseTemplate resourceGroup - sbNs.Validate() + Expect.equal + (jobj.SelectToken($"resources[0].properties.publicNetworkAccess").ToString()) + "Disabled" + "Public network access should be disabled" + } - Expect.equal sbNs.Name "serviceBus" "Invalid namespace name" - Expect.equal sbNs.Sku.Name SkuName.Standard "Invalid Sku" + test "Public network access can be toggled" { + let resourceGroup = arm { + add_resource ( + serviceBus { + name "serviceBus" + sku Standard + disable_public_network_access + disable_public_network_access FeatureFlag.Disabled + } + ) } - test "Namespace validation is respected" { - Expect.throws (fun _ -> serviceBus { name "myns" } |> ignore) "Namespace length is too small" + let jobj = parseTemplate resourceGroup - Expect.throws - (fun _ -> serviceBus { name (String.replicate 51 "x") } |> ignore) - "Namespace length is too long" + Expect.equal + (jobj.SelectToken($"resources[0].properties.publicNetworkAccess").ToString()) + "Enabled" + "Public network access should be enabled" + } - Expect.throws (fun _ -> serviceBus { name "-abcdefghijk" } |> ignore) "Namespace starts with a dash" - Expect.throws (fun _ -> serviceBus { name "abcdefghijk-" } |> ignore) "Namespace ends with a dash" - Expect.throws (fun _ -> serviceBus { name "1abcdefghijk" } |> ignore) "Namespace starts with a number" - Expect.throws (fun _ -> serviceBus { name "abcdefghijk-sb" } |> ignore) "Namespace ends with -sb" + test "Zone redundancy can be enabled" { + let resourceGroup = arm { + add_resource ( + serviceBus { + name "serviceBus" + sku (Premium MessagingUnits.OneUnit) + enable_zone_redundancy + } + ) + } - Expect.throws - (fun _ -> serviceBus { name "abcdefghijk-mgmt" } |> ignore) - "Namespace ends with management postifx" + let jobj = parseTemplate resourceGroup - Expect.throws - (fun _ -> serviceBus { name "c347834e-3f04-409c-b26b-c5ed702dea0b" } |> ignore) - "Namespace is a guid" - } + Expect.equal + (jobj.SelectToken($"resources[0].properties.zoneRedundant").ToString()) + "true" + "Zone redundancy should be enabled" + } - test "Public network access can be disabled" { - let resourceGroup = - arm { - add_resource ( - serviceBus { - name "serviceBus" - sku Standard - disable_public_network_access - } - ) + test "Zone redundancy can be toggled" { + let resourceGroup = arm { + add_resource ( + serviceBus { + name "serviceBus" + sku (Premium MessagingUnits.OneUnit) + enable_zone_redundancy + enable_zone_redundancy FeatureFlag.Disabled } + ) + } - let jobj = parseTemplate resourceGroup + let jobj = parseTemplate resourceGroup + + Expect.equal + (jobj.SelectToken($"resources[0].properties.zoneRedundant").ToString()) + "false" + "Zone redundancy should be disabled" + } + + test "Zone redundancy cannot be set against standard SKU namespace" { + Expect.throws + (fun () -> + serviceBus { + name "serviceBus" + sku Standard + enable_zone_redundancy + } + |> ignore) + "Zone redundancy can only be enabled against premium service bus namespaces" + } + + test "Min TLS version can be set" { + let resourceGroup = arm { + add_resource ( + serviceBus { + name "serviceBus" + sku Standard + min_tls_version TlsVersion.Tls12 + } + ) + } + + let jobj = parseTemplate resourceGroup + + Expect.equal + (jobj.SelectToken($"resources[0].properties.minimumTlsVersion").ToString()) + "1.2" + "Min TLS should be 1.2" + } + + testList "Queue Tests" [ + test "Queue is correctly created" { + let queue = serviceBus { + name "my-bus" + sku ServiceBus.Standard + + add_queues [ + queue { + name "my-queue" + duplicate_detection_minutes 5 + enable_dead_letter_on_message_expiration + enable_partition + enable_session + lock_duration_minutes 5 + max_delivery_count 3 + message_ttl 10 + } + ] + } + + let queue: SBQueue = queue |> getResourceAtIndex 1 + + Expect.equal queue.Name "my-bus/my-queue" "Invalid queue name" + + Expect.isTrue + (queue.RequiresDuplicateDetection.GetValueOrDefault false) + "Duplicate detection should be enabled" Expect.equal - (jobj.SelectToken($"resources[0].properties.publicNetworkAccess").ToString()) - "Disabled" - "Public network access should be disabled" - } + queue.DuplicateDetectionHistoryTimeWindow + (Nullable(TimeSpan(0, 5, 0))) + "Duplicate detection window incorrect" - test "Public network access can be toggled" { - let resourceGroup = - arm { - add_resource ( - serviceBus { - name "serviceBus" - sku Standard - disable_public_network_access - disable_public_network_access FeatureFlag.Disabled - } - ) - } + Expect.isTrue + (queue.DeadLetteringOnMessageExpiration.GetValueOrDefault false) + "Dead lettering should be enabled" + + Expect.isTrue (queue.EnablePartitioning.GetValueOrDefault false) "Partitioning should be enabled" - let jobj = parseTemplate resourceGroup + Expect.isTrue (queue.RequiresSession.GetValueOrDefault false) "Sessions should be enabled" + Expect.equal queue.LockDuration (Nullable(TimeSpan(0, 5, 0))) "Lock duration incorrect" Expect.equal - (jobj.SelectToken($"resources[0].properties.publicNetworkAccess").ToString()) - "Enabled" - "Public network access should be enabled" + (queue.DefaultMessageTimeToLive.GetValueOrDefault TimeSpan.MinValue).TotalDays + 10. + "Default TTL incorrect" + + Expect.equal queue.MaxDeliveryCount (Nullable 3) "Max delivery count incorrect" } + test "Can set duplicate dection from a TimeSpan" { + let queue = serviceBus { + name "my-bus" + sku ServiceBus.Standard + + add_queues [ + queue { + name "my-queue" + duplicate_detection (TimeSpan.FromSeconds(900.)) + } + ] + } - test "Zone redundancy can be enabled" { - let resourceGroup = - arm { - add_resource ( - serviceBus { - name "serviceBus" - sku (Premium MessagingUnits.OneUnit) - enable_zone_redundancy - } - ) - } + let queue: SBQueue = queue |> getResourceAtIndex 1 - let jobj = parseTemplate resourceGroup + Expect.isTrue + (queue.RequiresDuplicateDetection.GetValueOrDefault false) + "Duplicate detection should be enabled" Expect.equal - (jobj.SelectToken($"resources[0].properties.zoneRedundant").ToString()) - "true" - "Zone redundancy should be enabled" + queue.DuplicateDetectionHistoryTimeWindow + (Nullable(TimeSpan(0, 15, 0))) + "Duplicate detection window incorrect" } + test "Can set duplicate detection to None" { + let queue = serviceBus { + name "my-bus" + sku ServiceBus.Standard + + add_queues [ + queue { + name "my-queue" + duplicate_detection None + } + ] + } - test "Zone redundancy can be toggled" { - let resourceGroup = - arm { - add_resource ( - serviceBus { - name "serviceBus" - sku (Premium MessagingUnits.OneUnit) - enable_zone_redundancy - enable_zone_redundancy FeatureFlag.Disabled - } - ) - } + let queue: SBQueue = queue |> getResourceAtIndex 1 - let jobj = parseTemplate resourceGroup + Expect.equal queue.RequiresDuplicateDetection (Nullable()) "Duplicate detection should be null" Expect.equal - (jobj.SelectToken($"resources[0].properties.zoneRedundant").ToString()) - "false" - "Zone redundancy should be disabled" + queue.DuplicateDetectionHistoryTimeWindow + (Nullable()) + "Duplicate detection window incorrect" } - test "Zone redundancy cannot be set against standard SKU namespace" { + test "Cannot set duplicate detection on basic tier" { Expect.throws (fun () -> serviceBus { name "serviceBus" - sku Standard - enable_zone_redundancy + + add_queues [ + queue { + name "my-queue" + duplicate_detection_minutes 1 + } + ] } |> ignore) - "Zone redundancy can only be enabled against premium service bus namespaces" + "Duplicate detection isn't allowed on basic tier" } - test "Min TLS version can be set" { - let resourceGroup = - arm { - add_resource ( - serviceBus { - name "serviceBus" - sku Standard - min_tls_version TlsVersion.Tls12 - } - ) - } - - let jobj = parseTemplate resourceGroup + test "Cannot set lock duration more than 5 minutes" { + Expect.throws + (fun () -> + serviceBus { + name "serviceBus" - Expect.equal - (jobj.SelectToken($"resources[0].properties.minimumTlsVersion").ToString()) - "1.2" - "Min TLS should be 1.2" + add_queues [ + queue { + name "my-queue" + lock_duration_minutes 6 + } + ] + } + |> ignore) + "Lock duration max should be 5 minutes" } - testList - "Queue Tests" - [ - test "Queue is correctly created" { - let queue = - serviceBus { - name "my-bus" - sku ServiceBus.Standard - - add_queues - [ - queue { - name "my-queue" - duplicate_detection_minutes 5 - enable_dead_letter_on_message_expiration - enable_partition - enable_session - lock_duration_minutes 5 - max_delivery_count 3 - message_ttl 10 - } - ] - } - - let queue: SBQueue = queue |> getResourceAtIndex 1 - - Expect.equal queue.Name "my-bus/my-queue" "Invalid queue name" - - Expect.isTrue - (queue.RequiresDuplicateDetection.GetValueOrDefault false) - "Duplicate detection should be enabled" - - Expect.equal - queue.DuplicateDetectionHistoryTimeWindow - (Nullable(TimeSpan(0, 5, 0))) - "Duplicate detection window incorrect" + test "Default TTL set for Basic queue" { + let queue: SBQueue = + serviceBus { + name "serviceBus" + add_queues [ queue { name "my-queue" } ] + } + |> getResourceAtIndex 1 - Expect.isTrue - (queue.DeadLetteringOnMessageExpiration.GetValueOrDefault false) - "Dead lettering should be enabled" + Expect.isNone (Option.ofNullable queue.DefaultMessageTimeToLive) "The default TTL should be null" + } - Expect.isTrue - (queue.EnablePartitioning.GetValueOrDefault false) - "Partitioning should be enabled" + test "Set TTL by timespan for Basic queue" { + let queue: SBQueue = + serviceBus { + name "serviceBus" - Expect.isTrue (queue.RequiresSession.GetValueOrDefault false) "Sessions should be enabled" - Expect.equal queue.LockDuration (Nullable(TimeSpan(0, 5, 0))) "Lock duration incorrect" + add_queues [ + queue { + name "my-queue" + message_ttl "00:05:00" + } + ] + } + |> getResourceAtIndex 1 - Expect.equal - (queue.DefaultMessageTimeToLive.GetValueOrDefault TimeSpan.MinValue).TotalDays - 10. - "Default TTL incorrect" + Expect.equal + (queue.DefaultMessageTimeToLive.GetValueOrDefault TimeSpan.MinValue) + .TotalMinutes + 5. + "TTL from TimeSpan should be 5 minutes" + } - Expect.equal queue.MaxDeliveryCount (Nullable 3) "Max delivery count incorrect" + test "Default TTL set for Standard queue" { + let queue: SBQueue = + serviceBus { + name "serviceBus" + sku ServiceBus.Standard + add_queues [ queue { name "my-queue" } ] } - test "Can set duplicate dection from a TimeSpan" { - let queue = - serviceBus { - name "my-bus" - sku ServiceBus.Standard - - add_queues - [ - queue { - name "my-queue" - duplicate_detection (TimeSpan.FromSeconds(900.)) - } - ] - } + |> getResourceAtIndex 1 - let queue: SBQueue = queue |> getResourceAtIndex 1 + Expect.isNone (Option.ofNullable queue.DefaultMessageTimeToLive) "Default TTL should be null" + } - Expect.isTrue - (queue.RequiresDuplicateDetection.GetValueOrDefault false) - "Duplicate detection should be enabled" + test "Max size set for queue" { + let queue: SBQueue = + serviceBus { + name "serviceBus" - Expect.equal - queue.DuplicateDetectionHistoryTimeWindow - (Nullable(TimeSpan(0, 15, 0))) - "Duplicate detection window incorrect" - } - test "Can set duplicate detection to None" { - let queue = - serviceBus { - name "my-bus" - sku ServiceBus.Standard - - add_queues - [ - queue { - name "my-queue" - duplicate_detection None - } - ] + add_queues [ + queue { + name "my-queue" + max_queue_size 10240 } + ] + } + |> getResourceAtIndex 1 - let queue: SBQueue = queue |> getResourceAtIndex 1 + Expect.equal queue.MaxSizeInMegabytes (Nullable 10240) "Incorrect max queue size" + } - Expect.equal queue.RequiresDuplicateDetection (Nullable()) "Duplicate detection should be null" + test "Correctly creates multiple queues" { + let theBus = serviceBus { + name "serviceBus" + add_queues [ queue { name "queue-a" }; queue { name "queue-b" } ] + } - Expect.equal - queue.DuplicateDetectionHistoryTimeWindow - (Nullable()) - "Duplicate detection window incorrect" - } + let deployment = arm { add_resource theBus } - test "Cannot set duplicate detection on basic tier" { - Expect.throws - (fun () -> - serviceBus { - name "serviceBus" - - add_queues - [ - queue { - name "my-queue" - duplicate_detection_minutes 1 - } - ] - } - |> ignore) - "Duplicate detection isn't allowed on basic tier" - } + let queues = + deployment.Template.Resources + |> List.choose (function + | :? Queue as q -> Some q + | _ -> None) - test "Cannot set lock duration more than 5 minutes" { - Expect.throws - (fun () -> - serviceBus { - name "serviceBus" - - add_queues - [ - queue { - name "my-queue" - lock_duration_minutes 6 - } - ] - } - |> ignore) - "Lock duration max should be 5 minutes" - } + Expect.hasLength queues 2 "Should have two queues in a single namespace." + } - test "Default TTL set for Basic queue" { - let queue: SBQueue = + test "No authorization rule by default" { + let sbAuthorizationRules = + arm { + add_resource ( serviceBus { name "serviceBus" + sku Standard add_queues [ queue { name "my-queue" } ] } - |> getResourceAtIndex 1 - - Expect.isNone - (Option.ofNullable queue.DefaultMessageTimeToLive) - "The default TTL should be null" + ) } + |> findAzureResources dummyClient.SerializationSettings + |> List.filter (fun x -> (=) x.Type queueAuthorizationRules.Type) - test "Set TTL by timespan for Basic queue" { - let queue: SBQueue = + Expect.hasLength sbAuthorizationRules 0 "Should not have authorization rule by default" + } + + test "Authorization Rule writes correct template" { + let thing = + arm { + add_resource ( serviceBus { name "serviceBus" + sku Standard - add_queues - [ - queue { - name "my-queue" - message_ttl "00:05:00" - } - ] + add_queues [ + queue { + name "my-queue" + add_authorization_rule "my-rule" [ Manage ] + } + ] } - |> getResourceAtIndex 1 - - Expect.equal - (queue.DefaultMessageTimeToLive.GetValueOrDefault TimeSpan.MinValue) - .TotalMinutes - 5. - "TTL from TimeSpan should be 5 minutes" + ) } + |> findAzureResources dummyClient.SerializationSettings - test "Default TTL set for Standard queue" { - let queue: SBQueue = - serviceBus { - name "serviceBus" - sku ServiceBus.Standard - add_queues [ queue { name "my-queue" } ] - } - |> getResourceAtIndex 1 + let sbAuthorizationRule = + thing + |> List.filter (fun x -> (=) x.Type queueAuthorizationRules.Type) + |> List.head + + Expect.equal sbAuthorizationRule.Name "serviceBus/my-queue/my-rule" "Name is wrong" + Expect.equal sbAuthorizationRule.Rights.Count 1 "Wrong number of rights" + Expect.equal sbAuthorizationRule.Rights.[0] (Nullable AccessRights.Manage) "Wrong rights" + } - Expect.isNone (Option.ofNullable queue.DefaultMessageTimeToLive) "Default TTL should be null" + test "Queue IArmResource has correct resourceId for unmanaged namespace" { + let resource = + queue { + name "my-queue" + link_to_unmanaged_namespace "my-bus" } + |> getResources + |> getResource + |> List.head + :> IArmResource - test "Max size set for queue" { - let queue: SBQueue = - serviceBus { - name "serviceBus" + Expect.equal + (resource.ResourceId.Eval()) + "[resourceId('Microsoft.ServiceBus/namespaces/queues', 'my-bus', 'my-queue')]" + "" + } + ] - add_queues - [ - queue { - name "my-queue" - max_queue_size 10240 - } - ] - } - |> getResourceAtIndex 1 + testList "Topic Tests" [ + test "Can create a basic topic" { + let topic: SBTopic = + serviceBus { + name "my-bus" - Expect.equal queue.MaxSizeInMegabytes (Nullable 10240) "Incorrect max queue size" + add_topics [ + topic { + name "my-topic" + duplicate_detection_minutes 3 + message_ttl 2 + enable_partition + } + ] } + |> getResourceAtIndex 1 - test "Correctly creates multiple queues" { - let theBus = - serviceBus { - name "serviceBus" - add_queues [ queue { name "queue-a" }; queue { name "queue-b" } ] + Expect.equal topic.Name "my-bus/my-topic" "Name not set" + Expect.equal topic.RequiresDuplicateDetection (Nullable true) "Duplicate detection not set" + + Expect.equal + topic.DuplicateDetectionHistoryTimeWindow + (Nullable(TimeSpan.FromMinutes 3.)) + "Duplicate detection time not set" + + Expect.equal topic.DefaultMessageTimeToLive (Nullable(TimeSpan.FromDays 2.)) "Time to live not set" + + Expect.equal topic.EnablePartitioning (Nullable true) "Paritition not set" + } + test "Can set duplicate detection to None" { + let topic: SBTopic = + serviceBus { + name "my-bus" + + add_topics [ + topic { + name "my-topic" + duplicate_detection None } + ] + } + |> getResourceAtIndex 1 - let deployment = arm { add_resource theBus } + Expect.equal topic.Name "my-bus/my-topic" "Name not set" + Expect.equal topic.RequiresDuplicateDetection (Nullable()) "Duplicate detection set" - let queues = - deployment.Template.Resources - |> List.choose (function - | :? Queue as q -> Some q - | _ -> None) + Expect.equal topic.DuplicateDetectionHistoryTimeWindow (Nullable()) "Duplicate detection time not null" + } + test "Can set duplicate using a timespan" { + let topic: SBTopic = + serviceBus { + name "my-bus" - Expect.hasLength queues 2 "Should have two queues in a single namespace." + add_topics [ + topic { + name "my-topic" + duplicate_detection (TimeSpan.FromSeconds(900.)) + } + ] } + |> getResourceAtIndex 1 - test "No authorization rule by default" { - let sbAuthorizationRules = - arm { - add_resource ( - serviceBus { - name "serviceBus" - sku Standard - add_queues [ queue { name "my-queue" } ] - } - ) + Expect.equal topic.Name "my-bus/my-topic" "Name not set" + Expect.equal topic.RequiresDuplicateDetection (Nullable true) "Duplicate detection not set" + + Expect.equal + topic.DuplicateDetectionHistoryTimeWindow + (Nullable(TimeSpan.FromMinutes 15.)) + "Duplicate detection time incorrect" + } + test "Can create a topic with a max size" { + let topic: SBTopic = + serviceBus { + name "my-bus" + + add_topics [ + topic { + name "my-topic" + max_topic_size 10240 } - |> findAzureResources dummyClient.SerializationSettings - |> List.filter (fun x -> (=) x.Type queueAuthorizationRules.Type) + ] + } + |> getResourceAtIndex 1 + + Expect.equal topic.Name "my-bus/my-topic" "Name not set" + Expect.equal topic.MaxSizeInMegabytes (Nullable 10240) "Max size not set" + } + test "Can create a basic subscription" { + let sub: SBSubscription = + serviceBus { + name "my-bus" - Expect.hasLength sbAuthorizationRules 0 "Should not have authorization rule by default" + add_topics [ + topic { + name "my-topic" + add_subscriptions [ subscription { name "my-sub" } ] + } + ] } + |> getResourceAtIndex 2 + + Expect.equal sub.Name "my-bus/my-topic/my-sub" "Name not set" + } + test "Can create a forwarding subscription" { + let sub: SBSubscription = + serviceBus { + name "my-bus" - test "Authorization Rule writes correct template" { - let thing = - arm { - add_resource ( - serviceBus { - name "serviceBus" - sku Standard - - add_queues - [ - queue { - name "my-queue" - add_authorization_rule "my-rule" [ Manage ] - } - ] + add_topics [ + topic { + name "my-topic" + + add_subscriptions [ + subscription { + name "my-sub" + forward_to "my-other-topic" } - ) + ] } - |> findAzureResources dummyClient.SerializationSettings + topic { name "my-other-topic" } + ] + } + |> getResourceAtIndex 3 - let sbAuthorizationRule = - thing - |> List.filter (fun x -> (=) x.Type queueAuthorizationRules.Type) - |> List.head + Expect.equal sub.ForwardTo "my-other-topic" "ForwardTo not set" + } + test "Can create a subscription with a message ttl" { + let sub: SBSubscription = + serviceBus { + name "my-bus" - Expect.equal sbAuthorizationRule.Name "serviceBus/my-queue/my-rule" "Name is wrong" - Expect.equal sbAuthorizationRule.Rights.Count 1 "Wrong number of rights" - Expect.equal sbAuthorizationRule.Rights.[0] (Nullable AccessRights.Manage) "Wrong rights" - } + add_topics [ + topic { + name "my-topic" - test "Queue IArmResource has correct resourceId for unmanaged namespace" { - let resource = - queue { - name "my-queue" - link_to_unmanaged_namespace "my-bus" + add_subscriptions [ + subscription { + name "my-sub" + message_ttl (TimeSpan.FromHours 2.) + } + ] } - |> getResources - |> getResource - |> List.head - :> IArmResource - - Expect.equal - (resource.ResourceId.Eval()) - "[resourceId('Microsoft.ServiceBus/namespaces/queues', 'my-bus', 'my-queue')]" - "" + ] } - ] + |> getResourceAtIndex 2 - testList - "Topic Tests" - [ - test "Can create a basic topic" { - let topic: SBTopic = - serviceBus { - name "my-bus" - - add_topics - [ - topic { - name "my-topic" - duplicate_detection_minutes 3 - message_ttl 2 - enable_partition - } + Expect.equal sub.DefaultMessageTimeToLive (Nullable(TimeSpan.FromHours 2.)) "TTL not set" + } + test "Creates a correlation filter rule" { + let correlationRule = + ServiceBus.CorrelationFilter( + ResourceName "CompletedStatus", + Some "xyz", + Map [ "Status", "Completed"; "Operation", "DoStuff" ] + ) + + let builtCorrelationRule = + Rule.CreateCorrelationFilter( + "CompletedStatus", + [ "Status", "Completed"; "Operation", "DoStuff" ], + "xyz" + ) + + Expect.equal correlationRule builtCorrelationRule "Built incorrect correlation filter" + } + test "Can create a subscription with different filters" { + let sb = serviceBus { + name "my-bus" + sku Standard + + add_topics [ + topic { + name "my-topic" + + add_subscriptions [ + subscription { + name "my-sub" + + add_filters [ + Rule.CreateCorrelationFilter("SuccessfulStatus", [ "Status", "Success" ]) + Rule.CreateSqlFilter("Thing", "Status = Success") ] - } - |> getResourceAtIndex 1 - - Expect.equal topic.Name "my-bus/my-topic" "Name not set" - Expect.equal topic.RequiresDuplicateDetection (Nullable true) "Duplicate detection not set" - Expect.equal - topic.DuplicateDetectionHistoryTimeWindow - (Nullable(TimeSpan.FromMinutes 3.)) - "Duplicate detection time not set" + add_correlation_filter "FailedStatus" [ "Status", "Fail" ] + add_sql_filter "OtherSqlThing" "Status = Failed" + } + ] + } + ] + } - Expect.equal - topic.DefaultMessageTimeToLive - (Nullable(TimeSpan.FromDays 2.)) - "Time to live not set" + let template = arm { + location Location.EastUS + add_resource sb + } - Expect.equal topic.EnablePartitioning (Nullable true) "Paritition not set" - } - test "Can set duplicate detection to None" { - let topic: SBTopic = - serviceBus { - name "my-bus" - - add_topics - [ - topic { - name "my-topic" - duplicate_detection None - } - ] - } - |> getResourceAtIndex 1 + let generatedTemplate = template.Template + let genSubscription = generatedTemplate.Resources.Item 2 :?> Subscription + Expect.hasLength genSubscription.Rules 4 "Expected subscription should have 4 rules" - Expect.equal topic.Name "my-bus/my-topic" "Name not set" - Expect.equal topic.RequiresDuplicateDetection (Nullable()) "Duplicate detection set" + Expect.equal + genSubscription.Rules.[0] + (Rule.CreateCorrelationFilter("SuccessfulStatus", [ "Status", "Success" ])) + "Rule 0 is incorrect" - Expect.equal - topic.DuplicateDetectionHistoryTimeWindow - (Nullable()) - "Duplicate detection time not null" - } - test "Can set duplicate using a timespan" { - let topic: SBTopic = - serviceBus { - name "my-bus" - - add_topics - [ - topic { - name "my-topic" - duplicate_detection (TimeSpan.FromSeconds(900.)) - } - ] - } - |> getResourceAtIndex 1 + Expect.equal + genSubscription.Rules.[1] + (Rule.CreateSqlFilter("Thing", "Status = Success")) + "Rule 1 is incorrect" - Expect.equal topic.Name "my-bus/my-topic" "Name not set" - Expect.equal topic.RequiresDuplicateDetection (Nullable true) "Duplicate detection not set" + Expect.equal + genSubscription.Rules.[2] + (Rule.CreateCorrelationFilter("FailedStatus", [ "Status", "Fail" ])) + "Rule 2 is incorrect" - Expect.equal - topic.DuplicateDetectionHistoryTimeWindow - (Nullable(TimeSpan.FromMinutes 15.)) - "Duplicate detection time incorrect" + Expect.equal + genSubscription.Rules.[3] + (Rule.CreateSqlFilter("OtherSqlThing", "Status = Failed")) + "Rule 3 is incorrect" + } + test "Same subscription in different topic is ok" { + let myServiceBus = + let makeTopic topicName = topic { + name topicName + add_subscriptions [ subscription { name "debug" } ] } - test "Can create a topic with a max size" { - let topic: SBTopic = - serviceBus { - name "my-bus" - - add_topics - [ - topic { - name "my-topic" - max_topic_size 10240 - } - ] - } - |> getResourceAtIndex 1 - Expect.equal topic.Name "my-bus/my-topic" "Name not set" - Expect.equal topic.MaxSizeInMegabytes (Nullable 10240) "Max size not set" + serviceBus { + name "mynamespace" + add_topics [ makeTopic "topicA"; makeTopic "topicB" ] } - test "Can create a basic subscription" { - let sub: SBSubscription = - serviceBus { - name "my-bus" - - add_topics - [ - topic { - name "my-topic" - add_subscriptions [ subscription { name "my-sub" } ] - } - ] - } - |> getResourceAtIndex 2 - Expect.equal sub.Name "my-bus/my-topic/my-sub" "Name not set" - } - test "Can create a forwarding subscription" { - let sub: SBSubscription = - serviceBus { - name "my-bus" - - add_topics - [ - topic { - name "my-topic" - - add_subscriptions - [ - subscription { - name "my-sub" - forward_to "my-other-topic" - } - ] - } - topic { name "my-other-topic" } - ] - } - |> getResourceAtIndex 3 + let subscriptions = + arm { add_resource myServiceBus } + |> findAzureResources dummyClient.SerializationSettings + |> List.filter (fun s -> s.Name.Contains "debug") - Expect.equal sub.ForwardTo "my-other-topic" "ForwardTo not set" - } - test "Can create a subscription with a message ttl" { - let sub: SBSubscription = - serviceBus { - name "my-bus" - - add_topics - [ - topic { - name "my-topic" - - add_subscriptions - [ - subscription { - name "my-sub" - message_ttl (TimeSpan.FromHours 2.) - } - ] - } - ] - } - |> getResourceAtIndex 2 + Expect.hasLength subscriptions 2 "Subscription length" + Expect.hasLength subscriptions 2 "Subscription length" + } + test "Topic does not create dependencies for unmanaged linked resources" { + let resource = + topic { + name "my-topic" + link_to_unmanaged_namespace "my-bus" + } + |> getResources + |> getTopicResource + |> List.head - Expect.equal sub.DefaultMessageTimeToLive (Nullable(TimeSpan.FromHours 2.)) "TTL not set" - } - test "Creates a correlation filter rule" { - let correlationRule = - ServiceBus.CorrelationFilter( - ResourceName "CompletedStatus", - Some "xyz", - Map [ "Status", "Completed"; "Operation", "DoStuff" ] - ) - - let builtCorrelationRule = - Rule.CreateCorrelationFilter( - "CompletedStatus", - [ "Status", "Completed"; "Operation", "DoStuff" ], - "xyz" - ) - - Expect.equal correlationRule builtCorrelationRule "Built incorrect correlation filter" - } - test "Can create a subscription with different filters" { - let sb = - serviceBus { - name "my-bus" - sku Standard + Expect.isEmpty resource.Dependencies "" + } + test "Topic creates dependencies for managed linked resources" { + let resource = + serviceBus { + name "my-bus" - add_topics - [ - topic { - name "my-topic" - - add_subscriptions - [ - subscription { - name "my-sub" - - add_filters - [ - Rule.CreateCorrelationFilter( - "SuccessfulStatus", - [ "Status", "Success" ] - ) - Rule.CreateSqlFilter("Thing", "Status = Success") - ] - - add_correlation_filter "FailedStatus" [ "Status", "Fail" ] - add_sql_filter "OtherSqlThing" "Status = Failed" - } - ] - } - ] + add_topics [ + topic { + name "my-topic" + link_to_unmanaged_namespace "my-namespace" } + ] + } + |> getResources + |> getTopicResource + |> List.head - let template = - arm { - location Location.EastUS - add_resource sb - } + Expect.containsAll resource.Dependencies [ ResourceId.create (namespaces, ResourceName "my-bus") ] "" + } + test "Topic creates empty dependsOn in arm template json for unmanaged linked resources" { + let template = arm { + add_resources [ + topic { + name "my-topic" + link_to_unmanaged_namespace "my-bus" + } + ] + } - let generatedTemplate = template.Template - let genSubscription = generatedTemplate.Resources.Item 2 :?> Subscription - Expect.hasLength genSubscription.Rules 4 "Expected subscription should have 4 rules" - - Expect.equal - genSubscription.Rules.[0] - (Rule.CreateCorrelationFilter("SuccessfulStatus", [ "Status", "Success" ])) - "Rule 0 is incorrect" - - Expect.equal - genSubscription.Rules.[1] - (Rule.CreateSqlFilter("Thing", "Status = Success")) - "Rule 1 is incorrect" - - Expect.equal - genSubscription.Rules.[2] - (Rule.CreateCorrelationFilter("FailedStatus", [ "Status", "Fail" ])) - "Rule 2 is incorrect" - - Expect.equal - genSubscription.Rules.[3] - (Rule.CreateSqlFilter("OtherSqlThing", "Status = Failed")) - "Rule 3 is incorrect" - } - test "Same subscription in different topic is ok" { - let myServiceBus = - let makeTopic topicName = + let dependsOn = getResourceDependsOnByName template (ResourceName "my-bus/my-topic") + Expect.hasLength dependsOn 0 "" + } + test "Topic creates dependsOn in arm template json for managed linked resources" { + let template = arm { + add_resources [ + serviceBus { + name "my-bus" + + add_topics [ topic { - name topicName - add_subscriptions [ subscription { name "debug" } ] + name "my-topic" + link_to_unmanaged_namespace "my-namespace" } + ] + } + ] + } - serviceBus { - name "mynamespace" - add_topics [ makeTopic "topicA"; makeTopic "topicB" ] - } + let dependsOn = getResourceDependsOnByName template (ResourceName "my-bus/my-topic") + Expect.hasLength dependsOn 1 "" - let subscriptions = - arm { add_resource myServiceBus } - |> findAzureResources dummyClient.SerializationSettings - |> List.filter (fun s -> s.Name.Contains "debug") + let expectedNamespaceDependency = + "[resourceId('Microsoft.ServiceBus/namespaces', 'my-bus')]" - Expect.hasLength subscriptions 2 "Subscription length" - Expect.hasLength subscriptions 2 "Subscription length" + Expect.equal dependsOn.Head expectedNamespaceDependency "" + } + test "Topic IBuilder has correct resourceId for unmanaged namespace" { + let resource = + topic { + name "my-topic" + link_to_unmanaged_namespace "my-bus" } - test "Topic does not create dependencies for unmanaged linked resources" { - let resource = - topic { - name "my-topic" - link_to_unmanaged_namespace "my-bus" - } - |> getResources - |> getTopicResource - |> List.head + :> IBuilder - Expect.isEmpty resource.Dependencies "" - } - test "Topic creates dependencies for managed linked resources" { - let resource = - serviceBus { - name "my-bus" - - add_topics - [ - topic { - name "my-topic" - link_to_unmanaged_namespace "my-namespace" - } - ] - } - |> getResources - |> getTopicResource - |> List.head - - Expect.containsAll - resource.Dependencies - [ ResourceId.create (namespaces, ResourceName "my-bus") ] - "" - } - test "Topic creates empty dependsOn in arm template json for unmanaged linked resources" { - let template = - arm { - add_resources - [ - topic { - name "my-topic" - link_to_unmanaged_namespace "my-bus" - } - ] - } + Expect.equal + (resource.ResourceId.Eval()) + "[resourceId('Microsoft.ServiceBus/namespaces/topics', 'my-bus', 'my-topic')]" + "" + } + test "Topic IArmResource has correct resourceId for unmanaged namespace" { + let resource = + topic { + name "my-topic" + link_to_unmanaged_namespace "my-bus" + } + |> getResources + |> getTopicResource + |> List.head + :> IArmResource - let dependsOn = getResourceDependsOnByName template (ResourceName "my-bus/my-topic") - Expect.hasLength dependsOn 0 "" - } - test "Topic creates dependsOn in arm template json for managed linked resources" { - let template = - arm { - add_resources - [ - serviceBus { - name "my-bus" - - add_topics - [ - topic { - name "my-topic" - link_to_unmanaged_namespace "my-namespace" - } - ] - } - ] - } + Expect.equal + (resource.ResourceId.Eval()) + "[resourceId('Microsoft.ServiceBus/namespaces/topics', 'my-bus', 'my-topic')]" + "" + } + test "Topic IBuilder has correct resourceId for managed namespace" { + let topicName = "my-topic" - let dependsOn = getResourceDependsOnByName template (ResourceName "my-bus/my-topic") - Expect.hasLength dependsOn 1 "" + let svcBus = serviceBus { + name "my-bus" - let expectedNamespaceDependency = - "[resourceId('Microsoft.ServiceBus/namespaces', 'my-bus')]" + add_topics [ + topic { + name topicName + link_to_unmanaged_namespace "other-namespace" + } + ] + } - Expect.equal dependsOn.Head expectedNamespaceDependency "" - } - test "Topic IBuilder has correct resourceId for unmanaged namespace" { - let resource = - topic { - name "my-topic" - link_to_unmanaged_namespace "my-bus" - } - :> IBuilder + let topicBuilder = svcBus.Topics |> Map.find (ResourceName topicName) :> IBuilder - Expect.equal - (resource.ResourceId.Eval()) - "[resourceId('Microsoft.ServiceBus/namespaces/topics', 'my-bus', 'my-topic')]" - "" - } - test "Topic IArmResource has correct resourceId for unmanaged namespace" { - let resource = + Expect.equal + (topicBuilder.ResourceId.Eval()) + $"[resourceId('Microsoft.ServiceBus/namespaces/topics', 'my-bus', '{topicName}')]" + "" + } + test "Topic IArmResource has correct resourceId for managed namespace" { + let topicName = "my-topic" + + let resource = + serviceBus { + name "my-bus" + + add_topics [ topic { - name "my-topic" - link_to_unmanaged_namespace "my-bus" + name topicName + link_to_unmanaged_namespace "other-namespace" } - |> getResources - |> getTopicResource - |> List.head - :> IArmResource - - Expect.equal - (resource.ResourceId.Eval()) - "[resourceId('Microsoft.ServiceBus/namespaces/topics', 'my-bus', 'my-topic')]" - "" + ] } - test "Topic IBuilder has correct resourceId for managed namespace" { - let topicName = "my-topic" + |> getResources + |> getTopicResource + |> List.head + :> IArmResource - let svcBus = + Expect.equal + (resource.ResourceId.Eval()) + $"[resourceId('Microsoft.ServiceBus/namespaces/topics', 'my-bus', '{topicName}')]" + "" + } + ] + + testList "Namespace AuthorizationRule Tests" [ + test "AuthorizationRule should not be present by default" { + let sbAuthorizationRules = + arm { + add_resource ( serviceBus { - name "my-bus" - - add_topics - [ - topic { - name topicName - link_to_unmanaged_namespace "other-namespace" - } - ] + name "serviceBus" + sku Standard } - - let topicBuilder = svcBus.Topics |> Map.find (ResourceName topicName) :> IBuilder - - Expect.equal - (topicBuilder.ResourceId.Eval()) - $"[resourceId('Microsoft.ServiceBus/namespaces/topics', 'my-bus', '{topicName}')]" - "" + ) } - test "Topic IArmResource has correct resourceId for managed namespace" { - let topicName = "my-topic" + |> findAzureResources dummyClient.SerializationSettings + |> List.filter (fun x -> (=) x.Type namespaceAuthorizationRules.Type) - let resource = + Expect.equal sbAuthorizationRules.Length 0 "AuthorizationRule should not be present" + } + test "AuthorizationRule should write correct ARM template" { + let sbAuthorizationRule = + arm { + add_resource ( serviceBus { - name "my-bus" - - add_topics - [ - topic { - name topicName - link_to_unmanaged_namespace "other-namespace" - } - ] - } - |> getResources - |> getTopicResource - |> List.head - :> IArmResource - - Expect.equal - (resource.ResourceId.Eval()) - $"[resourceId('Microsoft.ServiceBus/namespaces/topics', 'my-bus', '{topicName}')]" - "" - } - ] - - testList - "Namespace AuthorizationRule Tests" - [ - test "AuthorizationRule should not be present by default" { - let sbAuthorizationRules = - arm { - add_resource ( - serviceBus { - name "serviceBus" - sku Standard - } - ) + name "serviceBus" + sku Standard + add_authorization_rule "my-rule" [ Manage ] } - |> findAzureResources dummyClient.SerializationSettings - |> List.filter (fun x -> (=) x.Type namespaceAuthorizationRules.Type) - - Expect.equal sbAuthorizationRules.Length 0 "AuthorizationRule should not be present" + ) } - test "AuthorizationRule should write correct ARM template" { - let sbAuthorizationRule = - arm { - add_resource ( - serviceBus { - name "serviceBus" - sku Standard - add_authorization_rule "my-rule" [ Manage ] - } - ) - } - |> findAzureResources dummyClient.SerializationSettings - |> List.filter (fun x -> (=) x.Type namespaceAuthorizationRules.Type) - |> List.head + |> findAzureResources dummyClient.SerializationSettings + |> List.filter (fun x -> (=) x.Type namespaceAuthorizationRules.Type) + |> List.head - sbAuthorizationRule.Validate() + sbAuthorizationRule.Validate() - Expect.equal sbAuthorizationRule.Name "serviceBus/my-rule" "Wrong name" - Expect.equal sbAuthorizationRule.Rights.Count 1 "Wrong number of rights" - Expect.equal sbAuthorizationRule.Rights.[0] (Nullable AccessRights.Manage) "Wrong rights" - } - ] + Expect.equal sbAuthorizationRule.Name "serviceBus/my-rule" "Wrong name" + Expect.equal sbAuthorizationRule.Rights.Count 1 "Wrong number of rights" + Expect.equal sbAuthorizationRule.Rights.[0] (Nullable AccessRights.Manage) "Wrong rights" + } ] + ] diff --git a/src/Tests/ServicePlan.fs b/src/Tests/ServicePlan.fs index 8fa239b91..c98234f8a 100644 --- a/src/Tests/ServicePlan.fs +++ b/src/Tests/ServicePlan.fs @@ -15,60 +15,56 @@ let getResource<'T when 'T :> IArmResource> (data: IArmResource list) = 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 + 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 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") + 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" + 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 } - test "Enable zoneRedundant in service plan" { - let servicePlan = - servicePlan { - name "test" - zone_redundant Enabled - } + let sf = servicePlan |> getResources |> getResource |> List.head - 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 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") - 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" + } - 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 } - test "Disable zoneRedundant in service plan" { - let servicePlan = - servicePlan { - name "test" - zone_redundant Disabled - } - - let sf = servicePlan |> getResources |> getResource |> List.head + 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 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") + 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" - } - ] + 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" + } + ] diff --git a/src/Tests/SignalR.fs b/src/Tests/SignalR.fs index ebed74022..32e090cc2 100644 --- a/src/Tests/SignalR.fs +++ b/src/Tests/SignalR.fs @@ -15,79 +15,74 @@ let client = new SignalRManagementClient(Uri "http://management.azure.com", TokenCredentials "NotNullOrWhiteSpace") let tests = - testList - "SignalR" - [ - test "Can create a basic SignalR account" { - let resource = - let mySignalR = - signalR { - name "my-signalr~@" - sku Free - } + testList "SignalR" [ + test "Can create a basic SignalR account" { + let resource = + let mySignalR = signalR { + name "my-signalr~@" + sku Free + } - arm { add_resource mySignalR } - |> findAzureResources client.SerializationSettings - |> List.head + arm { add_resource mySignalR } + |> findAzureResources client.SerializationSettings + |> List.head - resource.Validate() - Expect.equal resource.Name "my-signalr" "Name does not match" - Expect.equal resource.Sku.Name "Free_F1" "SKU does not match" - } + resource.Validate() + Expect.equal resource.Name "my-signalr" "Name does not match" + Expect.equal resource.Sku.Name "Free_F1" "SKU does not match" + } - test "Can create a SignalR account with specific allowed origins" { - let resource = - let mySignalR = - signalR { - name "my-signalr~@" - sku Free - allowed_origins [ "https://github.com"; "https://duckduckgo.com" ] - } + test "Can create a SignalR account with specific allowed origins" { + let resource = + let mySignalR = signalR { + name "my-signalr~@" + sku Free + allowed_origins [ "https://github.com"; "https://duckduckgo.com" ] + } - arm { add_resource mySignalR } - |> findAzureResources client.SerializationSettings - |> List.head + arm { add_resource mySignalR } + |> findAzureResources client.SerializationSettings + |> List.head - resource.Validate() - Expect.equal resource.Name "my-signalr" "Name does not match" - Expect.equal resource.Sku.Name "Free_F1" "SKU does not match" + resource.Validate() + Expect.equal resource.Name "my-signalr" "Name does not match" + Expect.equal resource.Sku.Name "Free_F1" "SKU does not match" - Expect.containsAll - resource.Cors.AllowedOrigins - [ "https://github.com"; "https://duckduckgo.com" ] - "Missing some or all allowed origins" - } + Expect.containsAll + resource.Cors.AllowedOrigins + [ "https://github.com"; "https://duckduckgo.com" ] + "Missing some or all allowed origins" + } - test "Can create a SignalR account with specific capacity" { - let resource = - let mySignalR = - signalR { - name "my-signalr~@" - sku Standard - capacity 10 - } + test "Can create a SignalR account with specific capacity" { + let resource = + let mySignalR = signalR { + name "my-signalr~@" + sku Standard + capacity 10 + } - arm { add_resource mySignalR } - |> findAzureResources client.SerializationSettings - |> List.head + arm { add_resource mySignalR } + |> findAzureResources client.SerializationSettings + |> List.head - resource.Validate() - Expect.equal resource.Name "my-signalr" "Name does not match" - Expect.equal resource.Sku.Name "Standard_S1" "SKU does not match" - Expect.equal resource.Sku.Capacity (Nullable 10) "Capacity does not match" - } + resource.Validate() + Expect.equal resource.Name "my-signalr" "Name does not match" + Expect.equal resource.Sku.Name "Standard_S1" "SKU does not match" + Expect.equal resource.Sku.Capacity (Nullable 10) "Capacity does not match" + } - test "Key is correctly emitted" { - let mySignalR = signalR { name "my-signalr" } + test "Key is correctly emitted" { + let mySignalR = signalR { name "my-signalr" } - Expect.equal - "[listKeys(resourceId('Microsoft.SignalRService/SignalR', 'my-signalr'), providers('Microsoft.SignalRService', 'SignalR').apiVersions[0]).primaryKey]" - (mySignalR.Key.Eval()) - "Key is incorrect" + Expect.equal + "[listKeys(resourceId('Microsoft.SignalRService/SignalR', 'my-signalr'), providers('Microsoft.SignalRService', 'SignalR').apiVersions[0]).primaryKey]" + (mySignalR.Key.Eval()) + "Key is incorrect" - Expect.equal - "[listKeys(resourceId('Microsoft.SignalRService/SignalR', 'my-signalr'), providers('Microsoft.SignalRService', 'SignalR').apiVersions[0]).primaryConnectionString]" - (mySignalR.ConnectionString.Eval()) - "Connection String is incorrect" - } - ] + Expect.equal + "[listKeys(resourceId('Microsoft.SignalRService/SignalR', 'my-signalr'), providers('Microsoft.SignalRService', 'SignalR').apiVersions[0]).primaryConnectionString]" + (mySignalR.ConnectionString.Eval()) + "Connection String is incorrect" + } + ] diff --git a/src/Tests/Sql.fs b/src/Tests/Sql.fs index df261e75e..520ff87e2 100644 --- a/src/Tests/Sql.fs +++ b/src/Tests/Sql.fs @@ -14,320 +14,298 @@ let client = new SqlManagementClient(Uri "http://management.azure.com", TokenCredentials "NotNullOrWhiteSpace") let tests = - testList - "SQL Server" - [ - test "Can create a basic server and DB" { - let sql = - sqlServer { - name "server" - admin_username "isaac" - - add_databases - [ - sqlDb { - name "db" - sku DtuSku.S0 - } - ] + testList "SQL Server" [ + test "Can create a basic server and DB" { + let sql = sqlServer { + name "server" + admin_username "isaac" + + add_databases [ + sqlDb { + name "db" + sku DtuSku.S0 } - - let model: Models.Server = sql |> getResourceAtIndex client.SerializationSettings 0 - Expect.equal model.Name "server" "Incorrect Server name" - Expect.equal model.AdministratorLogin "isaac" "Incorrect Administration Login" - - let model: Models.Database = - sql |> getResourceAtIndex client.SerializationSettings 1 - - Expect.equal model.Name "server/db" "Incorrect database name" - Expect.equal model.Sku.Name "S0" "Incorrect SKU" + ] } - test "Transparent data encryption name" { - let sql = - sqlServer { - name "server" - admin_username "isaac" + let model: Models.Server = sql |> getResourceAtIndex client.SerializationSettings 0 + Expect.equal model.Name "server" "Incorrect Server name" + Expect.equal model.AdministratorLogin "isaac" "Incorrect Administration Login" - add_databases - [ - sqlDb { - name "db" - use_encryption - sku DtuSku.S0 - } - ] - } + let model: Models.Database = + sql |> getResourceAtIndex client.SerializationSettings 1 - let encryptionModel: Models.TransparentDataEncryption = - sql |> getResourceAtIndex client.SerializationSettings 2 + Expect.equal model.Name "server/db" "Incorrect database name" + Expect.equal model.Sku.Name "S0" "Incorrect SKU" + } - Expect.equal encryptionModel.Name "server/db/current" "Should always equal to current" - } + test "Transparent data encryption name" { + let sql = sqlServer { + name "server" + admin_username "isaac" - test "Creates an elastic pool where needed" { - let sql = - sqlServer { - name "server" - admin_username "isaac" - elastic_pool_sku PoolSku.Basic200 - add_databases [ sqlDb { name "db" } ] + add_databases [ + sqlDb { + name "db" + use_encryption + sku DtuSku.S0 } + ] + } - let model: Models.Database = - sql |> getResourceAtIndex client.SerializationSettings 1 - - Expect.isNull model.Sku "Should not be a SKU on the DB" - - Expect.equal - "[resourceId('Microsoft.Sql/servers/elasticPools', 'server', 'server-pool')]" - model.ElasticPoolId - "Incorrect pool reference" + let encryptionModel: Models.TransparentDataEncryption = + sql |> getResourceAtIndex client.SerializationSettings 2 - let model: Models.ElasticPool = - sql |> getResourceAtIndex client.SerializationSettings 2 + Expect.equal encryptionModel.Name "server/db/current" "Should always equal to current" + } - Expect.equal model.Sku.Name "BasicPool" "Incorrect Elastic Pool SKU" - Expect.equal model.Sku.Capacity (Nullable 200) "Incorrect Elastic Pool SKU size" + test "Creates an elastic pool where needed" { + let sql = sqlServer { + name "server" + admin_username "isaac" + elastic_pool_sku PoolSku.Basic200 + add_databases [ sqlDb { name "db" } ] } - test "Works with VCore databases" { - let sql = - sqlServer { - name "server" - admin_username "isaac" + let model: Models.Database = + sql |> getResourceAtIndex client.SerializationSettings 1 - add_databases - [ - sqlDb { - name "db" - sku M_18 - } - ] - } + Expect.isNull model.Sku "Should not be a SKU on the DB" - let model: Models.Database = - sql |> getResourceAtIndex client.SerializationSettings 1 + Expect.equal + "[resourceId('Microsoft.Sql/servers/elasticPools', 'server', 'server-pool')]" + model.ElasticPoolId + "Incorrect pool reference" - Expect.equal model.Sku.Name "BC_M_18" "Incorrect SKU" - Expect.equal model.LicenseType "LicenseIncluded" "Incorrect License" - } + let model: Models.ElasticPool = + sql |> getResourceAtIndex client.SerializationSettings 2 - test "Cannot set hybrid if not VCore" { - Expect.throws - (fun () -> - sqlServer { - name "server" - admin_username "isaac" - - add_databases - [ - sqlDb { - name "db" - hybrid_benefit - } - ] - } - |> ignore) - "Shouldn't set hybrid on non-VCore" - } + Expect.equal model.Sku.Name "BasicPool" "Incorrect Elastic Pool SKU" + Expect.equal model.Sku.Capacity (Nullable 200) "Incorrect Elastic Pool SKU size" + } - test "Sets license and size correctly" { - let sql = - sqlServer { - name "server" - admin_username "isaac" + test "Works with VCore databases" { + let sql = sqlServer { + name "server" + admin_username "isaac" - add_databases - [ - sqlDb { - name "db" - sku (GeneralPurpose Gen5_12) - hybrid_benefit - db_size 2048 - } - ] + add_databases [ + sqlDb { + name "db" + sku M_18 } + ] + } - let model: Models.Database = - sql |> getResourceAtIndex client.SerializationSettings 1 + let model: Models.Database = + sql |> getResourceAtIndex client.SerializationSettings 1 - Expect.equal model.Sku.Name "GP_Gen5_12" "Incorrect SKU" - Expect.equal model.MaxSizeBytes (Nullable 2147483648L) "Incorrect Size" - Expect.equal model.LicenseType "BasePrice" "Incorrect SKU" - } + Expect.equal model.Sku.Name "BC_M_18" "Incorrect SKU" + Expect.equal model.LicenseType "LicenseIncluded" "Incorrect License" + } - test "SQL Firewall is correctly configured" { - let sql = + test "Cannot set hybrid if not VCore" { + Expect.throws + (fun () -> sqlServer { name "server" admin_username "isaac" - add_firewall_rule "Rule" "0.0.0.0" "255.255.255.255" - add_databases [ sqlDb { name "db" } ] - } - - let model: Models.FirewallRule = - sql |> getResourceAtIndex client.SerializationSettings 2 - Expect.equal model.StartIpAddress "0.0.0.0" "Incorrect start IP" - Expect.equal model.EndIpAddress "255.255.255.255" "Incorrect end IP" + add_databases [ + sqlDb { + name "db" + hybrid_benefit + } + ] + } + |> ignore) + "Shouldn't set hybrid on non-VCore" + } + + test "Sets license and size correctly" { + let sql = sqlServer { + name "server" + admin_username "isaac" + + add_databases [ + sqlDb { + name "db" + sku (GeneralPurpose Gen5_12) + hybrid_benefit + db_size 2048 + } + ] } - test "SQL Firewall is correctly configured with list" { - let sql = - sqlServer { - name "server" - admin_username "isaac" - add_firewall_rules [ "Rule", "0.0.0.0", "255.255.255.255" ] - add_databases [ sqlDb { name "db" } ] - } + let model: Models.Database = + sql |> getResourceAtIndex client.SerializationSettings 1 - let model: Models.FirewallRule = - sql |> getResourceAtIndex client.SerializationSettings 2 + Expect.equal model.Sku.Name "GP_Gen5_12" "Incorrect SKU" + Expect.equal model.MaxSizeBytes (Nullable 2147483648L) "Incorrect Size" + Expect.equal model.LicenseType "BasePrice" "Incorrect SKU" + } - Expect.equal model.StartIpAddress "0.0.0.0" "Incorrect start IP" - Expect.equal model.EndIpAddress "255.255.255.255" "Incorrect end IP" + test "SQL Firewall is correctly configured" { + let sql = sqlServer { + name "server" + admin_username "isaac" + add_firewall_rule "Rule" "0.0.0.0" "255.255.255.255" + add_databases [ sqlDb { name "db" } ] } - test "Validation occurs on account name" { - let check (v: string) m = - Expect.equal (SqlAccountName.Create v) (Error("SQL account names " + m)) - - check "" "cannot be empty" "Name too short" - let longName = Array.init 64 (fun _ -> 'a') |> String - check longName $"max length is 63, but here is 64. The invalid value is '{longName}'" "Name too long" - - check - "zzzT" - "can only contain lowercase letters. The invalid value is 'zzzT'" - "Upper case character allowed" + let model: Models.FirewallRule = + sql |> getResourceAtIndex client.SerializationSettings 2 - check - "zz!z" - "can only contain alphanumeric characters or the dash (-). The invalid value is 'zz!z'" - "Bad character allowed" + Expect.equal model.StartIpAddress "0.0.0.0" "Incorrect start IP" + Expect.equal model.EndIpAddress "255.255.255.255" "Incorrect end IP" + } - check "-zz" "cannot start with a dash (-). The invalid value is '-zz'" "Start with dash" - check "zz-" "cannot end with a dash (-). The invalid value is 'zz-'" "End with dash" + test "SQL Firewall is correctly configured with list" { + let sql = sqlServer { + name "server" + admin_username "isaac" + add_firewall_rules [ "Rule", "0.0.0.0", "255.255.255.255" ] + add_databases [ sqlDb { name "db" } ] } - test "Sets Min TLS version correctly" { - let sql = - sqlServer { - name "server" - admin_username "isaac" - add_databases - [ - sqlDb { - name "db" - sku DtuSku.S0 - } - ] - - min_tls_version Tls12 + let model: Models.FirewallRule = + sql |> getResourceAtIndex client.SerializationSettings 2 + + Expect.equal model.StartIpAddress "0.0.0.0" "Incorrect start IP" + Expect.equal model.EndIpAddress "255.255.255.255" "Incorrect end IP" + } + + test "Validation occurs on account name" { + let check (v: string) m = + Expect.equal (SqlAccountName.Create v) (Error("SQL account names " + m)) + + check "" "cannot be empty" "Name too short" + let longName = Array.init 64 (fun _ -> 'a') |> String + check longName $"max length is 63, but here is 64. The invalid value is '{longName}'" "Name too long" + + check + "zzzT" + "can only contain lowercase letters. The invalid value is 'zzzT'" + "Upper case character allowed" + + check + "zz!z" + "can only contain alphanumeric characters or the dash (-). The invalid value is 'zz!z'" + "Bad character allowed" + + check "-zz" "cannot start with a dash (-). The invalid value is '-zz'" "Start with dash" + check "zz-" "cannot end with a dash (-). The invalid value is 'zz-'" "End with dash" + } + test "Sets Min TLS version correctly" { + let sql = sqlServer { + name "server" + admin_username "isaac" + + add_databases [ + sqlDb { + name "db" + sku DtuSku.S0 } + ] - let model: Models.Server = sql |> getResourceAtIndex client.SerializationSettings 0 - Expect.equal model.Name "server" "Incorrect Server name" - Expect.equal model.MinimalTlsVersion "1.2" "Min TLS version is wrong" + min_tls_version Tls12 } - test "Test Geo-replication" { - let sql = - sqlServer { - name "my36server" - admin_username "isaac" + let model: Models.Server = sql |> getResourceAtIndex client.SerializationSettings 0 + Expect.equal model.Name "server" "Incorrect Server name" + Expect.equal model.MinimalTlsVersion "1.2" "Min TLS version is wrong" + } - add_databases - [ - sqlDb { - name "mydb21" - sku DtuSku.S0 - } - ] - - geo_replicate ( - { - NameSuffix = "geo" - Location = Location.UKWest - DbSku = Some Farmer.Sql.DtuSku.S0 - } - ) + test "Test Geo-replication" { + let sql = sqlServer { + name "my36server" + admin_username "isaac" + + add_databases [ + sqlDb { + name "mydb21" + sku DtuSku.S0 } + ] - let template = - arm { - location Location.UKSouth - add_resources [ sql ] + geo_replicate ( + { + NameSuffix = "geo" + Location = Location.UKWest + DbSku = Some Farmer.Sql.DtuSku.S0 } + ) + } - let jsn = template.Template |> Writer.toJson - let jobj = jsn |> Newtonsoft.Json.Linq.JObject.Parse + let template = arm { + location Location.UKSouth + add_resources [ sql ] + } - let geoLocated = - jobj.SelectToken("resources[?(@.name=='my36servergeo/mydb21geo')].location") + let jsn = template.Template |> Writer.toJson + let jobj = jsn |> Newtonsoft.Json.Linq.JObject.Parse - Expect.equal (geoLocated.ToString()) "ukwest" "Geo-replication with location not found" - () - } + let geoLocated = + jobj.SelectToken("resources[?(@.name=='my36servergeo/mydb21geo')].location") - test "Serverless sql has min and max capacity" { - let sql = - sqlServer { - name "my37server" - admin_username "isaac" + Expect.equal (geoLocated.ToString()) "ukwest" "Geo-replication with location not found" + () + } - add_databases - [ - sqlDb { - name "mydb22" - sku (GeneralPurpose(S_Gen5(2, 4))) - } - ] - } + test "Serverless sql has min and max capacity" { + let sql = sqlServer { + name "my37server" + admin_username "isaac" - let template = - arm { - location Location.UKSouth - add_resources [ sql ] + add_databases [ + sqlDb { + name "mydb22" + sku (GeneralPurpose(S_Gen5(2, 4))) } - - let jsn = template.Template |> Writer.toJson - let jobj = jsn |> Newtonsoft.Json.Linq.JObject.Parse - - Expect.equal - (jobj - .SelectToken("resources[?(@.name=='my37server/mydb22')].sku.name") - .ToString()) - "GP_S_Gen5" - "Not serverless name" - - Expect.equal - (jobj - .SelectToken("resources[?(@.name=='my37server/mydb22')].sku.capacity") - .ToString()) - "4" - "Incorrect max capacity" - - Expect.equal - (jobj - .SelectToken("resources[?(@.name=='my37server/mydb22')].properties.minCapacity") - .ToString()) - "2" - "Incorrect min capacity" - - Expect.equal - (jobj - .SelectToken("resources[?(@.name=='my37server/mydb22')].properties.autoPauseDelay") - .ToString()) - "-1" - "Incorrect autoPauseDelay" + ] } - - test "Must set a SQL Server account name" { - Expect.throws - (fun () -> sqlServer { admin_username "test" } |> ignore) - "Must set a name on a sql server account" + let template = arm { + location Location.UKSouth + add_resources [ sql ] } - ] + + let jsn = template.Template |> Writer.toJson + let jobj = jsn |> Newtonsoft.Json.Linq.JObject.Parse + + Expect.equal + (jobj + .SelectToken("resources[?(@.name=='my37server/mydb22')].sku.name") + .ToString()) + "GP_S_Gen5" + "Not serverless name" + + Expect.equal + (jobj + .SelectToken("resources[?(@.name=='my37server/mydb22')].sku.capacity") + .ToString()) + "4" + "Incorrect max capacity" + + Expect.equal + (jobj + .SelectToken("resources[?(@.name=='my37server/mydb22')].properties.minCapacity") + .ToString()) + "2" + "Incorrect min capacity" + + Expect.equal + (jobj + .SelectToken("resources[?(@.name=='my37server/mydb22')].properties.autoPauseDelay") + .ToString()) + "-1" + "Incorrect autoPauseDelay" + } + + + test "Must set a SQL Server account name" { + Expect.throws + (fun () -> sqlServer { admin_username "test" } |> ignore) + "Must set a name on a sql server account" + } + ] diff --git a/src/Tests/StaticWebApp.fs b/src/Tests/StaticWebApp.fs index b551edf3b..d30d43264 100644 --- a/src/Tests/StaticWebApp.fs +++ b/src/Tests/StaticWebApp.fs @@ -8,54 +8,49 @@ open Farmer.Arm open System let tests = - testList - "Static Web App Tests" - [ - test "Creates a basic static web app" { - let swa = - staticWebApp { - name "foo" - api_location "api" - app_location "app" - artifact_location "artifact" - branch "feature" - repository "https://compositional-it.com" - } - - let swaArm = - (swa :> IBuilder).BuildResources(Location.WestEurope).[0] :?> StaticSite - - Expect.equal swaArm.ApiLocation (Some "api") "Api" - Expect.equal swaArm.Name (ResourceName "foo") "Name" - Expect.equal swaArm.AppLocation "app" "AppLocation" - Expect.equal swaArm.AppArtifactLocation (Some "artifact") "ArtifactLocation" - Expect.equal swaArm.Branch "feature" "Branch" - Expect.equal swaArm.Repository (Uri "https://compositional-it.com") "Repository" + testList "Static Web App Tests" [ + test "Creates a basic static web app" { + let swa = staticWebApp { + name "foo" + api_location "api" + app_location "app" + artifact_location "artifact" + branch "feature" + repository "https://compositional-it.com" } - test "Defaults to master branch" { - let swa = - staticWebApp { - name "foo" - repository "https://compositional-it.com" - } - let swaArm = - (swa :> IBuilder).BuildResources(Location.WestEurope).[0] :?> StaticSite - - Expect.equal swaArm.Branch "master" "Branch" + let swaArm = + (swa :> IBuilder).BuildResources(Location.WestEurope).[0] :?> StaticSite + + Expect.equal swaArm.ApiLocation (Some "api") "Api" + Expect.equal swaArm.Name (ResourceName "foo") "Name" + Expect.equal swaArm.AppLocation "app" "AppLocation" + Expect.equal swaArm.AppArtifactLocation (Some "artifact") "ArtifactLocation" + Expect.equal swaArm.Branch "feature" "Branch" + Expect.equal swaArm.Repository (Uri "https://compositional-it.com") "Repository" + } + test "Defaults to master branch" { + let swa = staticWebApp { + name "foo" + repository "https://compositional-it.com" } - test "Supports app settings" { - let swa = - staticWebApp { - name "foo" - repository "https://compositional-it.com" - app_settings [ "foo", "bar"; "blip", "blop" ] - } + let swaArm = + (swa :> IBuilder).BuildResources(Location.WestEurope).[0] :?> StaticSite - let swaArm = - (swa :> IBuilder).BuildResources(Location.WestEurope).[1] :?> StaticSites.Config + Expect.equal swaArm.Branch "master" "Branch" + } + test "Supports app settings" { + let swa = staticWebApp { + name "foo" + repository "https://compositional-it.com" + app_settings [ "foo", "bar"; "blip", "blop" ] - Expect.equal swaArm.Properties (Map [ "foo", "bar"; "blip", "blop" ]) "App Settings not set" } - ] + + let swaArm = + (swa :> IBuilder).BuildResources(Location.WestEurope).[1] :?> StaticSites.Config + + Expect.equal swaArm.Properties (Map [ "foo", "bar"; "blip", "blop" ]) "App Settings not set" + } + ] diff --git a/src/Tests/Storage.fs b/src/Tests/Storage.fs index ad1e4090d..13c41716e 100644 --- a/src/Tests/Storage.fs +++ b/src/Tests/Storage.fs @@ -17,23 +17,39 @@ let client = let getStorageResource = findAzureResources client.SerializationSettings >> List.head -type PropertiesResource = - {| ``type``: string - properties: {| cors: {| corsRules: {| allowedHeaders: string array - allowedMethods: string array - allowedOrigins: string array - exposedHeaders: string array - maxAgeInSeconds: int |} array |} - IsVersioningEnabled: bool - deleteRetentionPolicy: {| enabled: bool; days: int |} - restorePolicy: {| enabled: bool; days: int |} - containerDeleteRetentionPolicy: {| enabled: bool; days: int |} - lastAccessTimeTrackingPolicy: {| enable: bool - name: string - trackingGranularityInDays: int - blobType: string[] |} - changeFeed: {| enabled: bool - retentionInDays: int |} |} |} +type PropertiesResource = {| + ``type``: string + properties: + {| + cors: + {| + corsRules: + {| + allowedHeaders: string array + allowedMethods: string array + allowedOrigins: string array + exposedHeaders: string array + maxAgeInSeconds: int + |} array + |} + IsVersioningEnabled: bool + deleteRetentionPolicy: {| enabled: bool; days: int |} + restorePolicy: {| enabled: bool; days: int |} + containerDeleteRetentionPolicy: {| enabled: bool; days: int |} + lastAccessTimeTrackingPolicy: + {| + enable: bool + name: string + trackingGranularityInDays: int + blobType: string[] + |} + changeFeed: + {| + enabled: bool + retentionInDays: int + |} + |} +|} let findPropertiesResource typeName x = x @@ -45,773 +61,733 @@ let findPropertiesResource typeName x = |> Seq.find (fun r -> r.``type`` = $"Microsoft.Storage/storageAccounts/%s{typeName}") let tests = - testList - "Storage Tests" - [ - test "Can create a basic storage account" { - let resource = - let account = storageAccount { name "mystorage123" } - arm { add_resource account } |> getStorageResource - - resource.Validate() - Expect.equal resource.Name "mystorage123" "Account name is wrong" - Expect.equal resource.Sku.Name "Standard_LRS" "SKU is wrong" - Expect.equal resource.Kind "StorageV2" "Kind" - Expect.equal resource.IsHnsEnabled (Nullable()) "Hierarchical namespace shouldn't be included" - Expect.equal resource.MinimumTlsVersion null "Minimum TLS version shouldn't be included" - } - test "Data lake is not enabled by default" { - let resource = - let account = - storageAccount { - name "mystorage123" - sku Sku.Premium_LRS - enable_data_lake true - } - - arm { add_resource account } |> getStorageResource + testList "Storage Tests" [ + test "Can create a basic storage account" { + let resource = + let account = storageAccount { name "mystorage123" } + arm { add_resource account } |> getStorageResource + + resource.Validate() + Expect.equal resource.Name "mystorage123" "Account name is wrong" + Expect.equal resource.Sku.Name "Standard_LRS" "SKU is wrong" + Expect.equal resource.Kind "StorageV2" "Kind" + Expect.equal resource.IsHnsEnabled (Nullable()) "Hierarchical namespace shouldn't be included" + Expect.equal resource.MinimumTlsVersion null "Minimum TLS version shouldn't be included" + } + test "Data lake is not enabled by default" { + let resource = + let account = storageAccount { + name "mystorage123" + sku Sku.Premium_LRS + enable_data_lake true + } + + arm { add_resource account } |> getStorageResource + + resource.Validate() + Expect.equal resource.Sku.Name "Premium_LRS" "SKU is wrong" + Expect.isTrue resource.IsHnsEnabled.Value "Hierarchical namespace not enabled" + } + test "When data lake can be disabled" { + let resource = + let account = storageAccount { + name "mystorage123" + enable_data_lake false + } + + arm { add_resource account } |> getStorageResource + + resource.Validate() + Expect.isFalse resource.IsHnsEnabled.Value "Hierarchical namespace should be false" + } + test "Creates containers correctly" { + let resources: BlobContainer list = + let account = storageAccount { + name "storage" + add_blob_container "blob" + add_private_container "private" + add_public_container "public" + } + + [ + for i in 1..3 do + account |> getResourceAtIndex client.SerializationSettings i + ] + + Expect.equal resources.[0].Name "storage/default/blob" "blob name is wrong" + Expect.equal resources.[0].PublicAccess.Value PublicAccess.Blob "blob access is wrong" + Expect.equal resources.[1].Name "storage/default/private" "private name is wrong" + Expect.equal resources.[1].PublicAccess.Value PublicAccess.None "private access is wrong" + Expect.equal resources.[2].Name "storage/default/public" "public name is wrong" + Expect.equal resources.[2].PublicAccess.Value PublicAccess.Container "container access is wrong" + } + test "Creates file shares correctly" { + let resources: FileShare list = + let account = storageAccount { + name "storage" + add_file_share "share1" + add_file_share_with_quota "share2" 1024 + } + + [ + for i in 1..2 do + account |> getResourceAtIndex client.SerializationSettings i + ] + + Expect.equal resources.[0].Name "storage/default/share1" "file share name for 'share1' is wrong" + Expect.equal resources.[1].Name "storage/default/share2" "file share name for 'share2' is wrong" + Expect.equal resources.[1].ShareQuota (Nullable 1024) "file share quota for 'share2' is wrong" + } + test "Creates tables correctly" { + let resources: Table list = + let account = storageAccount { + name "storage" + add_table "table1" + add_tables [ "table2"; "table3" ] + } + + [ + for i in 1..3 do + account |> getResourceAtIndex client.SerializationSettings i + ] + + Expect.equal resources.[0].Name "storage/default/table1" "table name for 'table1' is wrong" + Expect.equal resources.[1].Name "storage/default/table2" "table name for 'table2' is wrong" + Expect.equal resources.[2].Name "storage/default/table3" "table name for 'table3' is wrong" + } + test "Creates queues correctly" { + let resources: StorageQueue list = + let account = storageAccount { + name "storage" + add_queue "queue1" + add_queues [ "queue2"; "queue3" ] + } + + [ + for i in 1..3 do + account |> getResourceAtIndex client.SerializationSettings i + ] + + Expect.equal resources.[0].Name "storage/default/queue1" "queue name for 'queue1' is wrong" + Expect.equal resources.[1].Name "storage/default/queue2" "queue name for 'queue2' is wrong" + Expect.equal resources.[2].Name "storage/default/queue3" "queue name for 'queue3' is wrong" + } + test "Rejects invalid storage accounts" { + let check (v: string) m = + Expect.equal (StorageAccountName.Create v) (Error("Storage account names " + m)) + + check "" "cannot be empty" "Name too short" + check "zz" "min length is 3, but here is 2. The invalid value is 'zz'" "Name too short" + + check + "abcdefghij1234567890abcde" + "max length is 24, but here is 25. The invalid value is 'abcdefghij1234567890abcde'" + "Name too long" + + check + "zzzT" + "can only contain lowercase letters. The invalid value is 'zzzT'" + "Upper case character allowed" + + check + "zzz!" + "can only contain alphanumeric characters. The invalid value is 'zzz!'" + "Non alpha numeric character allowed" + + Expect.equal + (StorageResourceName.Create("abcdefghij1234567890abcd").OkValue.ResourceName) + (ResourceName "abcdefghij1234567890abcd") + "Should have created a valid storage account name" + } + test "Rejects invalid storage resource names" { + let check (v: string) m = + Expect.equal (StorageResourceName.Create v) (Error("Storage resource names " + m)) + + check "" "cannot be empty" "Name too short" + check "zz" "min length is 3, but here is 2. The invalid value is 'zz'" "Name too short" + let longName = Array.init 64 (fun _ -> 'a') |> String + check longName $"max length is 63, but here is 64. The invalid value is '{longName}'" "Name too long" + + check + "zzzT" + "can only contain lowercase letters. The invalid value is 'zzzT'" + "Upper case character allowed" + + check + "zz!z" + "can only contain alphanumeric characters or the dash (-). The invalid value is 'zz!z'" + "Bad character allowed" + + check "zzz--z" "do not allow consecutive dashes. The invalid value is 'zzz--z'" "Double dash allowed" + check "-zz" "must start with an alphanumeric character. The invalid value is '-zz'" "Start with dash" + check "zz-" "must end with an alphanumeric character. The invalid value is 'zz-'" "End with dash" + + Expect.equal + (StorageResourceName.Create("abcdefghij1234567890abcd").OkValue.ResourceName) + (ResourceName "abcdefghij1234567890abcd") + "Should have created a valid storage resource name" + } + test "Adds lifecycle policies correctly" { + let resource: ManagementPolicy = + let account = storageAccount { + name "storage" + add_lifecycle_rule "cleanup" [ Storage.DeleteAfter 7 ] Storage.NoRuleFilters + + add_lifecycle_rule "test" [ + Storage.DeleteAfter 1 + Storage.DeleteAfter 2 + Storage.ArchiveAfter 2 + ] [ "foo/bar" ] + } + + account |> getResourceAtIndex client.SerializationSettings 1 + + Expect.equal resource.Name "storage/default" "policy name for is wrong" + Expect.hasLength resource.Policy.Rules 2 "Should be two rules" + + let rule = resource.Policy.Rules.[0] + Expect.equal rule.Name "cleanup" "rule name is wrong" + + Expect.equal + rule.Definition.Actions.BaseBlob.Delete.DaysAfterModificationGreaterThan + (Nullable 7.) + "Incorrect policy action" + + Expect.isEmpty rule.Definition.Filters.PrefixMatch "should be no filters" + + let rule = resource.Policy.Rules.[1] + + Expect.equal + rule.Definition.Actions.BaseBlob.Delete.DaysAfterModificationGreaterThan + (Nullable 1.) + "should ignore duplicate actions" + + Expect.equal + rule.Definition.Actions.BaseBlob.TierToArchive.DaysAfterModificationGreaterThan + (Nullable 2.) + "should add multiple actions to a rule" + + Expect.equal (rule.Definition.Filters.PrefixMatch |> Seq.toList) [ "foo/bar" ] "incorrect filter" + } + test "Creates connection strings correctly" { + let strongConn = + StorageAccount.getConnectionString (StorageAccountName.Create("account").OkValue) + + let rgConn = + StorageAccount.getConnectionString (StorageAccountName.Create("account").OkValue, "rg") + + Expect.equal + "concat('DefaultEndpointsProtocol=https;AccountName=account;AccountKey=', listKeys(resourceId('Microsoft.Storage/storageAccounts', 'account'), '2017-10-01').keys[0].value)" + strongConn.Value + "Strong connection string" + + Expect.equal + "concat('DefaultEndpointsProtocol=https;AccountName=account;AccountKey=', listKeys(resourceId('rg', 'Microsoft.Storage/storageAccounts', 'account'), '2017-10-01').keys[0].value)" + rgConn.Value + "Complex connection string" + } + test "Creates Role Assignment correctly" { + let uai = UserAssignedIdentity.createUserAssignedIdentity "user" + + let builder = + storageAccount { + name "foo" + grant_access uai Roles.StorageBlobDataOwner + } + :> IBuilder + + let roleAssignment = + builder.BuildResources Location.NorthEurope |> List.last :?> Farmer.Arm.RoleAssignment.RoleAssignment - resource.Validate() - Expect.equal resource.Sku.Name "Premium_LRS" "SKU is wrong" - Expect.isTrue resource.IsHnsEnabled.Value "Hierarchical namespace not enabled" - } - test "When data lake can be disabled" { - let resource = - let account = - storageAccount { - name "mystorage123" - enable_data_lake false - } + Expect.equal roleAssignment.PrincipalId uai.PrincipalId "PrincipalId" + Expect.equal roleAssignment.RoleDefinitionId Roles.StorageBlobDataOwner "RoleId" + Expect.equal roleAssignment.Name.Value "105eb550-eb9f-56b6-955d-1def9d3139ec" "Storage Account Name" + Expect.equal roleAssignment.Scope Farmer.Arm.RoleAssignment.AssignmentScope.ResourceGroup "Scope" + + Expect.sequenceEqual + roleAssignment.Dependencies + [ uai.ResourceId; builder.ResourceId ] + "Role Assignment Dependencies" + + let storage = + builder.BuildResources Location.NorthEurope |> List.head :?> Farmer.Arm.Storage.StorageAccount - arm { add_resource account } |> getStorageResource + Expect.sequenceEqual storage.Dependencies [ uai.ResourceId ] "Storage Dependencies" + } + test "WebsitePrimaryEndpoint creation" { + let builder = storageAccount { name "foo" } - resource.Validate() - Expect.isFalse resource.IsHnsEnabled.Value "Hierarchical namespace should be false" + Expect.equal + builder.WebsitePrimaryEndpoint.Value + "reference(resourceId('Microsoft.Storage/storageAccounts', 'foo'), '2022-05-01').primaryEndpoints.web" + "Zone names are not fixed and should be related to a storage account name" + } + test "Creates different SKU kinds correctly" { + let account = storageAccount { + name "storage" + sku (Blobs(BlobReplication.LRS, Some DefaultAccessTier.Hot)) } - test "Creates containers correctly" { - let resources: BlobContainer list = - let account = - storageAccount { - name "storage" - add_blob_container "blob" - add_private_container "private" - add_public_container "public" - } - [ - for i in 1..3 do - account |> getResourceAtIndex client.SerializationSettings i - ] + let resource = arm { add_resource account } |> getStorageResource + Expect.equal resource.Kind "BlobStorage" "Kind" + Expect.equal resource.AccessTier (Nullable AccessTier.Hot) "Access Tier" + Expect.equal resource.Sku.Name "Standard_LRS" "Sku Name" - Expect.equal resources.[0].Name "storage/default/blob" "blob name is wrong" - Expect.equal resources.[0].PublicAccess.Value PublicAccess.Blob "blob access is wrong" - Expect.equal resources.[1].Name "storage/default/private" "private name is wrong" - Expect.equal resources.[1].PublicAccess.Value PublicAccess.None "private access is wrong" - Expect.equal resources.[2].Name "storage/default/public" "public name is wrong" - Expect.equal resources.[2].PublicAccess.Value PublicAccess.Container "container access is wrong" + let account = storageAccount { + name "storage" + sku (Files BasicReplication.ZRS) } - test "Creates file shares correctly" { - let resources: FileShare list = - let account = - storageAccount { - name "storage" - add_file_share "share1" - add_file_share_with_quota "share2" 1024 - } - [ - for i in 1..2 do - account |> getResourceAtIndex client.SerializationSettings i - ] + let resource = arm { add_resource account } |> getStorageResource + Expect.equal resource.Kind "FileStorage" "Kind" + Expect.equal resource.Sku.Name "Premium_ZRS" "Sku Name" - Expect.equal resources.[0].Name "storage/default/share1" "file share name for 'share1' is wrong" - Expect.equal resources.[1].Name "storage/default/share2" "file share name for 'share2' is wrong" - Expect.equal resources.[1].ShareQuota (Nullable 1024) "file share quota for 'share2' is wrong" + let account = storageAccount { + name "storage" + sku (BlockBlobs BasicReplication.LRS) } - test "Creates tables correctly" { - let resources: Table list = - let account = - storageAccount { - name "storage" - add_table "table1" - add_tables [ "table2"; "table3" ] - } - [ - for i in 1..3 do - account |> getResourceAtIndex client.SerializationSettings i - ] + let resource = arm { add_resource account } |> getStorageResource + Expect.equal resource.Kind "BlockBlobStorage" "Kind" + Expect.equal resource.Sku.Name "Premium_LRS" "Sku Name" - Expect.equal resources.[0].Name "storage/default/table1" "table name for 'table1' is wrong" - Expect.equal resources.[1].Name "storage/default/table2" "table name for 'table2' is wrong" - Expect.equal resources.[2].Name "storage/default/table3" "table name for 'table3' is wrong" + let account = storageAccount { + name "storage" + sku (GeneralPurpose(V1 V1Replication.RAGRS)) } - test "Creates queues correctly" { - let resources: StorageQueue list = - let account = - storageAccount { - name "storage" - add_queue "queue1" - add_queues [ "queue2"; "queue3" ] - } - [ - for i in 1..3 do - account |> getResourceAtIndex client.SerializationSettings i - ] + let resource = arm { add_resource account } |> getStorageResource + Expect.equal resource.Kind "Storage" "Kind" + Expect.equal resource.Sku.Name "Standard_RAGRS" "Sku Name" - Expect.equal resources.[0].Name "storage/default/queue1" "queue name for 'queue1' is wrong" - Expect.equal resources.[1].Name "storage/default/queue2" "queue name for 'queue2' is wrong" - Expect.equal resources.[2].Name "storage/default/queue3" "queue name for 'queue3' is wrong" - } - test "Rejects invalid storage accounts" { - let check (v: string) m = - Expect.equal (StorageAccountName.Create v) (Error("Storage account names " + m)) - - check "" "cannot be empty" "Name too short" - check "zz" "min length is 3, but here is 2. The invalid value is 'zz'" "Name too short" - - check - "abcdefghij1234567890abcde" - "max length is 24, but here is 25. The invalid value is 'abcdefghij1234567890abcde'" - "Name too long" - - check - "zzzT" - "can only contain lowercase letters. The invalid value is 'zzzT'" - "Upper case character allowed" - - check - "zzz!" - "can only contain alphanumeric characters. The invalid value is 'zzz!'" - "Non alpha numeric character allowed" - - Expect.equal - (StorageResourceName.Create("abcdefghij1234567890abcd").OkValue.ResourceName) - (ResourceName "abcdefghij1234567890abcd") - "Should have created a valid storage account name" + let account = storageAccount { + name "storage" + sku (GeneralPurpose(V2(V2Replication.LRS Premium, Some Cool))) } - test "Rejects invalid storage resource names" { - let check (v: string) m = - Expect.equal (StorageResourceName.Create v) (Error("Storage resource names " + m)) - - check "" "cannot be empty" "Name too short" - check "zz" "min length is 3, but here is 2. The invalid value is 'zz'" "Name too short" - let longName = Array.init 64 (fun _ -> 'a') |> String - check longName $"max length is 63, but here is 64. The invalid value is '{longName}'" "Name too long" - - check - "zzzT" - "can only contain lowercase letters. The invalid value is 'zzzT'" - "Upper case character allowed" - - check - "zz!z" - "can only contain alphanumeric characters or the dash (-). The invalid value is 'zz!z'" - "Bad character allowed" - - check "zzz--z" "do not allow consecutive dashes. The invalid value is 'zzz--z'" "Double dash allowed" - check "-zz" "must start with an alphanumeric character. The invalid value is '-zz'" "Start with dash" - check "zz-" "must end with an alphanumeric character. The invalid value is 'zz-'" "End with dash" - - Expect.equal - (StorageResourceName.Create("abcdefghij1234567890abcd").OkValue.ResourceName) - (ResourceName "abcdefghij1234567890abcd") - "Should have created a valid storage resource name" - } - test "Adds lifecycle policies correctly" { - let resource: ManagementPolicy = - let account = - storageAccount { - name "storage" - add_lifecycle_rule "cleanup" [ Storage.DeleteAfter 7 ] Storage.NoRuleFilters - - add_lifecycle_rule - "test" - [ - Storage.DeleteAfter 1 - Storage.DeleteAfter 2 - Storage.ArchiveAfter 2 - ] - [ "foo/bar" ] - } - account |> getResourceAtIndex client.SerializationSettings 1 - - Expect.equal resource.Name "storage/default" "policy name for is wrong" - Expect.hasLength resource.Policy.Rules 2 "Should be two rules" - - let rule = resource.Policy.Rules.[0] - Expect.equal rule.Name "cleanup" "rule name is wrong" - - Expect.equal - rule.Definition.Actions.BaseBlob.Delete.DaysAfterModificationGreaterThan - (Nullable 7.) - "Incorrect policy action" - - Expect.isEmpty rule.Definition.Filters.PrefixMatch "should be no filters" - - let rule = resource.Policy.Rules.[1] - - Expect.equal - rule.Definition.Actions.BaseBlob.Delete.DaysAfterModificationGreaterThan - (Nullable 1.) - "should ignore duplicate actions" - - Expect.equal - rule.Definition.Actions.BaseBlob.TierToArchive.DaysAfterModificationGreaterThan - (Nullable 2.) - "should add multiple actions to a rule" - - Expect.equal (rule.Definition.Filters.PrefixMatch |> Seq.toList) [ "foo/bar" ] "incorrect filter" + let resource = arm { add_resource account } |> getStorageResource + Expect.equal resource.Kind "StorageV2" "Kind" + Expect.equal resource.Sku.Name "Premium_LRS" "Sku Name" + } + test "Sets blob access tier correctly different SKU kinds correctly" { + let account = storageAccount { + name "storage" + default_blob_access_tier DefaultAccessTier.Cool } - test "Creates connection strings correctly" { - let strongConn = - StorageAccount.getConnectionString (StorageAccountName.Create("account").OkValue) - - let rgConn = - StorageAccount.getConnectionString (StorageAccountName.Create("account").OkValue, "rg") - - Expect.equal - "concat('DefaultEndpointsProtocol=https;AccountName=account;AccountKey=', listKeys(resourceId('Microsoft.Storage/storageAccounts', 'account'), '2017-10-01').keys[0].value)" - strongConn.Value - "Strong connection string" - - Expect.equal - "concat('DefaultEndpointsProtocol=https;AccountName=account;AccountKey=', listKeys(resourceId('rg', 'Microsoft.Storage/storageAccounts', 'account'), '2017-10-01').keys[0].value)" - rgConn.Value - "Complex connection string" - } - test "Creates Role Assignment correctly" { - let uai = UserAssignedIdentity.createUserAssignedIdentity "user" - let builder = - storageAccount { - name "foo" - grant_access uai Roles.StorageBlobDataOwner - } - :> IBuilder - - let roleAssignment = - builder.BuildResources Location.NorthEurope |> List.last - :?> Farmer.Arm.RoleAssignment.RoleAssignment - - Expect.equal roleAssignment.PrincipalId uai.PrincipalId "PrincipalId" - Expect.equal roleAssignment.RoleDefinitionId Roles.StorageBlobDataOwner "RoleId" - Expect.equal roleAssignment.Name.Value "105eb550-eb9f-56b6-955d-1def9d3139ec" "Storage Account Name" - Expect.equal roleAssignment.Scope Farmer.Arm.RoleAssignment.AssignmentScope.ResourceGroup "Scope" - - Expect.sequenceEqual - roleAssignment.Dependencies - [ uai.ResourceId; builder.ResourceId ] - "Role Assignment Dependencies" - - let storage = - builder.BuildResources Location.NorthEurope |> List.head :?> Farmer.Arm.Storage.StorageAccount + let resource = arm { add_resource account } |> getStorageResource + Expect.equal resource.AccessTier (Nullable AccessTier.Cool) "Access Tier" - Expect.sequenceEqual storage.Dependencies [ uai.ResourceId ] "Storage Dependencies" + let account = storageAccount { + name "storage" + default_blob_access_tier Hot } - test "WebsitePrimaryEndpoint creation" { - let builder = storageAccount { name "foo" } - - Expect.equal - builder.WebsitePrimaryEndpoint.Value - "reference(resourceId('Microsoft.Storage/storageAccounts', 'foo'), '2022-05-01').primaryEndpoints.web" - "Zone names are not fixed and should be related to a storage account name" - } - test "Creates different SKU kinds correctly" { - let account = - storageAccount { - name "storage" - sku (Blobs(BlobReplication.LRS, Some DefaultAccessTier.Hot)) - } - - let resource = arm { add_resource account } |> getStorageResource - Expect.equal resource.Kind "BlobStorage" "Kind" - Expect.equal resource.AccessTier (Nullable AccessTier.Hot) "Access Tier" - Expect.equal resource.Sku.Name "Standard_LRS" "Sku Name" - - let account = - storageAccount { - name "storage" - sku (Files BasicReplication.ZRS) - } - - let resource = arm { add_resource account } |> getStorageResource - Expect.equal resource.Kind "FileStorage" "Kind" - Expect.equal resource.Sku.Name "Premium_ZRS" "Sku Name" - - let account = - storageAccount { - name "storage" - sku (BlockBlobs BasicReplication.LRS) - } - - let resource = arm { add_resource account } |> getStorageResource - Expect.equal resource.Kind "BlockBlobStorage" "Kind" - Expect.equal resource.Sku.Name "Premium_LRS" "Sku Name" - - let account = - storageAccount { - name "storage" - sku (GeneralPurpose(V1 V1Replication.RAGRS)) - } - - let resource = arm { add_resource account } |> getStorageResource - Expect.equal resource.Kind "Storage" "Kind" - Expect.equal resource.Sku.Name "Standard_RAGRS" "Sku Name" - - let account = - storageAccount { - name "storage" - sku (GeneralPurpose(V2(V2Replication.LRS Premium, Some Cool))) - } - - let resource = arm { add_resource account } |> getStorageResource - Expect.equal resource.Kind "StorageV2" "Kind" - Expect.equal resource.Sku.Name "Premium_LRS" "Sku Name" - } - test "Sets blob access tier correctly different SKU kinds correctly" { - let account = - storageAccount { - name "storage" - default_blob_access_tier DefaultAccessTier.Cool - } - - let resource = arm { add_resource account } |> getStorageResource - Expect.equal resource.AccessTier (Nullable AccessTier.Cool) "Access Tier" - - let account = - storageAccount { - name "storage" - default_blob_access_tier Hot - } - - let resource = arm { add_resource account } |> getStorageResource - Expect.equal resource.AccessTier (Nullable AccessTier.Hot) "Access Tier" - - let account = - storageAccount { - name "storage" - sku (GeneralPurpose(V2(V2Replication.LRS Premium, None))) - } - - let resource = arm { add_resource account } |> getStorageResource - Expect.equal resource.AccessTier (Nullable()) "Access Tier" - let account = - storageAccount { - name "storage" - sku (Blobs(BlobReplication.LRS, Some Hot)) - } + let resource = arm { add_resource account } |> getStorageResource + Expect.equal resource.AccessTier (Nullable AccessTier.Hot) "Access Tier" - let resource = arm { add_resource account } |> getStorageResource - Expect.equal resource.AccessTier (Nullable AccessTier.Hot) "Access Tier" - } - test "Setting default access tier with incompatible sku throws an exception" { - Expect.throws - (fun _ -> - storageAccount { - name "storage" - sku (BlockBlobs BasicReplication.LRS) - default_blob_access_tier Cool - } - |> ignore) - "Can't set default tier for Block Blobs" + let account = storageAccount { + name "storage" + sku (GeneralPurpose(V2(V2Replication.LRS Premium, None))) } - test "Restrict by IP" { - let storage = - storageAccount { - name "onlymyhouse24125" - restrict_to_ip "8.8.8.8" - restrict_to_prefix "8.8.8.0/24" - } - let generated = arm { add_resource storage } |> getStorageResource - Expect.hasLength generated.NetworkRuleSet.IpRules 2 "Wrong number of IP rules" + let resource = arm { add_resource account } |> getStorageResource + Expect.equal resource.AccessTier (Nullable()) "Access Tier" - Expect.containsAll - (generated.NetworkRuleSet.IpRules |> Seq.map (fun rule -> rule.IPAddressOrRange)) - [ "8.8.8.8"; "8.8.8.0/24" ] - "Missing IP rules" + let account = storageAccount { + name "storage" + sku (Blobs(BlobReplication.LRS, Some Hot)) } - test "Restrict to vnet" { - let vnetName = "my-vnet" - let servicesSubnet = "services" - let containerSubnet = "containers" - - let storage = - storageAccount { - name "onlymynet" - restrict_to_subnet vnetName servicesSubnet - restrict_to_subnet vnetName containerSubnet - } - let generatedStorage = arm { add_resource storage } |> getStorageResource - Expect.hasLength generatedStorage.NetworkRuleSet.VirtualNetworkRules 2 "Wrong number of vnet rules" - - let allowedSubnets = - [ - (Arm.Network.subnets.resourceId (ResourceName vnetName, ResourceName servicesSubnet)) - .ArmExpression.Eval() - (Arm.Network.subnets.resourceId (ResourceName vnetName, ResourceName containerSubnet)) - .ArmExpression.Eval() - ] - - Expect.containsAll - allowedSubnets - (generatedStorage.NetworkRuleSet.VirtualNetworkRules - |> Seq.map (fun rule -> rule.VirtualNetworkResourceId)) - "Missing subnet rules" - } - test "Sets CORS correctly" { - let account = + let resource = arm { add_resource account } |> getStorageResource + Expect.equal resource.AccessTier (Nullable AccessTier.Hot) "Access Tier" + } + test "Setting default access tier with incompatible sku throws an exception" { + Expect.throws + (fun _ -> storageAccount { name "storage" - - add_cors_rules - [ - StorageService.Blobs, CorsRule.AllowAll - StorageService.Blobs, - { CorsRule.AllowAll with - AllowedOrigins = Specific [ Uri "https://compositional-it.com" ] - } - StorageService.Queues, - CorsRule.create ( - [ "https://compositional-it.com" ], - [ GET ], - 15, - [ "exposed1"; "exposed2" ], - [ "ALLOWED1"; "ALLOWED2" ] - ) - ] + sku (BlockBlobs BasicReplication.LRS) + default_blob_access_tier Cool } - - let rules = - (account |> findPropertiesResource "blobServices").properties.cors.corsRules - - Expect.equal rules.Length 2 "Incorrect number of CORS rules" - - let blobAllowAllRule = rules.[0] - Expect.equal [| "*" |] blobAllowAllRule.allowedHeaders "Incorrect default headers" - - Expect.equal - (HttpMethod.All.Value |> List.map string |> List.toArray) - blobAllowAllRule.allowedMethods - "Incorrect default methods" - - Expect.equal [| "*" |] blobAllowAllRule.allowedOrigins "Incorrect default origin" - Expect.equal [| "*" |] blobAllowAllRule.exposedHeaders "Incorrect default exposed headers" - Expect.equal 0 blobAllowAllRule.maxAgeInSeconds "Incorrect default max age is seconds" - - let blobSpecificRule = rules.[1] - - Expect.isTrue (not <| blobSpecificRule.allowedOrigins.[0].EndsWith('/')) "Should not add trailing slash" - - Expect.equal - blobSpecificRule.allowedOrigins - [| "https://compositional-it.com" |] - "Incorrect custom allowed origin" - - let queueRule = - (account |> findPropertiesResource "queueServices").properties.cors.corsRules - |> Seq.exactlyOne - - Expect.equal [| "ALLOWED1"; "ALLOWED2" |] queueRule.allowedHeaders "Incorrect factory headers" - Expect.equal [| string GET |] queueRule.allowedMethods "Incorrect factory methods" - Expect.equal queueRule.allowedOrigins [| "https://compositional-it.com" |] "Incorrect factory origin" - Expect.equal [| "exposed1"; "exposed2" |] queueRule.exposedHeaders "Incorrect factory exposed headers" - Expect.equal 15 queueRule.maxAgeInSeconds "Incorrect factory max age is seconds" - } - - test "Policies" { - let account = - storageAccount { - name "storage" - - add_policies - [ - StorageService.Blobs, - [ - Policy.Restore { Enabled = true; Days = 5 } - Policy.DeleteRetention { Enabled = true; Days = 10 } - Policy.LastAccessTimeTracking - { - Enabled = true - TrackingGranularityInDays = 12 - } - Policy.ContainerDeleteRetention { Enabled = true; Days = 11 } - Policy.ChangeFeed { Enabled = true; RetentionInDays = 30 } - ] - ] + |> ignore) + "Can't set default tier for Block Blobs" + } + test "Restrict by IP" { + let storage = storageAccount { + name "onlymyhouse24125" + restrict_to_ip "8.8.8.8" + restrict_to_prefix "8.8.8.0/24" + } + + let generated = arm { add_resource storage } |> getStorageResource + Expect.hasLength generated.NetworkRuleSet.IpRules 2 "Wrong number of IP rules" + + Expect.containsAll + (generated.NetworkRuleSet.IpRules |> Seq.map (fun rule -> rule.IPAddressOrRange)) + [ "8.8.8.8"; "8.8.8.0/24" ] + "Missing IP rules" + } + test "Restrict to vnet" { + let vnetName = "my-vnet" + let servicesSubnet = "services" + let containerSubnet = "containers" + + let storage = storageAccount { + name "onlymynet" + restrict_to_subnet vnetName servicesSubnet + restrict_to_subnet vnetName containerSubnet + } + + let generatedStorage = arm { add_resource storage } |> getStorageResource + Expect.hasLength generatedStorage.NetworkRuleSet.VirtualNetworkRules 2 "Wrong number of vnet rules" + + let allowedSubnets = [ + (Arm.Network.subnets.resourceId (ResourceName vnetName, ResourceName servicesSubnet)) + .ArmExpression.Eval() + (Arm.Network.subnets.resourceId (ResourceName vnetName, ResourceName containerSubnet)) + .ArmExpression.Eval() + ] + + Expect.containsAll + allowedSubnets + (generatedStorage.NetworkRuleSet.VirtualNetworkRules + |> Seq.map (fun rule -> rule.VirtualNetworkResourceId)) + "Missing subnet rules" + } + test "Sets CORS correctly" { + let account = storageAccount { + name "storage" + + add_cors_rules [ + StorageService.Blobs, CorsRule.AllowAll + StorageService.Blobs, + { + CorsRule.AllowAll with + AllowedOrigins = Specific [ Uri "https://compositional-it.com" ] } - - let properties = (account |> findPropertiesResource "blobServices").properties - let restore = properties.restorePolicy - - Expect.isTrue restore.enabled "" - Expect.equal restore.days 5 "" - - let deleteRetention = properties.deleteRetentionPolicy - - Expect.isTrue deleteRetention.enabled "" - Expect.equal deleteRetention.days 10 "" - - let changeFeed = properties.changeFeed - - Expect.isTrue changeFeed.enabled "" - Expect.equal changeFeed.retentionInDays 30 "" - - let lastAccessTimeTrackingPolicy = properties.lastAccessTimeTrackingPolicy - - Expect.isTrue lastAccessTimeTrackingPolicy.enable "" - Expect.equal lastAccessTimeTrackingPolicy.trackingGranularityInDays 12 "" - Expect.equal lastAccessTimeTrackingPolicy.name "AccessTimeTracking" "" - Expect.equal lastAccessTimeTrackingPolicy.blobType [| "blockBlob" |] "" - - let containerDeleteRetentionPolicy = properties.containerDeleteRetentionPolicy - - Expect.isTrue containerDeleteRetentionPolicy.enabled "" - Expect.equal containerDeleteRetentionPolicy.days 11 "" + StorageService.Queues, + CorsRule.create ( + [ "https://compositional-it.com" ], + [ GET ], + 15, + [ "exposed1"; "exposed2" ], + [ "ALLOWED1"; "ALLOWED2" ] + ) + ] } - test "Versioning" { - let account = - storageAccount { - name "storage" - enable_versioning [ StorageService.Blobs, true ] - } + let rules = + (account |> findPropertiesResource "blobServices").properties.cors.corsRules - let properties = (account |> findPropertiesResource "blobServices").properties + Expect.equal rules.Length 2 "Incorrect number of CORS rules" - Expect.isTrue properties.IsVersioningEnabled "" - } + let blobAllowAllRule = rules.[0] + Expect.equal [| "*" |] blobAllowAllRule.allowedHeaders "Incorrect default headers" - test "Sets Min TLS version correctly" { - let resource = - let account = - storageAccount { - name "mystorage123" - min_tls_version Tls12 - } + Expect.equal + (HttpMethod.All.Value |> List.map string |> List.toArray) + blobAllowAllRule.allowedMethods + "Incorrect default methods" - arm { add_resource account } |> getStorageResource + Expect.equal [| "*" |] blobAllowAllRule.allowedOrigins "Incorrect default origin" + Expect.equal [| "*" |] blobAllowAllRule.exposedHeaders "Incorrect default exposed headers" + Expect.equal 0 blobAllowAllRule.maxAgeInSeconds "Incorrect default max age is seconds" - Expect.equal resource.MinimumTlsVersion "TLS1_2" "Min TLS version is wrong" - } + let blobSpecificRule = rules.[1] - test "dnsEndpointType can be set to AzureDnsZone" { - let resource = - let account = - storageAccount { - name "mystorage123" - use_azure_dns_zone - } + Expect.isTrue (not <| blobSpecificRule.allowedOrigins.[0].EndsWith('/')) "Should not add trailing slash" - arm { add_resource account } + Expect.equal + blobSpecificRule.allowedOrigins + [| "https://compositional-it.com" |] + "Incorrect custom allowed origin" - let jsn = resource.Template |> Writer.toJson - let jobj = jsn |> Newtonsoft.Json.Linq.JObject.Parse + let queueRule = + (account |> findPropertiesResource "queueServices").properties.cors.corsRules + |> Seq.exactlyOne - Expect.equal - (jobj.SelectToken("resources[0].properties.dnsEndpointType").ToString()) - "AzureDnsZone" - "dnsEndpointType should AzureDnsZone" - } + Expect.equal [| "ALLOWED1"; "ALLOWED2" |] queueRule.allowedHeaders "Incorrect factory headers" + Expect.equal [| string GET |] queueRule.allowedMethods "Incorrect factory methods" + Expect.equal queueRule.allowedOrigins [| "https://compositional-it.com" |] "Incorrect factory origin" + Expect.equal [| "exposed1"; "exposed2" |] queueRule.exposedHeaders "Incorrect factory exposed headers" + Expect.equal 15 queueRule.maxAgeInSeconds "Incorrect factory max age is seconds" + } - test "Must set a storage account name" { - Expect.throws - (fun () -> storageAccount { sku Sku.Standard_ZRS } |> ignore) - "Must set a name on a storage account" - } + test "Policies" { + let account = storageAccount { + name "storage" - test "Public network access can be disabled" { - let resource = - let account = - storageAccount { - name "mystorage123" - disable_public_network_access + add_policies [ + StorageService.Blobs, + [ + Policy.Restore { Enabled = true; Days = 5 } + Policy.DeleteRetention { Enabled = true; Days = 10 } + Policy.LastAccessTimeTracking { + Enabled = true + TrackingGranularityInDays = 12 } - - arm { add_resource account } - - let jsn = resource.Template |> Writer.toJson - let jobj = jsn |> Newtonsoft.Json.Linq.JObject.Parse - - Expect.equal - (jobj.SelectToken("resources[0].properties.publicNetworkAccess").ToString()) - "Disabled" - "public network access should be disabled" - - Expect.equal - (jobj.SelectToken("resources[0].properties.networkAcls.defaultAction").ToString()) - "Deny" - "network acl should deny traffic when disabling public network access" - - Expect.equal - (jobj.SelectToken("resources[0].properties.networkAcls.bypass").ToString()) - "None" - "network acl should not allow bypass by default" + Policy.ContainerDeleteRetention { Enabled = true; Days = 11 } + Policy.ChangeFeed { Enabled = true; RetentionInDays = 30 } + ] + ] } - test "restrict_to_azure_services adds correct network acl" { - let resource = - let account = - storageAccount { - name "mystorage123" - restrict_to_azure_services [ Farmer.Arm.Storage.NetworkRuleSetBypass.AzureServices ] - restrict_to_azure_services [ Farmer.Arm.Storage.NetworkRuleSetBypass.Metrics ] - } + let properties = (account |> findPropertiesResource "blobServices").properties + let restore = properties.restorePolicy - arm { add_resource account } + Expect.isTrue restore.enabled "" + Expect.equal restore.days 5 "" - let jsn = resource.Template |> Writer.toJson - let jobj = jsn |> Newtonsoft.Json.Linq.JObject.Parse + let deleteRetention = properties.deleteRetentionPolicy - Expect.equal - (jobj.SelectToken("resources[0].properties.networkAcls.defaultAction").ToString()) - "Deny" - "network acl should deny traffic when restricting to azure services + private link" + Expect.isTrue deleteRetention.enabled "" + Expect.equal deleteRetention.days 10 "" - Expect.equal - (jobj.SelectToken("resources[0].properties.networkAcls.bypass").ToString()) - "AzureServices,Metrics" - "network acl should allow bypass for selected services" + let changeFeed = properties.changeFeed - Expect.isEmpty - (jobj.SelectToken("resources[0].properties.networkAcls.ipRules").Values()) - "network acl should not define ip restrictions" + Expect.isTrue changeFeed.enabled "" + Expect.equal changeFeed.retentionInDays 30 "" - Expect.isEmpty - (jobj - .SelectToken("resources[0].properties.networkAcls.virtualNetworkRules") - .Values()) - "network acl should not define vnet restrictions" + let lastAccessTimeTrackingPolicy = properties.lastAccessTimeTrackingPolicy - Expect.equal - (jobj.SelectToken("resources[0].properties.publicNetworkAccess").ToString()) - "Enabled" - "public network access should be disabled" - } + Expect.isTrue lastAccessTimeTrackingPolicy.enable "" + Expect.equal lastAccessTimeTrackingPolicy.trackingGranularityInDays 12 "" + Expect.equal lastAccessTimeTrackingPolicy.name "AccessTimeTracking" "" + Expect.equal lastAccessTimeTrackingPolicy.blobType [| "blockBlob" |] "" - test "Blob public access can be disabled" { - let resource = - let account = - storageAccount { - name "mystorage123" - disable_blob_public_access - } + let containerDeleteRetentionPolicy = properties.containerDeleteRetentionPolicy - arm { add_resource account } + Expect.isTrue containerDeleteRetentionPolicy.enabled "" + Expect.equal containerDeleteRetentionPolicy.days 11 "" + } - let jsn = resource.Template |> Writer.toJson - let jobj = jsn |> Newtonsoft.Json.Linq.JObject.Parse - - Expect.equal - (jobj.SelectToken("resources[0].properties.allowBlobPublicAccess").ToString()) - "false" - "blob public access should be disabled" + test "Versioning" { + let account = storageAccount { + name "storage" + enable_versioning [ StorageService.Blobs, true ] } - test "Blob public access can be toggled" { - let resource = - let account = - storageAccount { - name "mystorage123" - disable_blob_public_access - disable_blob_public_access FeatureFlag.Disabled - } + let properties = (account |> findPropertiesResource "blobServices").properties - arm { add_resource account } + Expect.isTrue properties.IsVersioningEnabled "" + } - let jsn = resource.Template |> Writer.toJson - let jobj = jsn |> Newtonsoft.Json.Linq.JObject.Parse + test "Sets Min TLS version correctly" { + let resource = + let account = storageAccount { + name "mystorage123" + min_tls_version Tls12 + } - Expect.equal - (jobj.SelectToken("resources[0].properties.allowBlobPublicAccess").ToString()) - "true" - "blob public access should be enabled" - } + arm { add_resource account } |> getStorageResource - test "Shared key access can be disabled" { - let resource = - let account = - storageAccount { - name "mystorage123" - disable_shared_key_access - } + Expect.equal resource.MinimumTlsVersion "TLS1_2" "Min TLS version is wrong" + } - arm { add_resource account } + test "dnsEndpointType can be set to AzureDnsZone" { + let resource = + let account = storageAccount { + name "mystorage123" + use_azure_dns_zone + } - let jsn = resource.Template |> Writer.toJson - let jobj = jsn |> Newtonsoft.Json.Linq.JObject.Parse + arm { add_resource account } - Expect.equal - (jobj.SelectToken("resources[0].properties.allowSharedKeyAccess").ToString()) - "false" - "shared key access should be disabled" - } + let jsn = resource.Template |> Writer.toJson + let jobj = jsn |> Newtonsoft.Json.Linq.JObject.Parse - test "Shared key access can be toggled" { - let resource = - let account = - storageAccount { - name "mystorage123" - disable_shared_key_access - disable_shared_key_access FeatureFlag.Disabled - } + Expect.equal + (jobj.SelectToken("resources[0].properties.dnsEndpointType").ToString()) + "AzureDnsZone" + "dnsEndpointType should AzureDnsZone" + } - arm { add_resource account } + test "Must set a storage account name" { + Expect.throws + (fun () -> storageAccount { sku Sku.Standard_ZRS } |> ignore) + "Must set a name on a storage account" + } - let jsn = resource.Template |> Writer.toJson - let jobj = jsn |> Newtonsoft.Json.Linq.JObject.Parse + test "Public network access can be disabled" { + let resource = + let account = storageAccount { + name "mystorage123" + disable_public_network_access + } - Expect.equal - (jobj.SelectToken("resources[0].properties.allowSharedKeyAccess").ToString()) - "true" - "shared key access should be enabled" - } + arm { add_resource account } + + let jsn = resource.Template |> Writer.toJson + let jobj = jsn |> Newtonsoft.Json.Linq.JObject.Parse - test "Default to OAuth can be disabled" { - let resource = - let account = - storageAccount { - name "mystorage123" - default_to_oauth_authentication - } - - arm { add_resource account } + Expect.equal + (jobj.SelectToken("resources[0].properties.publicNetworkAccess").ToString()) + "Disabled" + "public network access should be disabled" - let jsn = resource.Template |> Writer.toJson - let jobj = jsn |> Newtonsoft.Json.Linq.JObject.Parse + Expect.equal + (jobj.SelectToken("resources[0].properties.networkAcls.defaultAction").ToString()) + "Deny" + "network acl should deny traffic when disabling public network access" - Expect.equal - (jobj - .SelectToken("resources[0].properties.defaultToOAuthAuthentication") - .ToString()) - "true" - "default to OAuth should be enabled" - } - - test "Default to OAuth can be toggled" { - let resource = - let account = - storageAccount { - name "mystorage123" - default_to_oauth_authentication - default_to_oauth_authentication FeatureFlag.Disabled - } - - arm { add_resource account } - - let jsn = resource.Template |> Writer.toJson - let jobj = jsn |> Newtonsoft.Json.Linq.JObject.Parse - - Expect.equal - (jobj - .SelectToken("resources[0].properties.defaultToOAuthAuthentication") - .ToString()) - "false" - "default to OAuth should be disabled" - } - ] + Expect.equal + (jobj.SelectToken("resources[0].properties.networkAcls.bypass").ToString()) + "None" + "network acl should not allow bypass by default" + } + + test "restrict_to_azure_services adds correct network acl" { + let resource = + let account = storageAccount { + name "mystorage123" + restrict_to_azure_services [ Farmer.Arm.Storage.NetworkRuleSetBypass.AzureServices ] + restrict_to_azure_services [ Farmer.Arm.Storage.NetworkRuleSetBypass.Metrics ] + } + + arm { add_resource account } + + let jsn = resource.Template |> Writer.toJson + let jobj = jsn |> Newtonsoft.Json.Linq.JObject.Parse + + Expect.equal + (jobj.SelectToken("resources[0].properties.networkAcls.defaultAction").ToString()) + "Deny" + "network acl should deny traffic when restricting to azure services + private link" + + Expect.equal + (jobj.SelectToken("resources[0].properties.networkAcls.bypass").ToString()) + "AzureServices,Metrics" + "network acl should allow bypass for selected services" + + Expect.isEmpty + (jobj.SelectToken("resources[0].properties.networkAcls.ipRules").Values()) + "network acl should not define ip restrictions" + + Expect.isEmpty + (jobj + .SelectToken("resources[0].properties.networkAcls.virtualNetworkRules") + .Values()) + "network acl should not define vnet restrictions" + + Expect.equal + (jobj.SelectToken("resources[0].properties.publicNetworkAccess").ToString()) + "Enabled" + "public network access should be disabled" + } + + test "Blob public access can be disabled" { + let resource = + let account = storageAccount { + name "mystorage123" + disable_blob_public_access + } + + arm { add_resource account } + + let jsn = resource.Template |> Writer.toJson + let jobj = jsn |> Newtonsoft.Json.Linq.JObject.Parse + + Expect.equal + (jobj.SelectToken("resources[0].properties.allowBlobPublicAccess").ToString()) + "false" + "blob public access should be disabled" + } + + test "Blob public access can be toggled" { + let resource = + let account = storageAccount { + name "mystorage123" + disable_blob_public_access + disable_blob_public_access FeatureFlag.Disabled + } + + arm { add_resource account } + + let jsn = resource.Template |> Writer.toJson + let jobj = jsn |> Newtonsoft.Json.Linq.JObject.Parse + + Expect.equal + (jobj.SelectToken("resources[0].properties.allowBlobPublicAccess").ToString()) + "true" + "blob public access should be enabled" + } + + test "Shared key access can be disabled" { + let resource = + let account = storageAccount { + name "mystorage123" + disable_shared_key_access + } + + arm { add_resource account } + + let jsn = resource.Template |> Writer.toJson + let jobj = jsn |> Newtonsoft.Json.Linq.JObject.Parse + + Expect.equal + (jobj.SelectToken("resources[0].properties.allowSharedKeyAccess").ToString()) + "false" + "shared key access should be disabled" + } + + test "Shared key access can be toggled" { + let resource = + let account = storageAccount { + name "mystorage123" + disable_shared_key_access + disable_shared_key_access FeatureFlag.Disabled + } + + arm { add_resource account } + + let jsn = resource.Template |> Writer.toJson + let jobj = jsn |> Newtonsoft.Json.Linq.JObject.Parse + + Expect.equal + (jobj.SelectToken("resources[0].properties.allowSharedKeyAccess").ToString()) + "true" + "shared key access should be enabled" + } + + test "Default to OAuth can be disabled" { + let resource = + let account = storageAccount { + name "mystorage123" + default_to_oauth_authentication + } + + arm { add_resource account } + + let jsn = resource.Template |> Writer.toJson + let jobj = jsn |> Newtonsoft.Json.Linq.JObject.Parse + + Expect.equal + (jobj + .SelectToken("resources[0].properties.defaultToOAuthAuthentication") + .ToString()) + "true" + "default to OAuth should be enabled" + } + + test "Default to OAuth can be toggled" { + let resource = + let account = storageAccount { + name "mystorage123" + default_to_oauth_authentication + default_to_oauth_authentication FeatureFlag.Disabled + } + + arm { add_resource account } + + let jsn = resource.Template |> Writer.toJson + let jobj = jsn |> Newtonsoft.Json.Linq.JObject.Parse + + Expect.equal + (jobj + .SelectToken("resources[0].properties.defaultToOAuthAuthentication") + .ToString()) + "false" + "default to OAuth should be disabled" + } + ] diff --git a/src/Tests/Template.fs b/src/Tests/Template.fs index 642edf871..416bce9df 100644 --- a/src/Tests/Template.fs +++ b/src/Tests/Template.fs @@ -17,440 +17,417 @@ let dummyClient = new ResourceManagementClient(Uri "http://management.azure.com", TokenCredentials "NotNullOrWhiteSpace") let tests = - testList - "Template" - [ - test "Can create a basic template" { - let template = arm { location Location.NorthEurope } |> toTemplate - - Expect.equal - template.``$schema`` - "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#" - "" - - Expect.isEmpty template.outputs "" - Expect.isEmpty template.parameters "" - Expect.isEmpty template.resources "" - } - test "Correctly generates outputs" { - let template = - arm { - location Location.NorthEurope - output "p1" "v1" - output "p2" "v2" + testList "Template" [ + test "Can create a basic template" { + let template = arm { location Location.NorthEurope } |> toTemplate + + Expect.equal + template.``$schema`` + "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#" + "" + + Expect.isEmpty template.outputs "" + Expect.isEmpty template.parameters "" + Expect.isEmpty template.resources "" + } + test "Correctly generates outputs" { + let template = + arm { + location Location.NorthEurope + output "p1" "v1" + output "p2" "v2" + } + |> toTemplate + + Expect.equal template.outputs.["p1"].value "v1" "" + Expect.equal template.outputs.["p2"].value "v2" "" + Expect.equal template.outputs.Count 2 "" + } + test "Processes parameters correctly" { + let template = createSimpleDeployment [ "p1"; "p2" ] |> toTemplate + + Expect.equal template.parameters.["p1"].``type`` "securestring" "" + Expect.equal template.parameters.["p2"].``type`` "securestring" "" + Expect.equal template.parameters.Count 2 "" + } + + test "Can create a single resource" { + let template = arm { add_resource (storageAccount { name "test" }) } + + Expect.equal template.Template.Resources.Length 1 "Should be a single resource" + } + + test "Can create multiple resources simultaneously" { + let template = arm { add_resources [ storageAccount { name "test" }; storageAccount { name "test2" } ] } + + Expect.equal template.Template.Resources.Length 2 "Should be two resources" + } + + test "De-dupes the same resource name and type" { + let template = arm { add_resources [ storageAccount { name "test" }; storageAccount { name "test" } ] } + + Expect.equal template.Template.Resources.Length 1 "Should be a single resource" + } + + test "Does not de-dupe the same resource name but different type" { + let template = arm { add_resources [ storageAccount { name "test" }; cognitiveServices { name "test" } ] } + + Expect.equal template.Template.Resources.Length 2 "Should be two resources" + } + + test "Location is cascaded to all resources" { + let template = arm { + location Location.NorthCentralUS + add_resources [ storageAccount { name "test" }; storageAccount { name "test2" } ] + } + + let allLocations = + template.Template.Resources + |> List.map (fun r -> r.JsonModel |> convertTo<{| Location: string |}>) + + Expect.sequenceEqual + allLocations + [ + {| + Location = Location.NorthCentralUS.ArmValue + |} + {| + Location = Location.NorthCentralUS.ArmValue + |} + ] + "Incorrect Location" + } + + test "Secure parameter is correctly added" { + let template = arm { + add_resource ( + vm { + name "isaacvm" + username "foo" } - |> toTemplate - - Expect.equal template.outputs.["p1"].value "v1" "" - Expect.equal template.outputs.["p2"].value "v2" "" - Expect.equal template.outputs.Count 2 "" - } - test "Processes parameters correctly" { - let template = createSimpleDeployment [ "p1"; "p2" ] |> toTemplate - - Expect.equal template.parameters.["p1"].``type`` "securestring" "" - Expect.equal template.parameters.["p2"].``type`` "securestring" "" - Expect.equal template.parameters.Count 2 "" - } - - test "Can create a single resource" { - let template = arm { add_resource (storageAccount { name "test" }) } - - Expect.equal template.Template.Resources.Length 1 "Should be a single resource" + ) } - test "Can create multiple resources simultaneously" { - let template = - arm { add_resources [ storageAccount { name "test" }; storageAccount { name "test2" } ] } + Expect.sequenceEqual + template.Template.Parameters + [ SecureParameter "password-for-isaacvm" ] + "Missing parameter for VM." + } - Expect.equal template.Template.Resources.Length 2 "Should be two resources" + test "Outputs are correctly added" { + let template = arm { + output "foo" "bar" + output "foo" "baz" + output "bar" "bop" } - test "De-dupes the same resource name and type" { - let template = - arm { add_resources [ storageAccount { name "test" }; storageAccount { name "test" } ] } + Expect.sequenceEqual + template.Template.Outputs + [ "bar", "bop"; "foo", "baz" ] + "Outputs should work like a key/value store" + } - Expect.equal template.Template.Resources.Length 1 "Should be a single resource" - } - - test "Does not de-dupe the same resource name but different type" { - let template = - arm { add_resources [ storageAccount { name "test" }; cognitiveServices { name "test" } ] } - - Expect.equal template.Template.Resources.Length 2 "Should be two resources" - } + test "Can add a list of resources types together" { + let resources: IBuilder list = [ storageAccount { name "test" }; storageAccount { name "test2" } ] - test "Location is cascaded to all resources" { - let template = - arm { - location Location.NorthCentralUS - add_resources [ storageAccount { name "test" }; storageAccount { name "test2" } ] - } + let template = arm { add_resources resources } + Expect.hasLength template.Template.Resources 2 "Should be two resources added" + } - let allLocations = - template.Template.Resources - |> List.map (fun r -> r.JsonModel |> convertTo<{| Location: string |}>) - - Expect.sequenceEqual - allLocations - [ - {| - Location = Location.NorthCentralUS.ArmValue - |} - {| - Location = Location.NorthCentralUS.ArmValue - |} - ] - "Incorrect Location" + test "Can add a list of arm resources types together" { + let web = webApp { + name "test" + system_identity } - test "Secure parameter is correctly added" { - let template = - arm { - add_resource ( - vm { - name "isaacvm" - username "foo" - } - ) - } - - Expect.sequenceEqual - template.Template.Parameters - [ SecureParameter "password-for-isaacvm" ] - "Missing parameter for VM." - } - - test "Outputs are correctly added" { - let template = - arm { - output "foo" "bar" - output "foo" "baz" - output "bar" "bop" - } - - Expect.sequenceEqual - template.Template.Outputs - [ "bar", "bop"; "foo", "baz" ] - "Outputs should work like a key/value store" + let roleAssignment = { + Name = "r2" |> ResourceName + RoleDefinitionId = Roles.DNSZoneContributor + PrincipalId = web.SystemIdentity.PrincipalId + PrincipalType = Arm.RoleAssignment.PrincipalType.MSI + Scope = Arm.RoleAssignment.AssignmentScope.ResourceGroup + Dependencies = Set.ofList [ web.ResourceId ] } - test "Can add a list of resources types together" { - let resources: IBuilder list = - [ storageAccount { name "test" }; storageAccount { name "test2" } ] - - let template = arm { add_resources resources } - Expect.hasLength template.Template.Resources 2 "Should be two resources added" + let roleAssignment2 = { + Name = "r1" |> ResourceName + RoleDefinitionId = Roles.DNSZoneContributor + PrincipalId = web.SystemIdentity.PrincipalId + PrincipalType = Arm.RoleAssignment.PrincipalType.ServicePrincipal + Scope = Arm.RoleAssignment.AssignmentScope.ResourceGroup + Dependencies = Set.ofList [ web.ResourceId ] } - test "Can add a list of arm resources types together" { - let web = - webApp { - name "test" - system_identity - } + let resources: IArmResource list = [ roleAssignment; roleAssignment2 ] - let roleAssignment = - { - Name = "r2" |> ResourceName - RoleDefinitionId = Roles.DNSZoneContributor - PrincipalId = web.SystemIdentity.PrincipalId - PrincipalType = Arm.RoleAssignment.PrincipalType.MSI - Scope = Arm.RoleAssignment.AssignmentScope.ResourceGroup - Dependencies = Set.ofList [ web.ResourceId ] - } + let template = arm { add_arm_resources resources } - let roleAssignment2 = - { - Name = "r1" |> ResourceName - RoleDefinitionId = Roles.DNSZoneContributor - PrincipalId = web.SystemIdentity.PrincipalId - PrincipalType = Arm.RoleAssignment.PrincipalType.ServicePrincipal - Scope = Arm.RoleAssignment.AssignmentScope.ResourceGroup - Dependencies = Set.ofList [ web.ResourceId ] + Expect.hasLength template.Template.Resources 2 "Should be two resources added" + } + + test "Can add dependency through IBuilder" { + let a = storageAccount { name "aaa" } + + let b = webApp { + name "testweb" + depends_on a + } + + Expect.equal b.Dependencies (Set [ storageAccounts.resourceId "aaa" ]) "Dependency should have been set" + } + + test "Can add dependencies through IBuilder" { + let a = storageAccount { name "aaa" } :> IBuilder + let b = storageAccount { name "bbb" } :> IBuilder + + let b = webApp { + name "testweb" + depends_on [ a; b ] + } + + Expect.equal + b.Dependencies + (Set [ storageAccounts.resourceId "aaa"; storageAccounts.resourceId "bbb" ]) + "Dependencies should have been set" + } + + test "Generates untyped Resource Id" { + let rid = ResourceId.create (ResourceType.ResourceType("", ""), ResourceName "test") + let id = rid.Eval() + Expect.equal id "test" "resourceId template function should match" + } + + test "Generates typed Resource Id" { + let rid = connections.resourceId "test" + let id = rid.Eval() + + Expect.equal + id + "[resourceId('Microsoft.Network/connections', 'test')]" + "resourceId template function should match" + } + + test "Generates typed Resource Id with group" { + let rid = + ResourceId.create (Arm.Network.connections, ResourceName "test", "myGroup") + + let id = rid.Eval() + + Expect.equal + id + "[resourceId('myGroup', 'Microsoft.Network/connections', 'test')]" + "resourceId template function should match" + } + + test "Generates typed Resource Id with segments" { + let rid = + ResourceId.create ( + Arm.Network.connections, + ResourceName "test", + ResourceName "segment1", + ResourceName "segment2" + ) + + let id = rid.Eval() + + Expect.equal + id + "[resourceId('Microsoft.Network/connections', 'test', 'segment1', 'segment2')]" + "resourceId template function should match" + } + + test "Generates deployment Resource Id with template name" { + let deployment = resourceGroup { name "[resourceGroup.name()]" } + let id = deployment.ResourceId.Eval() + let expectedDeploymentIndex = ResourceGroup.deploymentIndex () - 1 + + Expect.equal + id + $"[resourceId(resourceGroup.name(), 'Microsoft.Resources/deployments', concat(resourceGroup.name(),'-deployment-{expectedDeploymentIndex}'))]" + "resourceId template function should match" + } + + test "Fails if ARM expression is already quoted" { + Expect.throws (fun () -> ArmExpression.create "[test]" |> ignore) "" + } + + test "Correctly strips a literal expression" { Expect.equal ((ArmExpression.literal "test").Eval()) "test" "" } + + test "Does not fail if ARM expression contains an inner quote" { + Expect.equal "[foo[test]]" ((ArmExpression.create "foo[test]").Eval()) "" + } + test "Does not create empty nodes for core resource fields when nothing is supplied" { + let createdResource = ResourceType("Test", "2017-01-01").Create(ResourceName "Name") + + Expect.equal + createdResource + {| + name = "Name" + ``type`` = "Test" + apiVersion = "2017-01-01" + dependsOn = null + location = null + tags = null + |} + "Default values don't match" + } + test "Can nest resource groups" { + let template = arm { + add_resource ( + resourceGroup { + name "inner" + add_resource (storageAccount { name "storage" }) + add_tag "deployment-tag" "inner-rg" } - - let resources: IArmResource list = [ roleAssignment; roleAssignment2 ] - - let template = arm { add_arm_resources resources } - - Expect.hasLength template.Template.Resources 2 "Should be two resources added" + ) } - test "Can add dependency through IBuilder" { - let a = storageAccount { name "aaa" } + Expect.hasLength template.Template.Resources 1 "Outer template should contain only nested deployment" - let b = - webApp { - name "testweb" - depends_on a - } + Expect.isTrue + (template.Template.Resources.[0] :? Arm.ResourceGroup.ResourceGroupDeployment) + "The only resource should be a resourceGroupDeployment" - Expect.equal b.Dependencies (Set [ storageAccounts.resourceId "aaa" ]) "Dependency should have been set" - } + let innerDeployment = + template.Template.Resources.[0] :?> Arm.ResourceGroup.ResourceGroupDeployment - test "Can add dependencies through IBuilder" { - let a = storageAccount { name "aaa" } :> IBuilder - let b = storageAccount { name "bbb" } :> IBuilder - - let b = - webApp { - name "testweb" - depends_on [ a; b ] - } + Expect.hasLength innerDeployment.Resources 1 "Inner template should have 1 resource" + Expect.equal innerDeployment.TargetResourceGroup.Value "inner" "Inner template name is incorrect" - Expect.equal - b.Dependencies - (Set [ storageAccounts.resourceId "aaa"; storageAccounts.resourceId "bbb" ]) - "Dependencies should have been set" + Expect.isTrue + (innerDeployment.Template.Resources.[0] :? Arm.Storage.StorageAccount) + "The only resource in the inner deployment should be a storageAccount" + } + test "Nested resource group outputs are copied to outer deployments" { + let inner1 = resourceGroup { + name "inner1" + deployment_name "inner1" + output "foo" "bax" } - test "Generates untyped Resource Id" { - let rid = ResourceId.create (ResourceType.ResourceType("", ""), ResourceName "test") - let id = rid.Eval() - Expect.equal id "test" "resourceId template function should match" + let inner2 = resourceGroup { + name "inner2" + deployment_name "inner2" + output "foo" "bay" } - test "Generates typed Resource Id" { - let rid = connections.resourceId "test" - let id = rid.Eval() - - Expect.equal - id - "[resourceId('Microsoft.Network/connections', 'test')]" - "resourceId template function should match" + let outer = arm { + add_resource inner1 + add_resource inner2 + output "foo" "baz" } - test "Generates typed Resource Id with group" { - let rid = - ResourceId.create (Arm.Network.connections, ResourceName "test", "myGroup") + Expect.hasLength outer.Template.Outputs 3 "inner outputs should copy to outer template" + Expect.equal outer.Template.Outputs.[0] ("foo", "baz") "output expression was incorrect" - let id = rid.Eval() + Expect.equal + outer.Template.Outputs.[1] + ("inner1.foo", "[reference('inner1').outputs['foo'].value]") + "output expression was incorrect" - Expect.equal - id - "[resourceId('myGroup', 'Microsoft.Network/connections', 'test')]" - "resourceId template function should match" - } + Expect.equal + outer.Template.Outputs.[2] + ("inner2.foo", "[reference('inner2').outputs['foo'].value]") + "output expression was incorrect" + } + test "Nested resource group can accept parameters" { + let inner1 = resourceGroup { + name "inner1" - test "Generates typed Resource Id with segments" { - let rid = - ResourceId.create ( - Arm.Network.connections, - ResourceName "test", - ResourceName "segment1", - ResourceName "segment2" - ) - - let id = rid.Eval() - - Expect.equal - id - "[resourceId('Microsoft.Network/connections', 'test', 'segment1', 'segment2')]" - "resourceId template function should match" + add_resource ( + vm { + name "vm" + username "foo" + } + ) } - test "Generates deployment Resource Id with template name" { - let deployment = resourceGroup { name "[resourceGroup.name()]" } - let id = deployment.ResourceId.Eval() - let expectedDeploymentIndex = ResourceGroup.deploymentIndex () - 1 + let outer = arm { add_resource inner1 } - Expect.equal - id - $"[resourceId(resourceGroup.name(), 'Microsoft.Resources/deployments', concat(resourceGroup.name(),'-deployment-{expectedDeploymentIndex}'))]" - "resourceId template function should match" - } + Expect.hasLength inner1.Template.Parameters 1 "inner template should have a parameter" + Expect.hasLength outer.Template.Parameters 1 "inner parameters should copy to outer template" - test "Fails if ARM expression is already quoted" { - Expect.throws (fun () -> ArmExpression.create "[test]" |> ignore) "" - } + Expect.equal + outer.Template.Parameters.[0] + (SecureParameter "password-for-vm") + "Parameter specification was incorrect" + } + test "Parameter value are copied to nested resource group deployment" { + let inner1 = resourceGroup { + name "inner1" - test "Correctly strips a literal expression" { - Expect.equal ((ArmExpression.literal "test").Eval()) "test" "" - } - - test "Does not fail if ARM expression contains an inner quote" { - Expect.equal "[foo[test]]" ((ArmExpression.create "foo[test]").Eval()) "" - } - test "Does not create empty nodes for core resource fields when nothing is supplied" { - let createdResource = ResourceType("Test", "2017-01-01").Create(ResourceName "Name") - - Expect.equal - createdResource - {| - name = "Name" - ``type`` = "Test" - apiVersion = "2017-01-01" - dependsOn = null - location = null - tags = null - |} - "Default values don't match" - } - test "Can nest resource groups" { - let template = - arm { - add_resource ( - resourceGroup { - name "inner" - add_resource (storageAccount { name "storage" }) - add_tag "deployment-tag" "inner-rg" - } - ) + add_resource ( + vm { + name "vm" + username "foo" } - - Expect.hasLength template.Template.Resources 1 "Outer template should contain only nested deployment" - - Expect.isTrue - (template.Template.Resources.[0] :? Arm.ResourceGroup.ResourceGroupDeployment) - "The only resource should be a resourceGroupDeployment" - - let innerDeployment = - template.Template.Resources.[0] :?> Arm.ResourceGroup.ResourceGroupDeployment - - Expect.hasLength innerDeployment.Resources 1 "Inner template should have 1 resource" - Expect.equal innerDeployment.TargetResourceGroup.Value "inner" "Inner template name is incorrect" - - Expect.isTrue - (innerDeployment.Template.Resources.[0] :? Arm.Storage.StorageAccount) - "The only resource in the inner deployment should be a storageAccount" + ) } - test "Nested resource group outputs are copied to outer deployments" { - let inner1 = - resourceGroup { - name "inner1" - deployment_name "inner1" - output "foo" "bax" - } - - let inner2 = - resourceGroup { - name "inner2" - deployment_name "inner2" - output "foo" "bay" - } - let outer = - arm { - add_resource inner1 - add_resource inner2 - output "foo" "baz" - } - - Expect.hasLength outer.Template.Outputs 3 "inner outputs should copy to outer template" - Expect.equal outer.Template.Outputs.[0] ("foo", "baz") "output expression was incorrect" + let outer = arm { add_resource inner1 } - Expect.equal - outer.Template.Outputs.[1] - ("inner1.foo", "[reference('inner1').outputs['foo'].value]") - "output expression was incorrect" + let deployment = + outer |> findAzureResources dummyClient.SerializationSettings - Expect.equal - outer.Template.Outputs.[2] - ("inner2.foo", "[reference('inner2').outputs['foo'].value]") - "output expression was incorrect" - } - test "Nested resource group can accept parameters" { - let inner1 = - resourceGroup { - name "inner1" - - add_resource ( - vm { - name "vm" - username "foo" - } - ) - } + let nestedParamsObj = deployment.[0].Properties.Parameters :?> JObject - let outer = arm { add_resource inner1 } + let nestedParams = + nestedParamsObj.Properties() + |> Seq.map (fun x -> x.Name, x.Value.SelectToken(".value").ToString()) + |> Map.ofSeq - Expect.hasLength inner1.Template.Parameters 1 "inner template should have a parameter" - Expect.hasLength outer.Template.Parameters 1 "inner parameters should copy to outer template" + Expect.equal + nestedParams.["password-for-vm"] + "[parameters('password-for-vm')]" + "Parameters not correctly proxied to nested template" + } + test "Can specify subscriptionId on nested deployment" { + let inner1 = resourceGroup { + name "inner1" + deployment_name "inner1-deployment" - Expect.equal - outer.Template.Parameters.[0] - (SecureParameter "password-for-vm") - "Parameter specification was incorrect" - } - test "Parameter value are copied to nested resource group deployment" { - let inner1 = - resourceGroup { - name "inner1" - - add_resource ( - vm { - name "vm" - username "foo" - } - ) + add_resource ( + vm { + name "vm" + username "foo" } + ) - let outer = arm { add_resource inner1 } - - let deployment = - outer |> findAzureResources dummyClient.SerializationSettings - - let nestedParamsObj = deployment.[0].Properties.Parameters :?> JObject - - let nestedParams = - nestedParamsObj.Properties() - |> Seq.map (fun x -> x.Name, x.Value.SelectToken(".value").ToString()) - |> Map.ofSeq - - Expect.equal - nestedParams.["password-for-vm"] - "[parameters('password-for-vm')]" - "Parameters not correctly proxied to nested template" + subscription_id "0c3054fb-f576-4458-acff-f2c29c4123e4" } - test "Can specify subscriptionId on nested deployment" { - let inner1 = - resourceGroup { - name "inner1" - deployment_name "inner1-deployment" - - add_resource ( - vm { - name "vm" - username "foo" - } - ) - - subscription_id "0c3054fb-f576-4458-acff-f2c29c4123e4" - } - let deployment = arm { add_resource inner1 } - let json = deployment.Template |> Writer.toJson - let jobj = JObject.Parse json + let deployment = arm { add_resource inner1 } + let json = deployment.Template |> Writer.toJson + let jobj = JObject.Parse json - let actual = - jobj.SelectToken("$.resources[?(@.name=='inner1-deployment')].subscriptionId") - |> string + let actual = + jobj.SelectToken("$.resources[?(@.name=='inner1-deployment')].subscriptionId") + |> string - Expect.equal - actual - "0c3054fb-f576-4458-acff-f2c29c4123e4" - "Nested deployment didn't have correct subscriptionId" - } - test "Simple parameter serializes correctly for nested deployment" { - let p1 = ParameterValue(Name = "param1", Value = "value1") + Expect.equal + actual + "0c3054fb-f576-4458-acff-f2c29c4123e4" + "Nested deployment didn't have correct subscriptionId" + } + test "Simple parameter serializes correctly for nested deployment" { + let p1 = ParameterValue(Name = "param1", Value = "value1") - let expectedP1 = - """{ + let expectedP1 = + """{ "param1": { "value": "value1" } }""" - let p1Json = dict [ p1.Key, p1.ParamValue ] |> Serialization.toJson - Expect.equal p1Json expectedP1 "p1 didn't serialize correctly" - } - test "Key Vault reference parameter serializes correctly for nested deployment" { - let kvRef1 = KeyVaultReference("param1", vaults.resourceId "myvault", "secret1") - let kvRef1Json = dict [ kvRef1.Key, kvRef1.ParamValue ] |> Serialization.toJson + let p1Json = dict [ p1.Key, p1.ParamValue ] |> Serialization.toJson + Expect.equal p1Json expectedP1 "p1 didn't serialize correctly" + } + test "Key Vault reference parameter serializes correctly for nested deployment" { + let kvRef1 = KeyVaultReference("param1", vaults.resourceId "myvault", "secret1") + let kvRef1Json = dict [ kvRef1.Key, kvRef1.ParamValue ] |> Serialization.toJson - let expected = - """{ + let expected = + """{ "param1": { "reference": { "keyVault": { @@ -461,135 +438,125 @@ let tests = } }""" - Expect.equal kvRef1Json expected "Key vault reference parameter didn't serialize correctly" - } - test "Can add simple parameters to nested deployment" { - let inner1 = - resourceGroup { - name "inner1" - - add_resources - [ - vm { - name "vm" - username "foo" - } - ] + Expect.equal kvRef1Json expected "Key vault reference parameter didn't serialize correctly" + } + test "Can add simple parameters to nested deployment" { + let inner1 = resourceGroup { + name "inner1" - add_parameter_values [ "param1", "value1"; "param2", "value2" ] + add_resources [ + vm { + name "vm" + username "foo" } + ] - let outer = arm { add_resource inner1 } + add_parameter_values [ "param1", "value1"; "param2", "value2" ] + } - let deployment = - outer |> findAzureResources dummyClient.SerializationSettings + let outer = arm { add_resource inner1 } - let nestedParamsObj = deployment.[0].Properties.Parameters :?> JObject + let deployment = + outer |> findAzureResources dummyClient.SerializationSettings - let nestedParams = - nestedParamsObj.Properties() - |> Seq.map (fun x -> x.Name, x.Value.SelectToken(".value").ToString()) - |> Map.ofSeq + let nestedParamsObj = deployment.[0].Properties.Parameters :?> JObject - Expect.equal nestedParams.["param1"] "value1" "Parameter 'param1' not passed to nested template" - Expect.equal nestedParams.["param2"] "value2" "Parameters 'param2' not passed to nested template" - } - test "Can add key vault reference parameters to nested deployment" { - let inner1 = - resourceGroup { - name "farmer-nested-params" + let nestedParams = + nestedParamsObj.Properties() + |> Seq.map (fun x -> x.Name, x.Value.SelectToken(".value").ToString()) + |> Map.ofSeq - add_resources - [ - vm { - name "vm" - username "foo" - } - ] + Expect.equal nestedParams.["param1"] "value1" "Parameter 'param1' not passed to nested template" + Expect.equal nestedParams.["param2"] "value2" "Parameters 'param2' not passed to nested template" + } + test "Can add key vault reference parameters to nested deployment" { + let inner1 = resourceGroup { + name "farmer-nested-params" - add_secret_references [ "password-for-vm", vaults.resourceId "myvault", "vm-password" ] + add_resources [ + vm { + name "vm" + username "foo" } + ] + + add_secret_references [ "password-for-vm", vaults.resourceId "myvault", "vm-password" ] + } - let outer = arm { add_resource inner1 } + let outer = arm { add_resource inner1 } - let deployment = - outer |> findAzureResources dummyClient.SerializationSettings + let deployment = + outer |> findAzureResources dummyClient.SerializationSettings - let nestedParamsObj = deployment.[0].Properties.Parameters :?> JObject + let nestedParamsObj = deployment.[0].Properties.Parameters :?> JObject - let nestedParams = - nestedParamsObj.Properties() - |> Seq.map (fun x -> x.Name, x.Value.SelectToken(".reference").ToString()) - |> Map.ofSeq + let nestedParams = + nestedParamsObj.Properties() + |> Seq.map (fun x -> x.Name, x.Value.SelectToken(".reference").ToString()) + |> Map.ofSeq - let expected = - """{ + let expected = + """{ "keyVault": { "id": "[resourceId('Microsoft.KeyVault/vaults', 'myvault')]" }, "secretName": "vm-password" }""" - Expect.equal - nestedParams.["password-for-vm"] - expected - "Parameter 'password-for-vm' keyvault reference incorrect in nested template." + Expect.equal + nestedParams.["password-for-vm"] + expected + "Parameter 'password-for-vm' keyvault reference incorrect in nested template." - let fullTemplate = outer.Template |> Writer.toJson - let jobjTemplate = JObject.Parse fullTemplate - let parametersJson = jobjTemplate.SelectToken("$.parameters") |> string + let fullTemplate = outer.Template |> Writer.toJson + let jobjTemplate = JObject.Parse fullTemplate + let parametersJson = jobjTemplate.SelectToken("$.parameters") |> string - Expect.equal - parametersJson - "{}" - "Outer template should not have parameter that is passed to inner template" - } - test "Can reference vault secret from another resource group" { - let webApp = - webApp { - name "resource-needing-vault-secret" + Expect.equal parametersJson "{}" "Outer template should not have parameter that is passed to inner template" + } + test "Can reference vault secret from another resource group" { + let webApp = webApp { + name "resource-needing-vault-secret" - secret_setting "SOME__ENV__VARIABLE" - } + secret_setting "SOME__ENV__VARIABLE" + } - let resourceGroupBeingDeployed = - resourceGroup { - name "rg-being-deployed" + let resourceGroupBeingDeployed = resourceGroup { + name "rg-being-deployed" - add_resource webApp + add_resource webApp - add_secret_references - [ - "SOME__ENV__VARIABLE", - vaults.resourceId ("vault-name", "already-deployed-resource-group-name"), - "vault-secret-name" - ] - } + add_secret_references [ + "SOME__ENV__VARIABLE", + vaults.resourceId ("vault-name", "already-deployed-resource-group-name"), + "vault-secret-name" + ] + } - let deployment = arm { add_resource resourceGroupBeingDeployed } + let deployment = arm { add_resource resourceGroupBeingDeployed } - let deployment = - deployment - |> findAzureResources dummyClient.SerializationSettings + let deployment = + deployment + |> findAzureResources dummyClient.SerializationSettings - let resourceGroupParamsObj = deployment.[0].Properties.Parameters :?> JObject + let resourceGroupParamsObj = deployment.[0].Properties.Parameters :?> JObject - let resourceGroupParams = - resourceGroupParamsObj.Properties() - |> Seq.map (fun x -> x.Name, x.Value.SelectToken(".reference").ToString()) - |> Map.ofSeq + let resourceGroupParams = + resourceGroupParamsObj.Properties() + |> Seq.map (fun x -> x.Name, x.Value.SelectToken(".reference").ToString()) + |> Map.ofSeq - let expected = - """{ + let expected = + """{ "keyVault": { "id": "[resourceId('already-deployed-resource-group-name', 'Microsoft.KeyVault/vaults', 'vault-name')]" }, "secretName": "vault-secret-name" }""" - Expect.equal - resourceGroupParams.["SOME__ENV__VARIABLE"] - expected - "Parameter 'vault-secret-name' is incorrect." - } - ] + Expect.equal + resourceGroupParams.["SOME__ENV__VARIABLE"] + expected + "Parameter 'vault-secret-name' is incorrect." + } + ] diff --git a/src/Tests/TrafficManager.fs b/src/Tests/TrafficManager.fs index efd690c7f..e0a46cf97 100644 --- a/src/Tests/TrafficManager.fs +++ b/src/Tests/TrafficManager.fs @@ -13,97 +13,93 @@ let dummyClient = new TrafficManagerManagementClient(Uri "http://management.azure.com", TokenCredentials "NotNullOrWhiteSpace") let tests = - testList - "Traffic Manager" - [ - test "Correctly serializes to JSON" { - let t = arm { add_resource (trafficManager { name "test" }) } - t.Template |> Writer.toJson |> ignore - } - - test "Can create a basic default Traffic Manager profile with sensible defaults" { - let tmName = "test-tm" - - let resource = - let tm = trafficManager { name tmName } - - arm { add_resource tm } - |> findAzureResources dummyClient.SerializationSettings - |> List.head - - Expect.equal resource.Name tmName "Profile name does not match" - Expect.equal resource.DnsConfig.Ttl (Nullable(int64 30)) "Default DNS TTL is incorrect" - - Expect.equal - resource.TrafficRoutingMethod - (Nullable Models.TrafficRoutingMethod.Performance) - "Default TrafficRoutingMethod is incorrect" - - Expect.equal - resource.TrafficViewEnrollmentStatus - (Nullable Models.TrafficViewEnrollmentStatus.Disabled) - "Default TrafficViewEnrollmentStatus is incorrect" - - Expect.equal resource.Location "global" "Location is incorrect" - Expect.equal resource.MonitorConfig.Path "/" "Default MonitorConfig Path is incorrect" - Expect.equal resource.MonitorConfig.Port (Nullable(int64 80)) "Default MonitorConfig Port is incorrect" - - Expect.equal - resource.MonitorConfig.IntervalInSeconds - (Nullable(int64 30)) - "Default MonitorConfig IntervalInSeconds is incorrect" - - Expect.equal - resource.MonitorConfig.Protocol - (Nullable Models.MonitorProtocol.HTTP) - "Default MonitorConfig MonitorProtocol is incorrect" - - Expect.equal - resource.MonitorConfig.TimeoutInSeconds - (Nullable(int64 10)) - "Default MonitorConfig TimeoutInSeconds is incorrect" - - Expect.equal - resource.MonitorConfig.ToleratedNumberOfFailures - (Nullable(int64 3)) - "Default MonitorConfig TimeoutInSeconds is incorrect" - } - test "Basic Traffic Manager profile with performance routing and equal priority endpoints" { - let tmName = "test-tm" - - let resource = - let tm = - trafficManager { - name tmName - - add_endpoints - [ - endpoint { - name "someapp-eastus" - target_external "eastus.farmer.com" Location.EastUS - } - endpoint { - name "someapp-westeurope" - target_external "westeurope.farmer.com" Location.WestEurope - } - ] + testList "Traffic Manager" [ + test "Correctly serializes to JSON" { + let t = arm { add_resource (trafficManager { name "test" }) } + t.Template |> Writer.toJson |> ignore + } + + test "Can create a basic default Traffic Manager profile with sensible defaults" { + let tmName = "test-tm" + + let resource = + let tm = trafficManager { name tmName } + + arm { add_resource tm } + |> findAzureResources dummyClient.SerializationSettings + |> List.head + + Expect.equal resource.Name tmName "Profile name does not match" + Expect.equal resource.DnsConfig.Ttl (Nullable(int64 30)) "Default DNS TTL is incorrect" + + Expect.equal + resource.TrafficRoutingMethod + (Nullable Models.TrafficRoutingMethod.Performance) + "Default TrafficRoutingMethod is incorrect" + + Expect.equal + resource.TrafficViewEnrollmentStatus + (Nullable Models.TrafficViewEnrollmentStatus.Disabled) + "Default TrafficViewEnrollmentStatus is incorrect" + + Expect.equal resource.Location "global" "Location is incorrect" + Expect.equal resource.MonitorConfig.Path "/" "Default MonitorConfig Path is incorrect" + Expect.equal resource.MonitorConfig.Port (Nullable(int64 80)) "Default MonitorConfig Port is incorrect" + + Expect.equal + resource.MonitorConfig.IntervalInSeconds + (Nullable(int64 30)) + "Default MonitorConfig IntervalInSeconds is incorrect" + + Expect.equal + resource.MonitorConfig.Protocol + (Nullable Models.MonitorProtocol.HTTP) + "Default MonitorConfig MonitorProtocol is incorrect" + + Expect.equal + resource.MonitorConfig.TimeoutInSeconds + (Nullable(int64 10)) + "Default MonitorConfig TimeoutInSeconds is incorrect" + + Expect.equal + resource.MonitorConfig.ToleratedNumberOfFailures + (Nullable(int64 3)) + "Default MonitorConfig TimeoutInSeconds is incorrect" + } + test "Basic Traffic Manager profile with performance routing and equal priority endpoints" { + let tmName = "test-tm" + + let resource = + let tm = trafficManager { + name tmName + + add_endpoints [ + endpoint { + name "someapp-eastus" + target_external "eastus.farmer.com" Location.EastUS } + endpoint { + name "someapp-westeurope" + target_external "westeurope.farmer.com" Location.WestEurope + } + ] + } - arm { add_resource tm } - |> findAzureResources dummyClient.SerializationSettings - |> List.head + arm { add_resource tm } + |> findAzureResources dummyClient.SerializationSettings + |> List.head - Expect.equal resource.Name tmName "Profile name does not match" + Expect.equal resource.Name tmName "Profile name does not match" - Expect.equal - resource.TrafficRoutingMethod - (Nullable Models.TrafficRoutingMethod.Performance) - "Default TrafficRoutingMethod is incorrect" + Expect.equal + resource.TrafficRoutingMethod + (Nullable Models.TrafficRoutingMethod.Performance) + "Default TrafficRoutingMethod is incorrect" - Expect.equal resource.Endpoints.Count 2 "Incorrect number of endpoints" + Expect.equal resource.Endpoints.Count 2 "Incorrect number of endpoints" - for endpoint in resource.Endpoints do - Expect.isFalse endpoint.Priority.HasValue "Should not have a value for priority" - Expect.isFalse endpoint.Weight.HasValue "Should not have a value for weight" - } - ] + for endpoint in resource.Endpoints do + Expect.isFalse endpoint.Priority.HasValue "Should not have a value for priority" + Expect.isFalse endpoint.Weight.HasValue "Should not have a value for weight" + } + ] diff --git a/src/Tests/Types.fs b/src/Tests/Types.fs index 0fc43df1a..5cccb0d33 100644 --- a/src/Tests/Types.fs +++ b/src/Tests/Types.fs @@ -5,11 +5,9 @@ open Farmer open System let tests = - testList - "Type Tests" - [ - test "Creates deterministic GUID correctly" { - let actual = DeterministicGuid.create "hello" - Expect.equal (Guid.Parse "4fbe461c-3438-55c4-941e-d1c2013210c5") actual "Incorrect GUID" - } - ] + testList "Type Tests" [ + test "Creates deterministic GUID correctly" { + let actual = DeterministicGuid.create "hello" + Expect.equal (Guid.Parse "4fbe461c-3438-55c4-941e-d1c2013210c5") actual "Incorrect GUID" + } + ] diff --git a/src/Tests/VirtualHub.fs b/src/Tests/VirtualHub.fs index 50813d494..22dacd010 100644 --- a/src/Tests/VirtualHub.fs +++ b/src/Tests/VirtualHub.fs @@ -60,257 +60,247 @@ let getResourceDependsOnByName (template: Deployment) (resourceName: ResourceNam ] let virtualHubTests = - testList - "VirtualHub only Tests" - [ - test "VirtualHub is correctly created" { - let vhub = vhub { name "my-vhub" } |> asAzureResource - Expect.equal vhub.Name "my-vhub" "" - Expect.equal vhub.Sku Sku.Standard.ArmValue "" - } - test "VirtualHub with address prefix" { - let expectedAddressPrefix = (IPAddressCidr.parse "10.0.0.0/24") - - let vhub = - vhub { - name "my-vhub" - address_prefix expectedAddressPrefix - } - |> asAzureResource - - Expect.equal vhub.AddressPrefix (IPAddressCidr.format expectedAddressPrefix) "" - } - test "VirtualHub does not create resources for unmanaged linked resources" { - let resources = - vhub { - name "my-vhub" - link_to_unmanaged_vwan (Farmer.Arm.VirtualWan.virtualWans.resourceId "my-vwan") - } - |> getResources - - Expect.hasLength resources 1 "" - } - test "VirtualHub does not create resources for managed linked resources" { - let resources = - vhub { - name "my-vhub" - link_to_vwan (vwan { name "my-vwan" }) - } - |> getResources - - Expect.hasLength resources 1 "" - } - test "VirtualHub does not create dependencies for unmanaged linked resources" { - let resource = - vhub { - name "my-vhub" - link_to_unmanaged_vwan (Farmer.Arm.VirtualWan.virtualWans.resourceId "my-vwan") - } - |> getResources - |> getVirtualHubResource - |> List.head - - Expect.isEmpty resource.Dependencies "" - } - test "VirtualHub creates dependencies for managed linked resources" { - let resource = - vhub { - name "my-vhub" - link_to_vwan (vwan { name "my-vwan" }) - } - |> getResources - |> getVirtualHubResource - |> List.head - - Expect.containsAll - resource.Dependencies - [ - ResourceId.create (Farmer.Arm.VirtualWan.virtualWans, ResourceName "my-vwan") + testList "VirtualHub only Tests" [ + test "VirtualHub is correctly created" { + let vhub = vhub { name "my-vhub" } |> asAzureResource + Expect.equal vhub.Name "my-vhub" "" + Expect.equal vhub.Sku Sku.Standard.ArmValue "" + } + test "VirtualHub with address prefix" { + let expectedAddressPrefix = (IPAddressCidr.parse "10.0.0.0/24") + + let vhub = + vhub { + name "my-vhub" + address_prefix expectedAddressPrefix + } + |> asAzureResource + + Expect.equal vhub.AddressPrefix (IPAddressCidr.format expectedAddressPrefix) "" + } + test "VirtualHub does not create resources for unmanaged linked resources" { + let resources = + vhub { + name "my-vhub" + link_to_unmanaged_vwan (Farmer.Arm.VirtualWan.virtualWans.resourceId "my-vwan") + } + |> getResources + + Expect.hasLength resources 1 "" + } + test "VirtualHub does not create resources for managed linked resources" { + let resources = + vhub { + name "my-vhub" + link_to_vwan (vwan { name "my-vwan" }) + } + |> getResources + + Expect.hasLength resources 1 "" + } + test "VirtualHub does not create dependencies for unmanaged linked resources" { + let resource = + vhub { + name "my-vhub" + link_to_unmanaged_vwan (Farmer.Arm.VirtualWan.virtualWans.resourceId "my-vwan") + } + |> getResources + |> getVirtualHubResource + |> List.head + + Expect.isEmpty resource.Dependencies "" + } + test "VirtualHub creates dependencies for managed linked resources" { + let resource = + vhub { + name "my-vhub" + link_to_vwan (vwan { name "my-vwan" }) + } + |> getResources + |> getVirtualHubResource + |> List.head + + Expect.containsAll + resource.Dependencies + [ + ResourceId.create (Farmer.Arm.VirtualWan.virtualWans, ResourceName "my-vwan") + ] + "" + } + test "VirtualHub creates empty dependsOn in arm template json for unmanaged linked resources" { + let template = + arm { + add_resources [ + vhub { + name "my-vhub" + link_to_unmanaged_vwan (Farmer.Arm.VirtualWan.virtualWans.resourceId "my-vwan") + } ] - "" - } - test "VirtualHub creates empty dependsOn in arm template json for unmanaged linked resources" { - let template = - arm { - add_resources - [ - vhub { - name "my-vhub" - link_to_unmanaged_vwan (Farmer.Arm.VirtualWan.virtualWans.resourceId "my-vwan") - } - ] - } - :> IDeploymentSource - - let dependsOn = - getResourceDependsOnByName template.Deployment (ResourceName "my-vhub") - - Expect.hasLength dependsOn 0 "" - } - test "VirtualHub creates dependsOn in arm template json for managed linked resources" { - let template = - arm { - add_resources - [ - vhub { - name "my-vhub" - link_to_vwan (vwan { name "my-vwan" }) - } - ] - } - :> IDeploymentSource - - let dependsOn = - getResourceDependsOnByName template.Deployment (ResourceName "my-vhub") - - Expect.hasLength dependsOn 1 "" - - let expectedVwanDependency = - "[resourceId('Microsoft.Network/virtualWans', 'my-vwan')]" - - Expect.equal dependsOn.Head expectedVwanDependency "" - } - ] + } + :> IDeploymentSource + + let dependsOn = + getResourceDependsOnByName template.Deployment (ResourceName "my-vhub") + + Expect.hasLength dependsOn 0 "" + } + test "VirtualHub creates dependsOn in arm template json for managed linked resources" { + let template = + arm { + add_resources [ + vhub { + name "my-vhub" + link_to_vwan (vwan { name "my-vwan" }) + } + ] + } + :> IDeploymentSource + + let dependsOn = + getResourceDependsOnByName template.Deployment (ResourceName "my-vhub") + + Expect.hasLength dependsOn 1 "" + + let expectedVwanDependency = + "[resourceId('Microsoft.Network/virtualWans', 'my-vwan')]" + + Expect.equal dependsOn.Head expectedVwanDependency "" + } + ] let hubRouteTableTests = - testList - "Hub Route Table Tests" - [ - test "HubRouteTable is correctly created" { - let routeTableResourceName = ResourceName "my-routetable" - let vhubResourceName = ResourceName "my-vhub" - - let routeTable = - hubRouteTable { - name routeTableResourceName - link_to_vhub (vhub { name vhubResourceName }) - } - |> hubRouteTableAsAzureResource - - Expect.equal routeTable.Name (vhubResourceName / routeTableResourceName).Value "" - Expect.isEmpty routeTable.Routes "" - } - ptest "HubRouteTable adds routes with same NextHop resourceId" { - let routeTableResourceName = ResourceName "my-routetable" - let vhubResourceName = ResourceName "my-vhub" - - let routeTable = - hubRouteTable { - name routeTableResourceName - link_to_vhub (vhub { name vhubResourceName }) - - add_routes - [ - { - Name = "route1" - Destination = Destination.CidrDestination [ (IPAddressCidr.parse "10.0.0.0/24") ] - NextHop = - NextHop.ResourceId(LinkedResource.Unmanaged(virtualHubs.resourceId "next-hub")) - } - ] - } - |> hubRouteTableAsAzureResource - - Expect.equal routeTable.Name (vhubResourceName / routeTableResourceName).Value "" - Expect.isEmpty routeTable.Routes "" - } - test "HubRouteTable does not create dependencies for unmanaged linked resources" { - let routeTableResourceName = "my-routetable" - let vhubResourceName = "my-vhub" - - let resource = - hubRouteTable { - name routeTableResourceName - link_to_unmanaged_vhub (virtualHubs.resourceId vhubResourceName) - } - |> getResources - |> getHubRouteTableResource - |> List.head - - Expect.isEmpty resource.Dependencies "" - } - test "HubRouteTable creates dependencies for managed linked resources" { - let routeTableResourceName = "my-routetable" - let vhubResourceName = "my-vhub" - - let resource = - hubRouteTable { - name routeTableResourceName - link_to_vhub (vhub { name vhubResourceName }) - } - |> getResources - |> getHubRouteTableResource - |> List.head - - Expect.containsAll - resource.Dependencies - [ ResourceId.create (virtualHubs, (ResourceName vhubResourceName)) ] - "" - } - test "HubRouteTable creates empty dependsOn in arm template json for unmanaged linked resources" { - let routeTableResourceName = ResourceName "my-routetable" - let vhubResourceName = ResourceName "my-vhub" - - let template = - arm { - add_resources - [ - hubRouteTable { - name routeTableResourceName - link_to_unmanaged_vhub (virtualHubs.resourceId vhubResourceName) - } - ] - } - :> IDeploymentSource - - let dependsOn = - getResourceDependsOnByName template.Deployment (vhubResourceName / routeTableResourceName) - - Expect.hasLength dependsOn 0 "" - } - test "HubRouteTable creates dependsOn in arm template json for managed linked resources" { - let routeTableResourceName = ResourceName "my-routetable" - let vhubResourceName = ResourceName "my-vhub" - - let template = - arm { - add_resources - [ - hubRouteTable { - name routeTableResourceName - link_to_vhub (vhub { name vhubResourceName }) - } - ] - } - :> IDeploymentSource - - let dependsOn = - getResourceDependsOnByName template.Deployment (vhubResourceName / routeTableResourceName) - - Expect.hasLength dependsOn 1 "" - - let expectedDependency = - $"[resourceId('Microsoft.Network/virtualHubs', '{vhubResourceName.Value}')]" - - Expect.equal dependsOn.Head expectedDependency "" - } - test "HubRouteTable appends labels" { - let routeTableResourceName = "my-routetable" - let vhubResourceName = "my-vhub" - let expectedLabels = [ "label1"; "label2" ] - - let resource = - hubRouteTable { - name routeTableResourceName - link_to_unmanaged_vhub (virtualHubs.resourceId vhubResourceName) - add_labels expectedLabels - } - |> getResources - |> getHubRouteTableResource - |> List.head - - Expect.equal resource.Labels expectedLabels "" - } - ] + testList "Hub Route Table Tests" [ + test "HubRouteTable is correctly created" { + let routeTableResourceName = ResourceName "my-routetable" + let vhubResourceName = ResourceName "my-vhub" + + let routeTable = + hubRouteTable { + name routeTableResourceName + link_to_vhub (vhub { name vhubResourceName }) + } + |> hubRouteTableAsAzureResource + + Expect.equal routeTable.Name (vhubResourceName / routeTableResourceName).Value "" + Expect.isEmpty routeTable.Routes "" + } + ptest "HubRouteTable adds routes with same NextHop resourceId" { + let routeTableResourceName = ResourceName "my-routetable" + let vhubResourceName = ResourceName "my-vhub" + + let routeTable = + hubRouteTable { + name routeTableResourceName + link_to_vhub (vhub { name vhubResourceName }) + + add_routes [ + { + Name = "route1" + Destination = Destination.CidrDestination [ (IPAddressCidr.parse "10.0.0.0/24") ] + NextHop = NextHop.ResourceId(LinkedResource.Unmanaged(virtualHubs.resourceId "next-hub")) + } + ] + } + |> hubRouteTableAsAzureResource + + Expect.equal routeTable.Name (vhubResourceName / routeTableResourceName).Value "" + Expect.isEmpty routeTable.Routes "" + } + test "HubRouteTable does not create dependencies for unmanaged linked resources" { + let routeTableResourceName = "my-routetable" + let vhubResourceName = "my-vhub" + + let resource = + hubRouteTable { + name routeTableResourceName + link_to_unmanaged_vhub (virtualHubs.resourceId vhubResourceName) + } + |> getResources + |> getHubRouteTableResource + |> List.head + + Expect.isEmpty resource.Dependencies "" + } + test "HubRouteTable creates dependencies for managed linked resources" { + let routeTableResourceName = "my-routetable" + let vhubResourceName = "my-vhub" + + let resource = + hubRouteTable { + name routeTableResourceName + link_to_vhub (vhub { name vhubResourceName }) + } + |> getResources + |> getHubRouteTableResource + |> List.head + + Expect.containsAll + resource.Dependencies + [ ResourceId.create (virtualHubs, (ResourceName vhubResourceName)) ] + "" + } + test "HubRouteTable creates empty dependsOn in arm template json for unmanaged linked resources" { + let routeTableResourceName = ResourceName "my-routetable" + let vhubResourceName = ResourceName "my-vhub" + + let template = + arm { + add_resources [ + hubRouteTable { + name routeTableResourceName + link_to_unmanaged_vhub (virtualHubs.resourceId vhubResourceName) + } + ] + } + :> IDeploymentSource + + let dependsOn = + getResourceDependsOnByName template.Deployment (vhubResourceName / routeTableResourceName) + + Expect.hasLength dependsOn 0 "" + } + test "HubRouteTable creates dependsOn in arm template json for managed linked resources" { + let routeTableResourceName = ResourceName "my-routetable" + let vhubResourceName = ResourceName "my-vhub" + + let template = + arm { + add_resources [ + hubRouteTable { + name routeTableResourceName + link_to_vhub (vhub { name vhubResourceName }) + } + ] + } + :> IDeploymentSource + + let dependsOn = + getResourceDependsOnByName template.Deployment (vhubResourceName / routeTableResourceName) + + Expect.hasLength dependsOn 1 "" + + let expectedDependency = + $"[resourceId('Microsoft.Network/virtualHubs', '{vhubResourceName.Value}')]" + + Expect.equal dependsOn.Head expectedDependency "" + } + test "HubRouteTable appends labels" { + let routeTableResourceName = "my-routetable" + let vhubResourceName = "my-vhub" + let expectedLabels = [ "label1"; "label2" ] + + let resource = + hubRouteTable { + name routeTableResourceName + link_to_unmanaged_vhub (virtualHubs.resourceId vhubResourceName) + add_labels expectedLabels + } + |> getResources + |> getHubRouteTableResource + |> List.head + + Expect.equal resource.Labels expectedLabels "" + } + ] let tests = testList "Virtual Hub Tests" [ virtualHubTests; hubRouteTableTests ] diff --git a/src/Tests/VirtualMachine.fs b/src/Tests/VirtualMachine.fs index c65447487..60e0b7706 100644 --- a/src/Tests/VirtualMachine.fs +++ b/src/Tests/VirtualMachine.fs @@ -16,1165 +16,1091 @@ let client = new ComputeManagementClient(Uri "http://management.azure.com", TokenCredentials "NotNullOrWhiteSpace") let tests = - testList - "Virtual Machine" - [ - test "Can create a basic virtual machine" { - let resource = - let myVm = - vm { - name "isaacsVM" - username "isaac" - vm_size Standard_A2 - operating_system WindowsServer_2012Datacenter - os_disk 128 StandardSSD_LRS - add_ssd_disk 128 - add_slow_disk 512 - diagnostics_support - } + testList "Virtual Machine" [ + test "Can create a basic virtual machine" { + let resource = + let myVm = vm { + name "isaacsVM" + username "isaac" + vm_size Standard_A2 + operating_system WindowsServer_2012Datacenter + os_disk 128 StandardSSD_LRS + add_ssd_disk 128 + add_slow_disk 512 + diagnostics_support + } - arm { add_resource myVm } - |> findAzureResources client.SerializationSettings - |> List.find (fun r -> r.StorageProfile |> isNull |> not) + arm { add_resource myVm } + |> findAzureResources client.SerializationSettings + |> List.find (fun r -> r.StorageProfile |> isNull |> not) - resource.Validate() + resource.Validate() - Expect.equal resource.StorageProfile.OsDisk.DiskSizeGB (Nullable 128) "Incorrect OS disk size" + Expect.equal resource.StorageProfile.OsDisk.DiskSizeGB (Nullable 128) "Incorrect OS disk size" - Expect.equal - resource.StorageProfile.ImageReference.Offer - WindowsServer_2012Datacenter.Offer.ArmValue - "Incorrect Offer" + Expect.equal + resource.StorageProfile.ImageReference.Offer + WindowsServer_2012Datacenter.Offer.ArmValue + "Incorrect Offer" - Expect.equal resource.StorageProfile.DataDisks.Count 2 "Incorrect number of data disks" - Expect.equal resource.OsProfile.AdminUsername "isaac" "Incorrect username" + Expect.equal resource.StorageProfile.DataDisks.Count 2 "Incorrect number of data disks" + Expect.equal resource.OsProfile.AdminUsername "isaac" "Incorrect username" - Expect.equal - resource.NetworkProfile.NetworkInterfaces.[0].Id - "[resourceId('Microsoft.Network/networkInterfaces', 'isaacsVM-nic')]" - "Incorrect NIC reference" + Expect.equal + resource.NetworkProfile.NetworkInterfaces.[0].Id + "[resourceId('Microsoft.Network/networkInterfaces', 'isaacsVM-nic')]" + "Incorrect NIC reference" - Expect.isTrue - (resource.DiagnosticsProfile.BootDiagnostics.Enabled.GetValueOrDefault false) - "Boot Diagnostics should be enabled" + Expect.isTrue + (resource.DiagnosticsProfile.BootDiagnostics.Enabled.GetValueOrDefault false) + "Boot Diagnostics should be enabled" - Expect.equal - resource.DiagnosticsProfile.BootDiagnostics.StorageUri - "[reference(resourceId('Microsoft.Storage/storageAccounts', 'isaacsvmstorage'), '2022-05-01').primaryEndpoints.blob]" - "Incorrect diagnostics storage Uri" - } + Expect.equal + resource.DiagnosticsProfile.BootDiagnostics.StorageUri + "[reference(resourceId('Microsoft.Storage/storageAccounts', 'isaacsvmstorage'), '2022-05-01').primaryEndpoints.blob]" + "Incorrect diagnostics storage Uri" + } - test "By default, VM does not include Priority" { - let template = - let myVm = - vm { - name "myvm" - username "me" - } + test "By default, VM does not include Priority" { + let template = + let myVm = vm { + name "myvm" + username "me" + } - arm { add_resource myVm } + arm { add_resource myVm } - let jobj = Newtonsoft.Json.Linq.JObject.Parse(template.Template |> Writer.toJson) + let jobj = Newtonsoft.Json.Linq.JObject.Parse(template.Template |> Writer.toJson) - let vmProperties = - jobj.SelectToken("resources[?(@.name=='myvm')].properties") :?> Newtonsoft.Json.Linq.JObject + let vmProperties = + jobj.SelectToken("resources[?(@.name=='myvm')].properties") :?> Newtonsoft.Json.Linq.JObject - Expect.isNull (vmProperties.Property "priority") "Priority should not be set by default" - } - test "Can create a basic virtual machine with managed boot diagnostics" { - let resource = - let myVm = - vm { - name "bootdiagvm" - username "farmeruser" - vm_size Standard_A2 - operating_system UbuntuServer_1804LTS - diagnostics_support_managed - } - - arm { add_resource myVm } - |> findAzureResources client.SerializationSettings - |> List.head + Expect.isNull (vmProperties.Property "priority") "Priority should not be set by default" + } + test "Can create a basic virtual machine with managed boot diagnostics" { + let resource = + let myVm = vm { + name "bootdiagvm" + username "farmeruser" + vm_size Standard_A2 + operating_system UbuntuServer_1804LTS + diagnostics_support_managed + } - resource.Validate() + arm { add_resource myVm } + |> findAzureResources client.SerializationSettings + |> List.head - Expect.isTrue - (resource.DiagnosticsProfile.BootDiagnostics.Enabled.GetValueOrDefault false) - "Boot Diagnostics should be enabled" + resource.Validate() - Expect.isNull - resource.DiagnosticsProfile.BootDiagnostics.StorageUri - "Storage should be null for managed boot diagnotics" - } - test "VM with existing external storage for diagnostics support doesn't have dependency for storage" { - let deployment = - arm { - add_resources - [ - vm { - name "myvm" - username "azureuser" - - diagnostics_support_external ( - Farmer.Arm.Storage.storageAccounts.resourceId "vmdiagstorage" - ) - } - ] - } + Expect.isTrue + (resource.DiagnosticsProfile.BootDiagnostics.Enabled.GetValueOrDefault false) + "Boot Diagnostics should be enabled" - let jobj = JObject.Parse(deployment.Template |> Writer.toJson) - let myvm = jobj.SelectToken "resources[?(@.name=='myvm')]" + Expect.isNull + resource.DiagnosticsProfile.BootDiagnostics.StorageUri + "Storage should be null for managed boot diagnotics" + } + test "VM with existing external storage for diagnostics support doesn't have dependency for storage" { + let deployment = arm { + add_resources [ + vm { + name "myvm" + username "azureuser" - Expect.isFalse - ((myvm.["dependsOn"] |> string).Contains "vmdiagstorage") - "Should not contain 'vmdiagstorage' dependency" - } - test "VM with existing managed storage for diagnostics support" { - let deployment = - arm { - add_resources - [ - vm { - name "myvm" - username "azureuser" - - diagnostics_support_external ( - LinkedResource.Managed( - Farmer.Arm.Storage.storageAccounts.resourceId "vmdiagstorage" - ) - ) - } - ] + diagnostics_support_external (Farmer.Arm.Storage.storageAccounts.resourceId "vmdiagstorage") } - - let jobj = JObject.Parse(deployment.Template |> Writer.toJson) - let myvm = jobj.SelectToken "resources[?(@.name=='myvm')]" - - Expect.contains - myvm.["dependsOn"] - (JValue "[resourceId('Microsoft.Storage/storageAccounts', 'vmdiagstorage')]") - "Should contain 'vmdiagstorage' dependency" + ] } - test "Can create a basic virtual machine with no data disk" { - let resource = - let myVm = - vm { - name "nodatadiskvm" - username "farmeruser" - vm_size Standard_A2 - no_data_disk - operating_system UbuntuServer_1804LTS - diagnostics_support_managed - } - arm { add_resource myVm } - |> findAzureResources client.SerializationSettings - |> List.head + let jobj = JObject.Parse(deployment.Template |> Writer.toJson) + let myvm = jobj.SelectToken "resources[?(@.name=='myvm')]" - resource.Validate() - Expect.hasLength resource.StorageProfile.DataDisks 0 "Should have no data disks" - } - test "Creates a parameter for the password" { - let deployment = - arm { - add_resource ( - vm { - name "isaac" - username "foo" - } - ) - } + Expect.isFalse + ((myvm.["dependsOn"] |> string).Contains "vmdiagstorage") + "Should not contain 'vmdiagstorage' dependency" + } + test "VM with existing managed storage for diagnostics support" { + let deployment = arm { + add_resources [ + vm { + name "myvm" + username "azureuser" - let template = deployment.Template |> Writer.TemplateGeneration.processTemplate - Expect.isTrue (template.parameters.ContainsKey "password-for-isaac") "Missing parameter" - Expect.equal template.parameters.Count 1 "Should only be one parameter" - } - test "Throws an error if you upload script files but no script" { - let createVm () = - arm { - add_resource ( - vm { - name "foo" - username "foo" - custom_script_files [ "http://test.fsx" ] - } + diagnostics_support_external ( + LinkedResource.Managed(Farmer.Arm.Storage.storageAccounts.resourceId "vmdiagstorage") ) } - |> ignore + ] + } + + let jobj = JObject.Parse(deployment.Template |> Writer.toJson) + let myvm = jobj.SelectToken "resources[?(@.name=='myvm')]" + + Expect.contains + myvm.["dependsOn"] + (JValue "[resourceId('Microsoft.Storage/storageAccounts', 'vmdiagstorage')]") + "Should contain 'vmdiagstorage' dependency" + } + test "Can create a basic virtual machine with no data disk" { + let resource = + let myVm = vm { + name "nodatadiskvm" + username "farmeruser" + vm_size Standard_A2 + no_data_disk + operating_system UbuntuServer_1804LTS + diagnostics_support_managed + } + + arm { add_resource myVm } + |> findAzureResources client.SerializationSettings + |> List.head - Expect.throws createVm "No script was supplied" + resource.Validate() + Expect.hasLength resource.StorageProfile.DataDisks 0 "Should have no data disks" + } + test "Creates a parameter for the password" { + let deployment = arm { + add_resource ( + vm { + name "isaac" + username "foo" + } + ) } - test "Does not throws an error if you provide a script" { + + let template = deployment.Template |> Writer.TemplateGeneration.processTemplate + Expect.isTrue (template.parameters.ContainsKey "password-for-isaac") "Missing parameter" + Expect.equal template.parameters.Count 1 "Should only be one parameter" + } + test "Throws an error if you upload script files but no script" { + let createVm () = arm { add_resource ( vm { name "foo" username "foo" - custom_script "foo" custom_script_files [ "http://test.fsx" ] } ) } |> ignore - arm { - add_resource ( - vm { - name "foo" - username "foo" - custom_script "foo" - } - ) - } - |> ignore + Expect.throws createVm "No script was supplied" + } + test "Does not throws an error if you provide a script" { + arm { + add_resource ( + vm { + name "foo" + username "foo" + custom_script "foo" + custom_script_files [ "http://test.fsx" ] + } + ) + } + |> ignore + + arm { + add_resource ( + vm { + name "foo" + username "foo" + custom_script "foo" + } + ) } + |> ignore + } - test "CustomData is correctly encoded" { - let deployment = - arm { - add_resources - [ - vm { - name "foo" - username "foo" - custom_data "foo" - } - ] + test "CustomData is correctly encoded" { + let deployment = arm { + add_resources [ + vm { + name "foo" + username "foo" + custom_data "foo" } + ] + } - let json = deployment.Template |> Writer.toJson - let jobj = Newtonsoft.Json.Linq.JObject.Parse(json) + let json = deployment.Template |> Writer.toJson + let jobj = Newtonsoft.Json.Linq.JObject.Parse(json) - let customData = - jobj.SelectToken("resources[?(@.name=='foo')].properties.osProfile.customData") + let customData = + jobj.SelectToken("resources[?(@.name=='foo')].properties.osProfile.customData") - let actualCustomData = (customData.ToString()) - let expectedCustomData = "Zm9v" - Expect.equal actualCustomData expectedCustomData "customData was not correctly encoded" - } + let actualCustomData = (customData.ToString()) + let expectedCustomData = "Zm9v" + Expect.equal actualCustomData expectedCustomData "customData was not correctly encoded" + } - test "Can remove public Ip" { - let deployment = - arm { - add_resources - [ - vm { - name "foo" - username "foo" - public_ip None - } - ] + test "Can remove public Ip" { + let deployment = arm { + add_resources [ + vm { + name "foo" + username "foo" + public_ip None } + ] + } - let json = deployment.Template |> Writer.toJson - let jobj = Newtonsoft.Json.Linq.JObject.Parse(json) + let json = deployment.Template |> Writer.toJson + let jobj = Newtonsoft.Json.Linq.JObject.Parse(json) - let publicIps = - jobj.SelectTokens("resources[?(@.type=='Microsoft.Network/publicIPAddresses')]") + let publicIps = + jobj.SelectTokens("resources[?(@.type=='Microsoft.Network/publicIPAddresses')]") - Expect.isEmpty publicIps "No public IP should be created" + Expect.isEmpty publicIps "No public IP should be created" - let nicDependsOn = - jobj.SelectTokens("resources[?(@.type=='Microsoft.Network/networkInterfaces')].dependsOn.[*]") - |> Seq.map (fun x -> x.ToString()) + let nicDependsOn = + jobj.SelectTokens("resources[?(@.type=='Microsoft.Network/networkInterfaces')].dependsOn.[*]") + |> Seq.map (fun x -> x.ToString()) - Expect.isEmpty - (nicDependsOn - |> Seq.filter (fun x -> x.Contains("Microsoft.Network/publicIPAddresses"))) - "Network Interface should not depende on any public IP" + Expect.isEmpty + (nicDependsOn + |> Seq.filter (fun x -> x.Contains("Microsoft.Network/publicIPAddresses"))) + "Network Interface should not depende on any public IP" - let nicPublicIp = - jobj.SelectTokens( - "resources[?(@.type=='Microsoft.Network/networkInterfaces')].properties.publicIpAddress" - ) + let nicPublicIp = + jobj.SelectTokens( + "resources[?(@.type=='Microsoft.Network/networkInterfaces')].properties.publicIpAddress" + ) - Expect.isEmpty (nicPublicIp) "Network Interface should not link to any public IP" - } + Expect.isEmpty (nicPublicIp) "Network Interface should not link to any public IP" + } - test "Can create static Ip" { - let deployment = - arm { - add_resources - [ - vm { - name "foo" - username "foo" - ip_allocation PublicIpAddress.Static - } - ] + test "Can create static Ip" { + let deployment = arm { + add_resources [ + vm { + name "foo" + username "foo" + ip_allocation PublicIpAddress.Static } + ] + } - let json = deployment.Template |> Writer.toJson - let jobj = Newtonsoft.Json.Linq.JObject.Parse(json) + let json = deployment.Template |> Writer.toJson + let jobj = Newtonsoft.Json.Linq.JObject.Parse(json) - let publicIpProps = - jobj.SelectTokens("resources[?(@.type=='Microsoft.Network/publicIPAddresses')].properties") + let publicIpProps = + jobj.SelectTokens("resources[?(@.type=='Microsoft.Network/publicIPAddresses')].properties") - Expect.isNonEmpty publicIpProps "IP settings not found" + Expect.isNonEmpty publicIpProps "IP settings not found" - let ipToken = publicIpProps |> Seq.head + let ipToken = publicIpProps |> Seq.head - let expectedToken = - Newtonsoft.Json.Linq.JToken.Parse("{\"publicIPAllocationMethod\": \"Static\"}") + let expectedToken = + Newtonsoft.Json.Linq.JToken.Parse("{\"publicIPAllocationMethod\": \"Static\"}") - Expect.equal (ipToken.ToString()) (expectedToken.ToString()) "Static IP was not found" + Expect.equal (ipToken.ToString()) (expectedToken.ToString()) "Static IP was not found" - } + } - test "Disabled password auth" { - let deployment = - arm { - add_resources - [ - vm { - name "foo" - username "foo" - disable_password_authentication true - add_authorized_key "fooPath" "fooKey" - } - ] + test "Disabled password auth" { + let deployment = arm { + add_resources [ + vm { + name "foo" + username "foo" + disable_password_authentication true + add_authorized_key "fooPath" "fooKey" } + ] + } - let json = deployment.Template |> Writer.toJson - let jobj = Newtonsoft.Json.Linq.JObject.Parse(json) + let json = deployment.Template |> Writer.toJson + let jobj = Newtonsoft.Json.Linq.JObject.Parse(json) - let linuxConfig = - jobj.SelectToken("resources[?(@.name=='foo')].properties.osProfile.linuxConfiguration") + let linuxConfig = + jobj.SelectToken("resources[?(@.name=='foo')].properties.osProfile.linuxConfiguration") - let passwordAuthentication = - jobj - .SelectToken( - "resources[?(@.name=='foo')].properties.osProfile.linuxConfiguration.disablePasswordAuthentication" - ) - .ToString() + let passwordAuthentication = + jobj + .SelectToken( + "resources[?(@.name=='foo')].properties.osProfile.linuxConfiguration.disablePasswordAuthentication" + ) + .ToString() - Expect.equal passwordAuthentication "True" "password authentication was not correctly added" - } + Expect.equal passwordAuthentication "True" "password authentication was not correctly added" + } - test "Public key and path added" { - let deployment = - arm { - add_resources - [ - vm { - name "foo" - username "foo" - add_authorized_key "fooPath" "fooKey" - } - ] + test "Public key and path added" { + let deployment = arm { + add_resources [ + vm { + name "foo" + username "foo" + add_authorized_key "fooPath" "fooKey" } + ] + } - let json = deployment.Template |> Writer.toJson - let jobj = Newtonsoft.Json.Linq.JObject.Parse(json) + let json = deployment.Template |> Writer.toJson + let jobj = Newtonsoft.Json.Linq.JObject.Parse(json) - let linuxConfig = - jobj.SelectToken("resources[?(@.name=='foo')].properties.osProfile.linuxConfiguration") + let linuxConfig = + jobj.SelectToken("resources[?(@.name=='foo')].properties.osProfile.linuxConfiguration") - let keyData = - jobj - .SelectToken( - "resources[?(@.name=='foo')].properties.osProfile.linuxConfiguration.ssh.publicKeys[0].keyData" - ) - .ToString() + let keyData = + jobj + .SelectToken( + "resources[?(@.name=='foo')].properties.osProfile.linuxConfiguration.ssh.publicKeys[0].keyData" + ) + .ToString() - let path = - jobj - .SelectToken( - "resources[?(@.name=='foo')].properties.osProfile.linuxConfiguration.ssh.publicKeys[0].path" - ) - .ToString() + let path = + jobj + .SelectToken( + "resources[?(@.name=='foo')].properties.osProfile.linuxConfiguration.ssh.publicKeys[0].path" + ) + .ToString() - Expect.equal keyData "fooKey" "public keys were not correctly added" - Expect.equal path "fooPath" "path was not correctly added" - } + Expect.equal keyData "fooKey" "public keys were not correctly added" + Expect.equal path "fooPath" "path was not correctly added" + } - test "Public keys and paths added" { - let deployment = - arm { - add_resources - [ - vm { - name "foo" - username "foo" - add_authorized_keys [ ("fooPath", "fooKey"); ("fooPath1", "fooKey1") ] - } - ] + test "Public keys and paths added" { + let deployment = arm { + add_resources [ + vm { + name "foo" + username "foo" + add_authorized_keys [ ("fooPath", "fooKey"); ("fooPath1", "fooKey1") ] } + ] + } - let json = deployment.Template |> Writer.toJson - let jobj = Newtonsoft.Json.Linq.JObject.Parse(json) + let json = deployment.Template |> Writer.toJson + let jobj = Newtonsoft.Json.Linq.JObject.Parse(json) - let linuxConfig = - jobj.SelectToken("resources[?(@.name=='foo')].properties.osProfile.linuxConfiguration") + let linuxConfig = + jobj.SelectToken("resources[?(@.name=='foo')].properties.osProfile.linuxConfiguration") - let keyData = - jobj - .SelectToken( - "resources[?(@.name=='foo')].properties.osProfile.linuxConfiguration.ssh.publicKeys[0].keyData" - ) - .ToString() + let keyData = + jobj + .SelectToken( + "resources[?(@.name=='foo')].properties.osProfile.linuxConfiguration.ssh.publicKeys[0].keyData" + ) + .ToString() - let path = - jobj - .SelectToken( - "resources[?(@.name=='foo')].properties.osProfile.linuxConfiguration.ssh.publicKeys[0].path" - ) - .ToString() + let path = + jobj + .SelectToken( + "resources[?(@.name=='foo')].properties.osProfile.linuxConfiguration.ssh.publicKeys[0].path" + ) + .ToString() - Expect.equal keyData "fooKey" "public keys were not correctly added" - Expect.equal path "fooPath" "path was not correctly added" + Expect.equal keyData "fooKey" "public keys were not correctly added" + Expect.equal path "fooPath" "path was not correctly added" - let keyData = - jobj - .SelectToken( - "resources[?(@.name=='foo')].properties.osProfile.linuxConfiguration.ssh.publicKeys[1].keyData" - ) - .ToString() + let keyData = + jobj + .SelectToken( + "resources[?(@.name=='foo')].properties.osProfile.linuxConfiguration.ssh.publicKeys[1].keyData" + ) + .ToString() - let path = - jobj - .SelectToken( - "resources[?(@.name=='foo')].properties.osProfile.linuxConfiguration.ssh.publicKeys[1].path" - ) - .ToString() + let path = + jobj + .SelectToken( + "resources[?(@.name=='foo')].properties.osProfile.linuxConfiguration.ssh.publicKeys[1].path" + ) + .ToString() - Expect.equal keyData "fooKey1" "public keys were not correctly added" - Expect.equal path "fooPath1" "path was not correctly added" - } + Expect.equal keyData "fooKey1" "public keys were not correctly added" + Expect.equal path "fooPath1" "path was not correctly added" + } - test "Handles identity correctly" { - let machine = - arm { - add_resource ( - vm { - name "" - username "isaac" - } - ) - } - |> findAzureResources client.SerializationSettings - |> Seq.head + test "Handles identity correctly" { + let machine = + arm { + add_resource ( + vm { + name "" + username "isaac" + } + ) + } + |> findAzureResources client.SerializationSettings + |> Seq.head - Expect.isNull machine.Identity "Default managed identity should be null" + Expect.isNull machine.Identity "Default managed identity should be null" - let machine = - arm { - add_resource ( - vm { - system_identity - username "isaac" - } - ) - } - |> findAzureResources client.SerializationSettings - |> Seq.head + let machine = + arm { + add_resource ( + vm { + system_identity + username "isaac" + } + ) + } + |> findAzureResources client.SerializationSettings + |> Seq.head - Expect.equal - machine.Identity.Type - (Nullable ResourceIdentityType.SystemAssigned) - "Should have system identity" + Expect.equal + machine.Identity.Type + (Nullable ResourceIdentityType.SystemAssigned) + "Should have system identity" - Expect.isNull machine.Identity.UserAssignedIdentities "Should have no user assigned identities" + Expect.isNull machine.Identity.UserAssignedIdentities "Should have no user assigned identities" - let machine = - arm { - add_resource ( - vm { - system_identity - add_identity (createUserAssignedIdentity "test") - add_identity (createUserAssignedIdentity "test2") - username "isaac" - } - ) + let machine = + arm { + add_resource ( + vm { + system_identity + add_identity (createUserAssignedIdentity "test") + add_identity (createUserAssignedIdentity "test2") + username "isaac" + } + ) + } + |> findAzureResources client.SerializationSettings + |> Seq.head + + Expect.equal + machine.Identity.Type + (Nullable ResourceIdentityType.SystemAssignedUserAssigned) + "Should have system identity" + + Expect.sequenceEqual + (machine.Identity.UserAssignedIdentities |> Seq.map (fun r -> r.Key)) + [ + "[resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', 'test2')]" + "[resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', 'test')]" + ] + "Should have two user assigned identities" + } + + test "PrivateIpAllocation set correctly" { + let deployment = arm { + add_resources [ + vm { + name "foo" + username "foo" + + private_ip_allocation (PrivateIpAddress.StaticPrivateIp(Net.IPAddress((int64 0x2414188f)))) } - |> findAzureResources client.SerializationSettings - |> Seq.head - - Expect.equal - machine.Identity.Type - (Nullable ResourceIdentityType.SystemAssignedUserAssigned) - "Should have system identity" - - Expect.sequenceEqual - (machine.Identity.UserAssignedIdentities |> Seq.map (fun r -> r.Key)) - [ - "[resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', 'test2')]" - "[resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', 'test')]" - ] - "Should have two user assigned identities" + ] } - test "PrivateIpAllocation set correctly" { - let deployment = - arm { - add_resources - [ - vm { - name "foo" - username "foo" - - private_ip_allocation ( - PrivateIpAddress.StaticPrivateIp(Net.IPAddress((int64 0x2414188f))) - ) - } - ] - } + let json = deployment.Template |> Writer.toJson + let jobj = Newtonsoft.Json.Linq.JObject.Parse(json) + + let privateIpProps = jobj.SelectToken("resources[?(@.name=='foo-nic')]").ToString() + Expect.isNonEmpty privateIpProps "IP settings not found" - let json = deployment.Template |> Writer.toJson - let jobj = Newtonsoft.Json.Linq.JObject.Parse(json) + let methodToken = + jobj.SelectToken( + "resources[?(@.name=='foo-nic')].properties.ipConfigurations[0].properties.privateIPAllocationMethod" + ) - let privateIpProps = jobj.SelectToken("resources[?(@.name=='foo-nic')]").ToString() - Expect.isNonEmpty privateIpProps "IP settings not found" + let expectedMethodToken = "Static" + Expect.equal (methodToken.ToString()) (expectedMethodToken) "Allocation Method is wrong or missing" - let methodToken = - jobj.SelectToken( - "resources[?(@.name=='foo-nic')].properties.ipConfigurations[0].properties.privateIPAllocationMethod" + let ipToken = + jobj + .SelectToken( + "resources[?(@.name=='foo-nic')].properties.ipConfigurations[0].properties.privateIPAddress" ) + .ToString() - let expectedMethodToken = "Static" - Expect.equal (methodToken.ToString()) (expectedMethodToken) "Allocation Method is wrong or missing" + let expectedIpToken = "143.24.20.36" + Expect.equal (ipToken.ToString()) (expectedIpToken) "Static IP is wrong or missing" + } - let ipToken = - jobj - .SelectToken( - "resources[?(@.name=='foo-nic')].properties.ipConfigurations[0].properties.privateIPAddress" - ) - .ToString() + test "Supports multiple private IP configurations" { + let deployment = arm { + add_resources [ + vm { + name "foo" + username "foo" - let expectedIpToken = "143.24.20.36" - Expect.equal (ipToken.ToString()) (expectedIpToken) "Static IP is wrong or missing" - } + private_ip_allocation (PrivateIpAddress.StaticPrivateIp(Net.IPAddress.Parse("192.168.12.13"))) - test "Supports multiple private IP configurations" { - let deployment = - arm { - add_resources - [ - vm { - name "foo" - username "foo" - - private_ip_allocation ( - PrivateIpAddress.StaticPrivateIp(Net.IPAddress.Parse("192.168.12.13")) - ) - - add_ip_configurations - [ - ipConfig { - private_ip_allocation ( - PrivateIpAddress.StaticPrivateIp( - Net.IPAddress.Parse("192.168.12.14") - ) - ) - } - ] - } - ] + add_ip_configurations [ + ipConfig { + private_ip_allocation ( + PrivateIpAddress.StaticPrivateIp(Net.IPAddress.Parse("192.168.12.14")) + ) + } + ] } + ] + } - let jobj = Newtonsoft.Json.Linq.JObject.Parse(deployment.Template |> Writer.toJson) - let nic = jobj.SelectToken("resources[?(@.name=='foo-nic')]") - Expect.isNotNull nic "VM NIC not found" - - let ip0 = - nic.SelectToken "properties.ipConfigurations[0].properties.privateIPAddress" - - Expect.equal (ip0.ToString()) "192.168.12.13" "First static IP is wrong or missing" - - let ip1 = - nic.SelectToken "properties.ipConfigurations[1].properties.privateIPAddress" - - Expect.equal (ip1.ToString()) "192.168.12.14" "Second static IP is wrong or missing" - - let ip1SubnetId = - nic.SelectToken "properties.ipConfigurations[1].properties.subnet.id" + let jobj = Newtonsoft.Json.Linq.JObject.Parse(deployment.Template |> Writer.toJson) + let nic = jobj.SelectToken("resources[?(@.name=='foo-nic')]") + Expect.isNotNull nic "VM NIC not found" - Expect.equal - (ip1SubnetId.ToString()) - "[resourceId('Microsoft.Network/virtualNetworks/subnets', 'foo-vnet', 'foo-subnet')]" - "Second subnet is wrong or missing" - } + let ip0 = + nic.SelectToken "properties.ipConfigurations[0].properties.privateIPAddress" - test "Supports adding multiple private IP addresses" { - let deployment = - arm { - add_resources - [ - vm { - name "foo" - username "foo" - public_ip None - - add_ip_configurations - [ - ipConfig { - private_ip_allocation ( - PrivateIpAddress.StaticPrivateIp( - Net.IPAddress.Parse("192.168.12.13") - ) - ) - } - ] - } - ] - } + Expect.equal (ip0.ToString()) "192.168.12.13" "First static IP is wrong or missing" - let jobj = Newtonsoft.Json.Linq.JObject.Parse(deployment.Template |> Writer.toJson) - let nic = jobj.SelectToken("resources[?(@.name=='foo-nic')]").ToString() - Expect.isNonEmpty nic "NIC not found" + let ip1 = + nic.SelectToken "properties.ipConfigurations[1].properties.privateIPAddress" - let nicProps = jobj.SelectToken("resources[?(@.name=='foo-nic')].properties") + Expect.equal (ip1.ToString()) "192.168.12.14" "Second static IP is wrong or missing" - Expect.isNotNull nicProps "NIC properties not found" + let ip1SubnetId = + nic.SelectToken "properties.ipConfigurations[1].properties.subnet.id" - let ip0Token = - nicProps.SelectToken "ipConfigurations[1].properties.privateIPAddress" + Expect.equal + (ip1SubnetId.ToString()) + "[resourceId('Microsoft.Network/virtualNetworks/subnets', 'foo-vnet', 'foo-subnet')]" + "Second subnet is wrong or missing" + } - Expect.equal (ip0Token.ToString()) "192.168.12.13" "Static IP is wrong or missing" - } + test "Supports adding multiple private IP addresses" { + let deployment = arm { + add_resources [ + vm { + name "foo" + username "foo" + public_ip None - test "Builds multiple NICs when attaching to multiple subnets" { - let deployment = - arm { - add_resources - [ - vm { - name "foo" - username "foo" - public_ip None - - add_ip_configurations - [ - ipConfig { - private_ip_allocation ( - PrivateIpAddress.StaticPrivateIp( - Net.IPAddress.Parse("192.168.12.13") - ) - ) - - subnet_name (ResourceName "another-subnet") - } - ] - } - ] + add_ip_configurations [ + ipConfig { + private_ip_allocation ( + PrivateIpAddress.StaticPrivateIp(Net.IPAddress.Parse("192.168.12.13")) + ) + } + ] } + ] + } - let jobj = Newtonsoft.Json.Linq.JObject.Parse(deployment.Template |> Writer.toJson) + let jobj = Newtonsoft.Json.Linq.JObject.Parse(deployment.Template |> Writer.toJson) + let nic = jobj.SelectToken("resources[?(@.name=='foo-nic')]").ToString() + Expect.isNonEmpty nic "NIC not found" - let vm = - jobj.SelectToken("resources[?(@.type=='Microsoft.Compute/virtualMachines')]") + let nicProps = jobj.SelectToken("resources[?(@.name=='foo-nic')].properties") - let vmProps = vm.["properties"] - let vmNics = vmProps.["networkProfile"].["networkInterfaces"] - Expect.hasLength vmNics 2 "Emitted VM should have two network interfaces" - let vmDepends = vm.["dependsOn"] - Expect.hasLength vmDepends 2 "Emitted VM should have two dependencies" + Expect.isNotNull nicProps "NIC properties not found" - let nics = - jobj.SelectTokens("resources[?(@.type=='Microsoft.Network/networkInterfaces')]") + let ip0Token = + nicProps.SelectToken "ipConfigurations[1].properties.privateIPAddress" - Expect.hasLength nics 2 "Should have emitted two network interfaces" - let secondNic = jobj.SelectToken("resources[?(@.name=='foo-nic-another-subnet')]") - Expect.isNotNull secondNic "Second NIC not found" + Expect.equal (ip0Token.ToString()) "192.168.12.13" "Static IP is wrong or missing" + } - let secondNicProps = secondNic["properties"] - Expect.isNotNull secondNicProps "Second NIC properties not found" + test "Builds multiple NICs when attaching to multiple subnets" { + let deployment = arm { + add_resources [ + vm { + name "foo" + username "foo" + public_ip None - let secondNicIp = - secondNicProps.SelectToken "ipConfigurations[0].properties.privateIPAddress" + add_ip_configurations [ + ipConfig { + private_ip_allocation ( + PrivateIpAddress.StaticPrivateIp(Net.IPAddress.Parse("192.168.12.13")) + ) - Expect.equal (secondNicIp.ToString()) "192.168.12.13" "Static IP is wrong or missing" + subnet_name (ResourceName "another-subnet") + } + ] + } + ] } - test "IP forwarding set for first NIC only" { - let deployment = - arm { - add_resources - [ - vm { - name "foo" - username "foo" - ip_forwarding Enabled - - add_ip_configurations [ ipConfig { subnet_name (ResourceName "another-subnet") } ] - } - ] - } + let jobj = Newtonsoft.Json.Linq.JObject.Parse(deployment.Template |> Writer.toJson) - let jobj = Newtonsoft.Json.Linq.JObject.Parse(deployment.Template |> Writer.toJson) - let firstNicProps = jobj.SelectToken("resources[?(@.name=='foo-nic')].properties") + let vm = + jobj.SelectToken("resources[?(@.type=='Microsoft.Compute/virtualMachines')]") - Expect.equal - firstNicProps.["enableIPForwarding"] - (JValue true) - "First NIC should have IP forwarding enabled" + let vmProps = vm.["properties"] + let vmNics = vmProps.["networkProfile"].["networkInterfaces"] + Expect.hasLength vmNics 2 "Emitted VM should have two network interfaces" + let vmDepends = vm.["dependsOn"] + Expect.hasLength vmDepends 2 "Emitted VM should have two dependencies" - let secondNicProps = - jobj.SelectToken("resources[?(@.name=='foo-nic-another-subnet')].properties") + let nics = + jobj.SelectTokens("resources[?(@.type=='Microsoft.Network/networkInterfaces')]") - Expect.isNull secondNicProps.["enableIPForwarding"] "Second NIC should not have IP forwarding" - } + Expect.hasLength nics 2 "Should have emitted two network interfaces" + let secondNic = jobj.SelectToken("resources[?(@.name=='foo-nic-another-subnet')]") + Expect.isNotNull secondNic "Second NIC not found" - test "Accelerated networking set for all NICs" { - let deployment = - arm { - add_resources - [ - vm { - name "foo" - username "foo" - vm_size Standard_D2s_v5 - accelerated_networking Enabled - - add_ip_configurations [ ipConfig { subnet_name (ResourceName "another-subnet") } ] - } - ] - } + let secondNicProps = secondNic["properties"] + Expect.isNotNull secondNicProps "Second NIC properties not found" - let jobj = Newtonsoft.Json.Linq.JObject.Parse(deployment.Template |> Writer.toJson) - let firstNicProps = jobj.SelectToken("resources[?(@.name=='foo-nic')].properties") + let secondNicIp = + secondNicProps.SelectToken "ipConfigurations[0].properties.privateIPAddress" - Expect.equal - firstNicProps.["enableAcceleratedNetworking"] - (JValue true) - "First NIC should have accelerated networking enabled" + Expect.equal (secondNicIp.ToString()) "192.168.12.13" "Static IP is wrong or missing" + } - let secondNicProps = - jobj.SelectToken("resources[?(@.name=='foo-nic-another-subnet')].properties") + test "IP forwarding set for first NIC only" { + let deployment = arm { + add_resources [ + vm { + name "foo" + username "foo" + ip_forwarding Enabled - Expect.equal - secondNicProps.["enableAcceleratedNetworking"] - (JValue true) - "Second NIC should have accelerated networking enabled" + add_ip_configurations [ ipConfig { subnet_name (ResourceName "another-subnet") } ] + } + ] } - test "Accelerated networking not allowed on A-series VM" { - Expect.throws - (fun _ -> - let _ = - arm { - add_resources - [ - vm { - name "foo" - username "foo" - vm_size Basic_A0 - accelerated_networking Enabled - - add_ip_configurations - [ ipConfig { subnet_name (ResourceName "another-subnet") } ] - } - ] - } + let jobj = Newtonsoft.Json.Linq.JObject.Parse(deployment.Template |> Writer.toJson) + let firstNicProps = jobj.SelectToken("resources[?(@.name=='foo-nic')].properties") - ()) - "Expected failure using accelerated networking with default VM size." - } + Expect.equal + firstNicProps.["enableIPForwarding"] + (JValue true) + "First NIC should have IP forwarding enabled" - test "Can attach to NSG" { - let vmName = "fooVm" - let myNsg = nsg { name "testNsg" } + let secondNicProps = + jobj.SelectToken("resources[?(@.name=='foo-nic-another-subnet')].properties") - let myVm = + Expect.isNull secondNicProps.["enableIPForwarding"] "Second NIC should not have IP forwarding" + } + + test "Accelerated networking set for all NICs" { + let deployment = arm { + add_resources [ vm { - name vmName + name "foo" username "foo" - network_security_group myNsg + vm_size Standard_D2s_v5 + accelerated_networking Enabled + + add_ip_configurations [ ipConfig { subnet_name (ResourceName "another-subnet") } ] } + ] + } - let deployment = arm { add_resources [ myNsg; myVm ] } - let json = deployment.Template |> Writer.toJson - let jobj = Newtonsoft.Json.Linq.JObject.Parse json + let jobj = Newtonsoft.Json.Linq.JObject.Parse(deployment.Template |> Writer.toJson) + let firstNicProps = jobj.SelectToken("resources[?(@.name=='foo-nic')].properties") - let vmNsgId = - jobj - .SelectToken($"resources[?(@.name=='{vmName}-nic')].properties.networkSecurityGroup.id") - .ToString() + Expect.equal + firstNicProps.["enableAcceleratedNetworking"] + (JValue true) + "First NIC should have accelerated networking enabled" - Expect.isFalse (String.IsNullOrEmpty vmNsgId) "NSG not attached" - } + let secondNicProps = + jobj.SelectToken("resources[?(@.name=='foo-nic-another-subnet')].properties") - test "Link new VM to existing vnet" { - let template = - let myVm = - vm { - name "myvm" - username "azureuser" - link_to_unmanaged_vnet "myvnet" - subnet_name "default" - } + Expect.equal + secondNicProps.["enableAcceleratedNetworking"] + (JValue true) + "Second NIC should have accelerated networking enabled" + } - arm { add_resource myVm } + test "Accelerated networking not allowed on A-series VM" { + Expect.throws + (fun _ -> + let _ = arm { + add_resources [ + vm { + name "foo" + username "foo" + vm_size Basic_A0 + accelerated_networking Enabled - let jobj = Newtonsoft.Json.Linq.JObject.Parse(template.Template |> Writer.toJson) - let vmResource = jobj.SelectToken("resources[?(@.name=='myvm')]") - let vmDependsOn = (vmResource.["dependsOn"] :?> Newtonsoft.Json.Linq.JArray) - Expect.hasLength vmDependsOn 1 "Incorrect number of VM dependencies" + add_ip_configurations [ ipConfig { subnet_name (ResourceName "another-subnet") } ] + } + ] + } - Expect.sequenceEqual - vmDependsOn - (Newtonsoft.Json.Linq.JArray [ "[resourceId('Microsoft.Network/networkInterfaces', 'myvm-nic')]" ]) - $"VM should only depend on its NIC, not also the vnet: {vmDependsOn}" + ()) + "Expected failure using accelerated networking with default VM size." + } - let nicResource = jobj.SelectToken("resources[?(@.name=='myvm-nic')]") - let nicDependsOn = (nicResource.["dependsOn"] :?> Newtonsoft.Json.Linq.JArray) - Expect.hasLength nicDependsOn 1 "NIC should only have 1 dependency - the public IP" + test "Can attach to NSG" { + let vmName = "fooVm" + let myNsg = nsg { name "testNsg" } - Expect.sequenceEqual - nicDependsOn - (Newtonsoft.Json.Linq.JArray [ "[resourceId('Microsoft.Network/publicIPAddresses', 'myvm-ip')]" ]) - $"NIC should only depend on its public IP, not also the vnet: {nicDependsOn}" + let myVm = vm { + name vmName + username "foo" + network_security_group myNsg } - test "Link new VM to existing vnet in different resource group" { - let myVnet = - Arm.Network.virtualNetworks.resourceId ("myvnet", groupName = "other-group") + let deployment = arm { add_resources [ myNsg; myVm ] } + let json = deployment.Template |> Writer.toJson + let jobj = Newtonsoft.Json.Linq.JObject.Parse json + + let vmNsgId = + jobj + .SelectToken($"resources[?(@.name=='{vmName}-nic')].properties.networkSecurityGroup.id") + .ToString() + + Expect.isFalse (String.IsNullOrEmpty vmNsgId) "NSG not attached" + } + + test "Link new VM to existing vnet" { + let template = + let myVm = vm { + name "myvm" + username "azureuser" + link_to_unmanaged_vnet "myvnet" + subnet_name "default" + } - let template = - let myVm = - vm { - name "myvm" - username "azureuser" - link_to_unmanaged_vnet myVnet - subnet_name "default" - } + arm { add_resource myVm } + + let jobj = Newtonsoft.Json.Linq.JObject.Parse(template.Template |> Writer.toJson) + let vmResource = jobj.SelectToken("resources[?(@.name=='myvm')]") + let vmDependsOn = (vmResource.["dependsOn"] :?> Newtonsoft.Json.Linq.JArray) + Expect.hasLength vmDependsOn 1 "Incorrect number of VM dependencies" + + Expect.sequenceEqual + vmDependsOn + (Newtonsoft.Json.Linq.JArray [ "[resourceId('Microsoft.Network/networkInterfaces', 'myvm-nic')]" ]) + $"VM should only depend on its NIC, not also the vnet: {vmDependsOn}" + + let nicResource = jobj.SelectToken("resources[?(@.name=='myvm-nic')]") + let nicDependsOn = (nicResource.["dependsOn"] :?> Newtonsoft.Json.Linq.JArray) + Expect.hasLength nicDependsOn 1 "NIC should only have 1 dependency - the public IP" + + Expect.sequenceEqual + nicDependsOn + (Newtonsoft.Json.Linq.JArray [ "[resourceId('Microsoft.Network/publicIPAddresses', 'myvm-ip')]" ]) + $"NIC should only depend on its public IP, not also the vnet: {nicDependsOn}" + } + + test "Link new VM to existing vnet in different resource group" { + let myVnet = + Arm.Network.virtualNetworks.resourceId ("myvnet", groupName = "other-group") + + let template = + let myVm = vm { + name "myvm" + username "azureuser" + link_to_unmanaged_vnet myVnet + subnet_name "default" + } - arm { add_resource myVm } + arm { add_resource myVm } + + let jobj = Newtonsoft.Json.Linq.JObject.Parse(template.Template |> Writer.toJson) + let vmResource = jobj.SelectToken("resources[?(@.name=='myvm')]") + let vmDependsOn = (vmResource.["dependsOn"] :?> Newtonsoft.Json.Linq.JArray) + Expect.hasLength vmDependsOn 1 "Incorrect number of VM dependencies" + + Expect.sequenceEqual + vmDependsOn + (Newtonsoft.Json.Linq.JArray [ "[resourceId('Microsoft.Network/networkInterfaces', 'myvm-nic')]" ]) + $"VM should only depend on its NIC, not also the vnet: {vmDependsOn}" + + let nicResource = jobj.SelectToken("resources[?(@.name=='myvm-nic')]") + let nicDependsOn = (nicResource.["dependsOn"] :?> Newtonsoft.Json.Linq.JArray) + Expect.hasLength nicDependsOn 1 "NIC should only have 1 dependency - the public IP" + + let nicSubnetId = + nicResource + .SelectToken("properties.ipConfigurations[0].properties.subnet.id") + .ToString() + + Expect.equal + nicSubnetId + "[resourceId('other-group', 'Microsoft.Network/virtualNetworks/subnets', 'myvnet', 'default')]" + "NIC subnet should repect resource group specified in VM VNet" + + Expect.sequenceEqual + nicDependsOn + (Newtonsoft.Json.Linq.JArray [ "[resourceId('Microsoft.Network/publicIPAddresses', 'myvm-ip')]" ]) + $"NIC should only depend on its public IP, not also the vnet: {nicDependsOn}" + } + + test "Enables Azure AD SSH access on Linux virtual machine" { + let template = + let myVm = vm { + name "myvm" + username "ubuntu" + vm_size Standard_B1s + operating_system UbuntuServer_1804LTS + system_identity + aad_ssh_login Enabled + } - let jobj = Newtonsoft.Json.Linq.JObject.Parse(template.Template |> Writer.toJson) - let vmResource = jobj.SelectToken("resources[?(@.name=='myvm')]") - let vmDependsOn = (vmResource.["dependsOn"] :?> Newtonsoft.Json.Linq.JArray) - Expect.hasLength vmDependsOn 1 "Incorrect number of VM dependencies" + arm { add_resource myVm } - Expect.sequenceEqual - vmDependsOn - (Newtonsoft.Json.Linq.JArray [ "[resourceId('Microsoft.Network/networkInterfaces', 'myvm-nic')]" ]) - $"VM should only depend on its NIC, not also the vnet: {vmDependsOn}" + let jobj = Newtonsoft.Json.Linq.JObject.Parse(template.Template |> Writer.toJson) - let nicResource = jobj.SelectToken("resources[?(@.name=='myvm-nic')]") - let nicDependsOn = (nicResource.["dependsOn"] :?> Newtonsoft.Json.Linq.JArray) - Expect.hasLength nicDependsOn 1 "NIC should only have 1 dependency - the public IP" + let extensionResource = + jobj.SelectToken("resources[?(@.name=='myvm/AADSSHLoginForLinux')]") - let nicSubnetId = - nicResource - .SelectToken("properties.ipConfigurations[0].properties.subnet.id") - .ToString() + Expect.sequenceEqual + (extensionResource.["dependsOn"] :?> Newtonsoft.Json.Linq.JArray) + (Newtonsoft.Json.Linq.JArray [ "[resourceId('Microsoft.Compute/virtualMachines', 'myvm')]" ]) + $"Missing or incorrect extension dependency." - Expect.equal - nicSubnetId - "[resourceId('other-group', 'Microsoft.Network/virtualNetworks/subnets', 'myvnet', 'default')]" - "NIC subnet should repect resource group specified in VM VNet" + Expect.equal + (string extensionResource.["properties"].["type"]) + "AADSSHLoginForLinux" + $"Missing or incorrect extension type." - Expect.sequenceEqual - nicDependsOn - (Newtonsoft.Json.Linq.JArray [ "[resourceId('Microsoft.Network/publicIPAddresses', 'myvm-ip')]" ]) - $"NIC should only depend on its public IP, not also the vnet: {nicDependsOn}" - } + Expect.equal + (string extensionResource.["properties"].["typeHandlerVersion"]) + "1.0" + $"Missing or incorrect extension typeHandlerVersion." + } - test "Enables Azure AD SSH access on Linux virtual machine" { - let template = - let myVm = + test "throws an error if you set priority more than once" { + let createVm () = + arm { + add_resource ( vm { - name "myvm" - username "ubuntu" - vm_size Standard_B1s - operating_system UbuntuServer_1804LTS - system_identity - aad_ssh_login Enabled + name "foo" + username "foo" + priority Regular + priority Regular } + ) + } + |> ignore - arm { add_resource myVm } + Expect.throws createVm "priority set more than once" + } - let jobj = Newtonsoft.Json.Linq.JObject.Parse(template.Template |> Writer.toJson) + 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 - let extensionResource = - jobj.SelectToken("resources[?(@.name=='myvm/AADSSHLoginForLinux')]") + Expect.throws createVm "spot_instance set more than once" + } - Expect.sequenceEqual - (extensionResource.["dependsOn"] :?> Newtonsoft.Json.Linq.JArray) - (Newtonsoft.Json.Linq.JArray [ "[resourceId('Microsoft.Compute/virtualMachines', 'myvm')]" ]) - $"Missing or incorrect extension dependency." + 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.equal - (string extensionResource.["properties"].["type"]) - "AADSSHLoginForLinux" - $"Missing or incorrect extension type." + Expect.throws createVm "priority and spot_instance both set" + } - Expect.equal - (string extensionResource.["properties"].["typeHandlerVersion"]) - "1.0" - $"Missing or incorrect extension typeHandlerVersion." - } + test "Creates zonal VM and public IP" { + let deployment = arm { + location Location.WestUS3 - 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 - } - ) + add_resources [ + vm { + name "zonal-vm" + vm_size Standard_B1ms + username "azureuser" + add_availability_zone "2" } - |> 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" - } + let jobj = deployment.Template |> Writer.toJson |> JObject.Parse + let vmZones = jobj.SelectToken "resources[?(@.name=='zonal-vm')].zones" :?> JArray + Expect.hasLength vmZones 1 "VM s have a zone assignment." + Expect.equal (string vmZones.[0]) "2" "VM zone should be '2'" - 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 + let publicIpZone = + jobj.SelectToken "resources[?(@.name=='zonal-vm-ip')].zones" :?> JArray - Expect.throws createVm "priority and spot_instance both set" - } + Expect.hasLength publicIpZone 1 "Public IP should have a zone assignment." + Expect.equal (string publicIpZone.[0]) "2" "Public IP zone should be '2'" + } + test "Creates VM with Ultra disk and zone" { + let deployment = arm { + location Location.WestUS3 - test "Creates zonal VM and public IP" { - let deployment = - arm { - location Location.WestUS3 - - add_resources - [ - vm { - name "zonal-vm" - vm_size Standard_B1ms - username "azureuser" - add_availability_zone "2" - } - ] + add_resources [ + vm { + name "ultra-disk-vm" + vm_size Standard_D2s_v5 + username "azureuser" + add_availability_zone "2" + add_disk 4096 UltraSSD_LRS } + ] + } - let jobj = deployment.Template |> Writer.toJson |> JObject.Parse - let vmZones = jobj.SelectToken "resources[?(@.name=='zonal-vm')].zones" :?> JArray - Expect.hasLength vmZones 1 "VM s have a zone assignment." - Expect.equal (string vmZones.[0]) "2" "VM zone should be '2'" + let jobj = deployment.Template |> Writer.toJson |> JObject.Parse + let vm = jobj.SelectToken "resources[?(@.name=='ultra-disk-vm')]" + let vmProps = vm.["properties"] + let ultraSsdEnabled = vmProps.SelectToken "additionalCapabilities.ultraSSDEnabled" + Expect.equal ultraSsdEnabled (JValue true) "Ultra SSD capability not enabled on VM" - let publicIpZone = - jobj.SelectToken "resources[?(@.name=='zonal-vm-ip')].zones" :?> JArray + let dataDiskType = + vmProps.SelectToken "storageProfile.dataDisks[0].managedDisk.storageAccountType" - Expect.hasLength publicIpZone 1 "Public IP should have a zone assignment." - Expect.equal (string publicIpZone.[0]) "2" "Public IP zone should be '2'" - } - test "Creates VM with Ultra disk and zone" { - let deployment = - arm { - location Location.WestUS3 - - add_resources - [ - vm { - name "ultra-disk-vm" - vm_size Standard_D2s_v5 - username "azureuser" - add_availability_zone "2" - add_disk 4096 UltraSSD_LRS - } - ] - } + Expect.equal dataDiskType (JValue "UltraSSD_LRS") "Data disk not set to Ultra disk type" + } - let jobj = deployment.Template |> Writer.toJson |> JObject.Parse - let vm = jobj.SelectToken "resources[?(@.name=='ultra-disk-vm')]" - let vmProps = vm.["properties"] - let ultraSsdEnabled = vmProps.SelectToken "additionalCapabilities.ultraSSDEnabled" - Expect.equal ultraSsdEnabled (JValue true) "Ultra SSD capability not enabled on VM" + test "Creates VM and attaches newly imported OS disk" { + let deployment = arm { + location Location.EastUS - let dataDiskType = - vmProps.SelectToken "storageProfile.dataDisks[0].managedDisk.storageAccountType" + add_resources [ + disk { + name "imported-disk-image" + sku Vm.DiskType.Premium_LRS + os_type Linux - Expect.equal dataDiskType (JValue "UltraSSD_LRS") "Data disk not set to Ultra disk type" + import + (Uri + "https://rxw1n3qxt54dnvfen1gnza5n.blob.core.windows.net/vhds/Ubuntu2004WithJava_20230213141703.vhd") + (ResourceId.create ( + Arm.Storage.storageAccounts, + ResourceName "rxw1n3qxt54dnvfen1gnza5n", + "IT_farmer-imgbldr_Ubuntu2004WithJava_aea5facc-e1b5-47de-aa5b-2c6aafe2161d" + )) + } + vm { + name "attached-os-disk-vm" + vm_size Standard_B1ms + username "azureuser" + attach_os_disk Linux (Arm.Disk.disks.resourceId "imported-disk-image") + } + ] } - test "Creates VM and attaches newly imported OS disk" { - let deployment = - arm { - location Location.EastUS - - add_resources - [ - disk { - name "imported-disk-image" - sku Vm.DiskType.Premium_LRS - os_type Linux - - import - (Uri - "https://rxw1n3qxt54dnvfen1gnza5n.blob.core.windows.net/vhds/Ubuntu2004WithJava_20230213141703.vhd") - (ResourceId.create ( - Arm.Storage.storageAccounts, - ResourceName "rxw1n3qxt54dnvfen1gnza5n", - "IT_farmer-imgbldr_Ubuntu2004WithJava_aea5facc-e1b5-47de-aa5b-2c6aafe2161d" - )) - } - vm { - name "attached-os-disk-vm" - vm_size Standard_B1ms - username "azureuser" - attach_os_disk Linux (Arm.Disk.disks.resourceId "imported-disk-image") - } - ] - } + let jobj = deployment.Template |> Writer.toJson |> JObject.Parse + let vm = jobj.SelectToken "resources[?(@.name=='attached-os-disk-vm')]" + let dependencies = vm.["dependsOn"] - let jobj = deployment.Template |> Writer.toJson |> JObject.Parse - let vm = jobj.SelectToken "resources[?(@.name=='attached-os-disk-vm')]" - let dependencies = vm.["dependsOn"] + Expect.contains + dependencies + (JValue "[resourceId('Microsoft.Compute/disks', 'imported-disk-image')]") + "Missing imported-disk-image dependency" - Expect.contains - dependencies - (JValue "[resourceId('Microsoft.Compute/disks', 'imported-disk-image')]") - "Missing imported-disk-image dependency" + let vmOsProfile = vm.SelectToken "properties.osProfile" + Expect.isNull vmOsProfile "The osProfile should not be set when attaching an OS disk" - let vmOsProfile = vm.SelectToken "properties.osProfile" - Expect.isNull vmOsProfile "The osProfile should not be set when attaching an OS disk" + let vmOsDisk = vm.SelectToken "properties.storageProfile.osDisk" + Expect.isNotNull vmOsDisk "VM missing OS disk" + Expect.equal vmOsDisk.["createOption"] (JValue "Attach") "OS disk createOption incorrect" - let vmOsDisk = vm.SelectToken "properties.storageProfile.osDisk" - Expect.isNotNull vmOsDisk "VM missing OS disk" - Expect.equal vmOsDisk.["createOption"] (JValue "Attach") "OS disk createOption incorrect" + Expect.equal vmOsDisk.["name"] (JValue "imported-disk-image") "OS disk name should match attached disk name" - Expect.equal - vmOsDisk.["name"] - (JValue "imported-disk-image") - "OS disk name should match attached disk name" + Expect.equal vmOsDisk.["osType"] (JValue "Linux") "OS disk osType incorrect" - Expect.equal vmOsDisk.["osType"] (JValue "Linux") "OS disk osType incorrect" + Expect.equal + (vm.SelectToken "properties.storageProfile.osDisk.managedDisk.id") + (JValue "[resourceId('Microsoft.Compute/disks', 'imported-disk-image')]") + "Incorrect reference to managed disk" + } - Expect.equal - (vm.SelectToken "properties.storageProfile.osDisk.managedDisk.id") - (JValue "[resourceId('Microsoft.Compute/disks', 'imported-disk-image')]") - "Incorrect reference to managed disk" - } + test "Creates VM and attaches existing OS disk" { + let deployment = arm { + location Location.EastUS - test "Creates VM and attaches existing OS disk" { - let deployment = - arm { - location Location.EastUS - - add_resources - [ - vm { - name "attached-os-disk-vm" - vm_size Standard_B1ms - username "azureuser" - attach_existing_os_disk Linux (Arm.Disk.disks.resourceId "existing-os-disk") - } - ] + add_resources [ + vm { + name "attached-os-disk-vm" + vm_size Standard_B1ms + username "azureuser" + attach_existing_os_disk Linux (Arm.Disk.disks.resourceId "existing-os-disk") } + ] + } - let jobj = deployment.Template |> Writer.toJson |> JObject.Parse - let vm = jobj.SelectToken "resources[?(@.name=='attached-os-disk-vm')]" - Expect.hasLength vm.["dependsOn"] 1 "Should only have dependency for NIC when attaching existing disk" + let jobj = deployment.Template |> Writer.toJson |> JObject.Parse + let vm = jobj.SelectToken "resources[?(@.name=='attached-os-disk-vm')]" + Expect.hasLength vm.["dependsOn"] 1 "Should only have dependency for NIC when attaching existing disk" + } + + test "Creates VM and attaches newly created data disks" { + let disk0 = disk { + name "ultra-disk-0" + sku Vm.DiskType.UltraSSD_LRS + os_type Linux + create_empty 1024 + add_availability_zone "1" } - test "Creates VM and attaches newly created data disks" { - let disk0 = - disk { - name "ultra-disk-0" - sku Vm.DiskType.UltraSSD_LRS - os_type Linux - create_empty 1024 - add_availability_zone "1" - } + let disk1 = disk { + name "standard-disk-1" + sku Vm.DiskType.Standard_LRS + os_type Linux + create_empty 1024 + add_availability_zone "1" + } - let disk1 = - disk { - name "standard-disk-1" - sku Vm.DiskType.Standard_LRS - os_type Linux - create_empty 1024 - add_availability_zone "1" - } + let deployment = arm { + location Location.EastUS - let deployment = - arm { - location Location.EastUS - - add_resources - [ - disk0 - disk1 - vm { - name "attached-data-disk-vm" - vm_size Standard_B1ms - operating_system UbuntuServer_2204LTS - username "azureuser" - add_availability_zone "1" - attach_data_disk disk0 - attach_data_disk disk1 - } - ] + add_resources [ + disk0 + disk1 + vm { + name "attached-data-disk-vm" + vm_size Standard_B1ms + operating_system UbuntuServer_2204LTS + username "azureuser" + add_availability_zone "1" + attach_data_disk disk0 + attach_data_disk disk1 } + ] + } - let jobj = deployment.Template |> Writer.toJson |> JObject.Parse - let vm = jobj.SelectToken "resources[?(@.name=='attached-data-disk-vm')]" - let dependencies = vm.["dependsOn"] + let jobj = deployment.Template |> Writer.toJson |> JObject.Parse + let vm = jobj.SelectToken "resources[?(@.name=='attached-data-disk-vm')]" + let dependencies = vm.["dependsOn"] - Expect.contains - dependencies - (JValue "[resourceId('Microsoft.Compute/disks', 'ultra-disk-0')]") - "Missing disk-0" + Expect.contains + dependencies + (JValue "[resourceId('Microsoft.Compute/disks', 'ultra-disk-0')]") + "Missing disk-0" - Expect.contains - dependencies - (JValue "[resourceId('Microsoft.Compute/disks', 'standard-disk-1')]") - "Missing disk-1" + Expect.contains + dependencies + (JValue "[resourceId('Microsoft.Compute/disks', 'standard-disk-1')]") + "Missing disk-1" - let dataDisks = vm.SelectToken "properties.storageProfile.dataDisks" - Expect.hasLength dataDisks 2 "Incorrect number of data disks on VM" + let dataDisks = vm.SelectToken "properties.storageProfile.dataDisks" + Expect.hasLength dataDisks 2 "Incorrect number of data disks on VM" - for disk in dataDisks do - Expect.equal disk.["createOption"] (JValue "Attach") "Incorrect createOption" + for disk in dataDisks do + Expect.equal disk.["createOption"] (JValue "Attach") "Incorrect createOption" - let firstDisk = dataDisks.[0] + let firstDisk = dataDisks.[0] - Expect.equal - (firstDisk.SelectToken "managedDisk.id") - (JValue "[resourceId('Microsoft.Compute/disks', 'ultra-disk-0')]") - "Incorrect managedDisk.id" + Expect.equal + (firstDisk.SelectToken "managedDisk.id") + (JValue "[resourceId('Microsoft.Compute/disks', 'ultra-disk-0')]") + "Incorrect managedDisk.id" - Expect.equal - (firstDisk.SelectToken "name") - (JValue "ultra-disk-0") - "Ultra disk name should match name from resourceId" + Expect.equal + (firstDisk.SelectToken "name") + (JValue "ultra-disk-0") + "Ultra disk name should match name from resourceId" - let secondDisk = dataDisks.[1] + let secondDisk = dataDisks.[1] - Expect.equal - (secondDisk.SelectToken "managedDisk.id") - (JValue "[resourceId('Microsoft.Compute/disks', 'standard-disk-1')]") - "Incorrect managedDisk.id" + Expect.equal + (secondDisk.SelectToken "managedDisk.id") + (JValue "[resourceId('Microsoft.Compute/disks', 'standard-disk-1')]") + "Incorrect managedDisk.id" - Expect.equal - (secondDisk.SelectToken "name") - (JValue "standard-disk-1") - "Standard disk name should match name from resourceId" - } + Expect.equal + (secondDisk.SelectToken "name") + (JValue "standard-disk-1") + "Standard disk name should match name from resourceId" + } - ] + ] diff --git a/src/Tests/VirtualNetworkGateway.fs b/src/Tests/VirtualNetworkGateway.fs index 83b289dee..e477aab37 100644 --- a/src/Tests/VirtualNetworkGateway.fs +++ b/src/Tests/VirtualNetworkGateway.fs @@ -11,203 +11,201 @@ open Farmer.Arm.Network let tests = - testList - "VirtualNetworkGateway" - [ - test "Can create a basic VirtualNetworkGateway" { - let b = gateway { name "gateway" } :> IBuilder - let resources = b.BuildResources Location.WestEurope - - let gw = - resources - |> List.pick (function - | :? VirtualNetworkGateway as v -> Some v - | _ -> None) - - Expect.equal gw.Name.Value "gateway" "Incorrect Resource Name" - Expect.equal gw.Location Location.WestEurope "Incorrect location" - Expect.isNone gw.VpnClientConfiguration "Incorrect VpnClientConfiguration" - } - - test "Can create a VirtualNetworkGateway attached to a vnet" { - let b = - gateway { - name "gateway" - vnet "vnet" + testList "VirtualNetworkGateway" [ + test "Can create a basic VirtualNetworkGateway" { + let b = gateway { name "gateway" } :> IBuilder + let resources = b.BuildResources Location.WestEurope + + let gw = + resources + |> List.pick (function + | :? VirtualNetworkGateway as v -> Some v + | _ -> None) + + Expect.equal gw.Name.Value "gateway" "Incorrect Resource Name" + Expect.equal gw.Location Location.WestEurope "Incorrect location" + Expect.isNone gw.VpnClientConfiguration "Incorrect VpnClientConfiguration" + } + + test "Can create a VirtualNetworkGateway attached to a vnet" { + let b = + gateway { + name "gateway" + vnet "vnet" + } + :> IBuilder + + let resources = b.BuildResources Location.WestEurope + + let gw = + resources + |> List.pick (function + | :? VirtualNetworkGateway as v -> Some v + | _ -> None) + + Expect.equal gw.VirtualNetwork.Value "vnet" "Incorrect Virtual network" + } + + test "Can create a VirtualNetworkGateway with VpnClientConfiguration" { + let b = + gateway { + name "gateway" + + vpn_client ( + vpnclient { + add_address_pool "10.31.0.0/16" + add_address_pool "10.32.1.0/24" + } + ) + } + :> IBuilder + + let resources = b.BuildResources Location.WestEurope + + let gw = + resources + |> List.pick (function + | :? VirtualNetworkGateway as v -> Some v + | _ -> None) + + Expect.isSome gw.VpnClientConfiguration "Incorrect VpnClientConfiguration" + + Expect.equal + gw.VpnClientConfiguration.Value.ClientAddressPools + [ + { + Address = Net.IPAddress.Parse "10.31.0.0" + Prefix = 16 } - :> IBuilder - - let resources = b.BuildResources Location.WestEurope - - let gw = - resources - |> List.pick (function - | :? VirtualNetworkGateway as v -> Some v - | _ -> None) - - Expect.equal gw.VirtualNetwork.Value "vnet" "Incorrect Virtual network" - } - - test "Can create a VirtualNetworkGateway with VpnClientConfiguration" { - let b = - gateway { - name "gateway" - - vpn_client ( - vpnclient { - add_address_pool "10.31.0.0/16" - add_address_pool "10.32.1.0/24" - } - ) + { + Address = Net.IPAddress.Parse "10.32.1.0" + Prefix = 24 } - :> IBuilder - - let resources = b.BuildResources Location.WestEurope - - let gw = - resources - |> List.pick (function - | :? VirtualNetworkGateway as v -> Some v - | _ -> None) - - Expect.isSome gw.VpnClientConfiguration "Incorrect VpnClientConfiguration" - - Expect.equal - gw.VpnClientConfiguration.Value.ClientAddressPools - [ - { - Address = Net.IPAddress.Parse "10.31.0.0" - Prefix = 16 - } - { - Address = Net.IPAddress.Parse "10.32.1.0" - Prefix = 24 - } - ] - "Incorect client pools" - } - test "Can create a VirtualNetworkGateway with root cert" { - let b = - gateway { - name "gateway" - - vpn_client ( - vpnclient { - add_root_certificate "root" "certdata" - - add_root_certificate - "root2" - """ + ] + "Incorect client pools" + } + test "Can create a VirtualNetworkGateway with root cert" { + let b = + gateway { + name "gateway" + + vpn_client ( + vpnclient { + add_root_certificate "root" "certdata" + + add_root_certificate + "root2" + """ -----BEGIN CERTIFICATE----- IQfNUTod7Jl7ZOacFlV3fvJTANBgkqh TER7A0qo591ewpAPMpugHh9eQ3ucR5o -----END CERTIFICATE-----""" - } - ) - } - :> IBuilder - - let resources = b.BuildResources Location.WestEurope - - let gw = - resources - |> List.pick (function - | :? VirtualNetworkGateway as v -> Some v - | _ -> None) - - Expect.isSome gw.VpnClientConfiguration "Incorrect VpnClientConfiguration" - - Expect.equal - gw.VpnClientConfiguration.Value.ClientRootCertificates - [ - {| - Name = "root" - PublicCertData = "certdata" - |} - {| - Name = "root2" - PublicCertData = "IQfNUTod7Jl7ZOacFlV3fvJTANBgkqhTER7A0qo591ewpAPMpugHh9eQ3ucR5o" - |} - ] - "Incorect Root Certificates" - } - - test "Can create a VirtualNetworkGateway with revoked client certs" { - let b = - gateway { - name "gateway" - - vpn_client ( - vpnclient { - add_revoked_certificate "revoked" "certdata" - add_revoked_certificate "revoked2" "certdata2" - } - ) - } - :> IBuilder - - let resources = b.BuildResources Location.WestEurope - - let gw = - resources - |> List.pick (function - | :? VirtualNetworkGateway as v -> Some v - | _ -> None) - - Expect.isSome gw.VpnClientConfiguration "Incorrect VpnClientConfiguration" - - Expect.equal - gw.VpnClientConfiguration.Value.ClientRevokedCertificates - [ - {| - Name = "revoked" - Thumbprint = "certdata" - |} - {| - Name = "revoked2" - Thumbprint = "certdata2" - |} - ] - "Incorect Revoked Certificates" - } - - test "Can create a VirtualNetworkGateway with protocols" { - let b = - gateway { - name "gateway" - vpn_client (vpnclient { protocols [ OpenVPN; IkeV2 ] }) - } - :> IBuilder - - let resources = b.BuildResources Location.WestEurope - - let gw = - resources - |> List.pick (function - | :? VirtualNetworkGateway as v -> Some v - | _ -> None) - - Expect.isSome gw.VpnClientConfiguration "Incorrect VpnClientConfiguration" - Expect.equal gw.VpnClientConfiguration.Value.ClientProtocols [ OpenVPN; IkeV2 ] "Incorect Protocols" - } - - test "Can create a VirtualNetworkGateway with default protocol" { - let b = - gateway { - name "gateway" - vpn_client (vpnclient { add_address_pool "10.31.0.0/16" }) - } - :> IBuilder - - let resources = b.BuildResources Location.WestEurope - - let gw = - resources - |> List.pick (function - | :? VirtualNetworkGateway as v -> Some v - | _ -> None) - - Expect.isSome gw.VpnClientConfiguration "Incorrect VpnClientConfiguration" - Expect.equal gw.VpnClientConfiguration.Value.ClientProtocols [ SSTP ] "Incorect Protocols" - } - ] + } + ) + } + :> IBuilder + + let resources = b.BuildResources Location.WestEurope + + let gw = + resources + |> List.pick (function + | :? VirtualNetworkGateway as v -> Some v + | _ -> None) + + Expect.isSome gw.VpnClientConfiguration "Incorrect VpnClientConfiguration" + + Expect.equal + gw.VpnClientConfiguration.Value.ClientRootCertificates + [ + {| + Name = "root" + PublicCertData = "certdata" + |} + {| + Name = "root2" + PublicCertData = "IQfNUTod7Jl7ZOacFlV3fvJTANBgkqhTER7A0qo591ewpAPMpugHh9eQ3ucR5o" + |} + ] + "Incorect Root Certificates" + } + + test "Can create a VirtualNetworkGateway with revoked client certs" { + let b = + gateway { + name "gateway" + + vpn_client ( + vpnclient { + add_revoked_certificate "revoked" "certdata" + add_revoked_certificate "revoked2" "certdata2" + } + ) + } + :> IBuilder + + let resources = b.BuildResources Location.WestEurope + + let gw = + resources + |> List.pick (function + | :? VirtualNetworkGateway as v -> Some v + | _ -> None) + + Expect.isSome gw.VpnClientConfiguration "Incorrect VpnClientConfiguration" + + Expect.equal + gw.VpnClientConfiguration.Value.ClientRevokedCertificates + [ + {| + Name = "revoked" + Thumbprint = "certdata" + |} + {| + Name = "revoked2" + Thumbprint = "certdata2" + |} + ] + "Incorect Revoked Certificates" + } + + test "Can create a VirtualNetworkGateway with protocols" { + let b = + gateway { + name "gateway" + vpn_client (vpnclient { protocols [ OpenVPN; IkeV2 ] }) + } + :> IBuilder + + let resources = b.BuildResources Location.WestEurope + + let gw = + resources + |> List.pick (function + | :? VirtualNetworkGateway as v -> Some v + | _ -> None) + + Expect.isSome gw.VpnClientConfiguration "Incorrect VpnClientConfiguration" + Expect.equal gw.VpnClientConfiguration.Value.ClientProtocols [ OpenVPN; IkeV2 ] "Incorect Protocols" + } + + test "Can create a VirtualNetworkGateway with default protocol" { + let b = + gateway { + name "gateway" + vpn_client (vpnclient { add_address_pool "10.31.0.0/16" }) + } + :> IBuilder + + let resources = b.BuildResources Location.WestEurope + + let gw = + resources + |> List.pick (function + | :? VirtualNetworkGateway as v -> Some v + | _ -> None) + + Expect.isSome gw.VpnClientConfiguration "Incorrect VpnClientConfiguration" + Expect.equal gw.VpnClientConfiguration.Value.ClientProtocols [ SSTP ] "Incorect Protocols" + } + ] diff --git a/src/Tests/VirtualWan.fs b/src/Tests/VirtualWan.fs index a32acb8be..2d9599c73 100644 --- a/src/Tests/VirtualWan.fs +++ b/src/Tests/VirtualWan.fs @@ -18,128 +18,117 @@ let getVirtualWanResource = /// Collection of tests for the VirtualWan resource and builders. Needs to be included in AllTests.fs let tests = - testList - "VirtualWan" - [ - test "Can create a basic VirtualWan" { - let vwan = - arm { - location Location.WestUS - add_resources [ vwan { name "my-vwan" } ] - } - |> getVirtualWanResource - - Expect.equal vwan.Name "my-vwan" "Incorrect Resource Name" - Expect.equal vwan.Location "westus" "Incorrect Location" - Expect.equal vwan.VirtualWANType "Basic" "Default should be 'Basic'" - Expect.isFalse vwan.AllowBranchToBranchTraffic.Value "AllowBranchToBranchTraffic should be false" - Expect.isFalse vwan.DisableVpnEncryption.Value "DisableVpnEncryption should not have a value" - - Expect.equal - vwan.Office365LocalBreakoutCategory - "None" - "Office365LocalBreakoutCategory should be 'None'" - } - test "Can create a standard VirtualWan" { - let vwan = - arm { - location Location.WestUS - - add_resources - [ - vwan { - name "my-vwan" - standard_vwan - } - ] - } - |> getVirtualWanResource - - Expect.equal vwan.VirtualWANType "Standard" "" - } - test "Can create a VirtualWan with DisableVpnEncryption" { - let vwan = - arm { - location Location.WestUS - - add_resources - [ - vwan { - name "my-vwan" - disable_vpn_encryption - } - ] - } - |> getVirtualWanResource - - Expect.equal vwan.DisableVpnEncryption (Nullable true) "" - } - test "Can create a VirtualWan with AllowBranchToBranchTraffic" { - let vwan = - arm { - location Location.WestUS - - add_resources - [ - vwan { - name "my-vwan" - allow_branch_to_branch_traffic - } - ] - } - |> getVirtualWanResource - - Expect.equal vwan.AllowBranchToBranchTraffic (Nullable true) "" - } - test "Can create a VirtualWan with Office365LocalBreakoutCategory.All" { - let vwan = - arm { - location Location.WestUS - - add_resources - [ - vwan { - name "my-vwan" - office_365_local_breakout_category Office365LocalBreakoutCategory.All - } - ] - } - |> getVirtualWanResource - - Expect.equal vwan.Office365LocalBreakoutCategory "All" "" - } - test "Can create a VirtualWan with Office365LocalBreakoutCategory.Optimize" { - let vwan = - arm { - location Location.WestUS - - add_resources - [ - vwan { - name "my-vwan" - office_365_local_breakout_category Office365LocalBreakoutCategory.Optimize - } - ] - } - |> getVirtualWanResource - - Expect.equal vwan.Office365LocalBreakoutCategory "Optimize" "" - } - test "Can create a VirtualWan with Office365LocalBreakoutCategory.OptimizeAndAllow" { - let vwan = - arm { - location Location.WestUS - - add_resources - [ - vwan { - name "my-vwan" - office_365_local_breakout_category Office365LocalBreakoutCategory.OptimizeAndAllow - } - ] - } - |> getVirtualWanResource - - Expect.equal vwan.Office365LocalBreakoutCategory "OptimizeAndAllow" "" - } - ] + testList "VirtualWan" [ + test "Can create a basic VirtualWan" { + let vwan = + arm { + location Location.WestUS + add_resources [ vwan { name "my-vwan" } ] + } + |> getVirtualWanResource + + Expect.equal vwan.Name "my-vwan" "Incorrect Resource Name" + Expect.equal vwan.Location "westus" "Incorrect Location" + Expect.equal vwan.VirtualWANType "Basic" "Default should be 'Basic'" + Expect.isFalse vwan.AllowBranchToBranchTraffic.Value "AllowBranchToBranchTraffic should be false" + Expect.isFalse vwan.DisableVpnEncryption.Value "DisableVpnEncryption should not have a value" + + Expect.equal vwan.Office365LocalBreakoutCategory "None" "Office365LocalBreakoutCategory should be 'None'" + } + test "Can create a standard VirtualWan" { + let vwan = + arm { + location Location.WestUS + + add_resources [ + vwan { + name "my-vwan" + standard_vwan + } + ] + } + |> getVirtualWanResource + + Expect.equal vwan.VirtualWANType "Standard" "" + } + test "Can create a VirtualWan with DisableVpnEncryption" { + let vwan = + arm { + location Location.WestUS + + add_resources [ + vwan { + name "my-vwan" + disable_vpn_encryption + } + ] + } + |> getVirtualWanResource + + Expect.equal vwan.DisableVpnEncryption (Nullable true) "" + } + test "Can create a VirtualWan with AllowBranchToBranchTraffic" { + let vwan = + arm { + location Location.WestUS + + add_resources [ + vwan { + name "my-vwan" + allow_branch_to_branch_traffic + } + ] + } + |> getVirtualWanResource + + Expect.equal vwan.AllowBranchToBranchTraffic (Nullable true) "" + } + test "Can create a VirtualWan with Office365LocalBreakoutCategory.All" { + let vwan = + arm { + location Location.WestUS + + add_resources [ + vwan { + name "my-vwan" + office_365_local_breakout_category Office365LocalBreakoutCategory.All + } + ] + } + |> getVirtualWanResource + + Expect.equal vwan.Office365LocalBreakoutCategory "All" "" + } + test "Can create a VirtualWan with Office365LocalBreakoutCategory.Optimize" { + let vwan = + arm { + location Location.WestUS + + add_resources [ + vwan { + name "my-vwan" + office_365_local_breakout_category Office365LocalBreakoutCategory.Optimize + } + ] + } + |> getVirtualWanResource + + Expect.equal vwan.Office365LocalBreakoutCategory "Optimize" "" + } + test "Can create a VirtualWan with Office365LocalBreakoutCategory.OptimizeAndAllow" { + let vwan = + arm { + location Location.WestUS + + add_resources [ + vwan { + name "my-vwan" + office_365_local_breakout_category Office365LocalBreakoutCategory.OptimizeAndAllow + } + ] + } + |> getVirtualWanResource + + Expect.equal vwan.Office365LocalBreakoutCategory "OptimizeAndAllow" "" + } + ] diff --git a/src/Tests/WebApp.fs b/src/Tests/WebApp.fs index 0e314c87c..e1950207a 100644 --- a/src/Tests/WebApp.fs +++ b/src/Tests/WebApp.fs @@ -27,1683 +27,1587 @@ let getResourceAtIndex o = let getResources (v: IBuilder) = v.BuildResources Location.WestEurope let tests = - testList - "Web App Tests" - [ - test "Basic Web App has service plan and AI dependencies set" { - let resources = webApp { name "test" } |> getResources - let wa = resources |> getResource |> List.head - - Expect.containsAll - wa.Dependencies - [ - ResourceId.create (components, ResourceName "test-ai") - ResourceId.create (serverFarms, ResourceName "test-farm") - ] - "Missing dependencies" - - Expect.hasLength (resources |> getResource) 1 "Should be one AI component" - Expect.hasLength (resources |> getResource) 1 "Should be one server farm" - } - test "Web App allows renaming of service plan and AI" { - let resources = - webApp { - name "test" - service_plan_name "supersp" - app_insights_name "superai" - } - |> getResources + testList "Web App Tests" [ + test "Basic Web App has service plan and AI dependencies set" { + let resources = webApp { name "test" } |> getResources + let wa = resources |> getResource |> List.head + + Expect.containsAll + wa.Dependencies + [ + ResourceId.create (components, ResourceName "test-ai") + ResourceId.create (serverFarms, ResourceName "test-farm") + ] + "Missing dependencies" + + Expect.hasLength (resources |> getResource) 1 "Should be one AI component" + Expect.hasLength (resources |> getResource) 1 "Should be one server farm" + } + test "Web App allows renaming of service plan and AI" { + let resources = + webApp { + name "test" + service_plan_name "supersp" + app_insights_name "superai" + } + |> getResources + + let wa = resources |> getResource |> List.head + + Expect.containsAll + wa.Dependencies + [ + ResourceId.create (serverFarms, ResourceName "supersp") + ResourceId.create (components, ResourceName "superai") + ] + "Missing dependencies" + + Expect.hasLength (resources |> getResource) 1 "Should be one AI component" + Expect.hasLength (resources |> getResource) 1 "Should be one server farm" + } + test "Web App creates dependencies but no resources with linked AI and Server Farm configs" { + let sp = servicePlan { name "plan" } + let ai = appInsights { name "ai" } + + let resources = + webApp { + name "test" + link_to_app_insights ai + link_to_service_plan sp + } + |> getResources - let wa = resources |> getResource |> List.head + let wa = resources |> getResource |> List.head - Expect.containsAll - wa.Dependencies - [ - ResourceId.create (serverFarms, ResourceName "supersp") - ResourceId.create (components, ResourceName "superai") - ] - "Missing dependencies" + Expect.containsAll + wa.Dependencies + [ + ResourceId.create (serverFarms, ResourceName "plan") + ResourceId.create (components, ResourceName "ai") + ] + "Missing dependencies" - Expect.hasLength (resources |> getResource) 1 "Should be one AI component" - Expect.hasLength (resources |> getResource) 1 "Should be one server farm" + Expect.isEmpty (resources |> getResource) "Should be no AI component" + Expect.isEmpty (resources |> getResource) "Should be no server farm" + } + test "Web Apps link together" { + let first = webApp { + name "first" + link_to_service_plan (servicePlan { name "firstSp" }) } - test "Web App creates dependencies but no resources with linked AI and Server Farm configs" { - let sp = servicePlan { name "plan" } - let ai = appInsights { name "ai" } - let resources = - webApp { - name "test" - link_to_app_insights ai - link_to_service_plan sp - } - |> getResources + let second = + webApp { + name "test" + link_to_service_plan first + } + |> getResources - let wa = resources |> getResource |> List.head + let wa = second |> getResource |> List.head - Expect.containsAll - wa.Dependencies - [ - ResourceId.create (serverFarms, ResourceName "plan") - ResourceId.create (components, ResourceName "ai") - ] - "Missing dependencies" + Expect.containsAll + wa.Dependencies + [ ResourceId.create (serverFarms, ResourceName "firstSp") ] + "Missing dependencies" - Expect.isEmpty (resources |> getResource) "Should be no AI component" - Expect.isEmpty (resources |> getResource) "Should be no server farm" - } - test "Web Apps link together" { - let first = - webApp { - name "first" - link_to_service_plan (servicePlan { name "firstSp" }) - } + Expect.isEmpty (second |> getResource) "Should be no server farm" + } + test "Web App does not create dependencies for unmanaged linked resources" { + let resources = + webApp { + name "test" + link_to_unmanaged_app_insights (components.resourceId "test") + link_to_unmanaged_service_plan (serverFarms.resourceId "test2") + } + |> getResources + + let wa = resources |> getResource |> List.head + Expect.isEmpty wa.Dependencies "Should be no dependencies" + Expect.isEmpty (resources |> getResource) "Should be no AI component" + Expect.isEmpty (resources |> getResource) "Should be no server farm" + } + test "Web app supports adding tags to resource" { + let resources = + webApp { + name "test" + add_tag "key" "value" + add_tags [ "alpha", "a"; "beta", "b" ] + } + |> getResources - let second = - webApp { - name "test" - link_to_service_plan first - } - |> getResources + let wa = resources |> getResource |> List.head - let wa = second |> getResource |> List.head + Expect.containsAll + (wa.Tags |> Map.toSeq) + [ "key", "value"; "alpha", "a"; "beta", "b" ] + "Should contain the given tags" - Expect.containsAll - wa.Dependencies - [ ResourceId.create (serverFarms, ResourceName "firstSp") ] - "Missing dependencies" + Expect.equal 3 (wa.Tags |> Map.count) "Should not contain additional tags" + } + test "Web App correctly adds connection strings" { + let sa = storageAccount { name "foo" } - Expect.isEmpty (second |> getResource) "Should be no server farm" - } - test "Web App does not create dependencies for unmanaged linked resources" { + let wa = let resources = webApp { name "test" - link_to_unmanaged_app_insights (components.resourceId "test") - link_to_unmanaged_service_plan (serverFarms.resourceId "test2") + connection_string "a" + connection_string ("b", sa.Key) + connection_string ("c", ArmExpression.create ("c"), SQLAzure) } |> getResources - let wa = resources |> getResource |> List.head - Expect.isEmpty wa.Dependencies "Should be no dependencies" - Expect.isEmpty (resources |> getResource) "Should be no AI component" - Expect.isEmpty (resources |> getResource) "Should be no server farm" - } - test "Web app supports adding tags to resource" { - let resources = - webApp { - name "test" - add_tag "key" "value" - add_tags [ "alpha", "a"; "beta", "b" ] - } - |> getResources + resources |> getResource |> List.head - let wa = resources |> getResource |> List.head + let expected = [ + "a", (ParameterSetting(SecureParameter "a"), Custom) + "b", (ExpressionSetting sa.Key, Custom) + "c", (ExpressionSetting(ArmExpression.create ("c")), SQLAzure) + ] - Expect.containsAll - (wa.Tags |> Map.toSeq) - [ "key", "value"; "alpha", "a"; "beta", "b" ] - "Should contain the given tags" + let parameters = wa :> IParameters - Expect.equal 3 (wa.Tags |> Map.count) "Should not contain additional tags" - } - 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) - 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 - - Expect.equal wa.ConnectionStrings (Map expected |> Some) "Missing connections" - Expect.equal parameters.SecureParameters [ SecureParameter "a" ] "Missing parameter" - } - test "CORS works correctly" { - let wa: Site = + Expect.equal wa.ConnectionStrings (Map expected |> Some) "Missing connections" + Expect.equal parameters.SecureParameters [ SecureParameter "a" ] "Missing parameter" + } + test "CORS works correctly" { + let wa: Site = + webApp { + name "test" + enable_cors [ "https://bbc.co.uk" ] + enable_cors_credentials + } + |> getResourceAtIndex 3 + + Expect.sequenceEqual wa.SiteConfig.Cors.AllowedOrigins [ "https://bbc.co.uk" ] "Allowed Origins should be *" + + Expect.equal wa.SiteConfig.Cors.SupportCredentials (Nullable true) "Support Credentials" + } + + test "If CORS is AllOrigins, cannot enable credentials" { + Expect.throws + (fun () -> webApp { name "test" - enable_cors [ "https://bbc.co.uk" ] + enable_cors AllOrigins enable_cors_credentials } - |> getResourceAtIndex 3 + |> ignore) + "Invalid CORS combination" + } - Expect.sequenceEqual - wa.SiteConfig.Cors.AllowedOrigins - [ "https://bbc.co.uk" ] - "Allowed Origins should be *" + test "Automatically converts from * to AllOrigins" { + let wa: Site = + webApp { + name "test" + enable_cors [ "*" ] + } + |> getResourceAtIndex 3 - Expect.equal wa.SiteConfig.Cors.SupportCredentials (Nullable true) "Support Credentials" - } + Expect.sequenceEqual wa.SiteConfig.Cors.AllowedOrigins [ "*" ] "Allowed Origins should be *" + } - test "If CORS is AllOrigins, cannot enable credentials" { - Expect.throws - (fun () -> - webApp { - name "test" - enable_cors AllOrigins - enable_cors_credentials - } - |> ignore) - "Invalid CORS combination" + test "CORS without credentials does not crash" { + webApp { + name "test" + enable_cors AllOrigins } + |> ignore - test "Automatically converts from * to AllOrigins" { - let wa: Site = - webApp { - name "test" - enable_cors [ "*" ] - } - |> getResourceAtIndex 3 - - Expect.sequenceEqual wa.SiteConfig.Cors.AllowedOrigins [ "*" ] "Allowed Origins should be *" + webApp { + name "test" + enable_cors [ "https://bbc.co.uk" ] } + |> ignore + } - test "CORS without credentials does not crash" { + test "If CORS is not enabled, ignores enable credentials" { + let wa: Site = webApp { name "test" - enable_cors AllOrigins + enable_cors_credentials } - |> ignore + |> getResourceAtIndex 3 - webApp { - name "test" - enable_cors [ "https://bbc.co.uk" ] - } - |> ignore - } + Expect.isNull wa.SiteConfig.Cors "Should be no CORS settings" + } - test "If CORS is not enabled, ignores enable credentials" { - let wa: Site = - webApp { - name "test" - enable_cors_credentials - } - |> getResourceAtIndex 3 + test "Implicitly adds a dependency when adding a setting" { + let sa = storageAccount { name "teststorage" } - Expect.isNull wa.SiteConfig.Cors "Should be no CORS settings" + let sql = sqlServer { + name "test" + admin_username "user" + add_databases [ sqlDb { name "thedb" } ] } - test "Implicitly adds a dependency when adding a setting" { - let sa = storageAccount { name "teststorage" } + let wa = webApp { + name "testweb" + setting "storage" sa.Key + setting "conn" (sql.ConnectionString "thedb") + setting "bad" (ArmExpression.literal "ignore_me") + } - let sql = - sqlServer { - name "test" - admin_username "user" - add_databases [ sqlDb { name "thedb" } ] - } + let wa = wa |> getResources |> getResource |> List.head - let wa = - webApp { - name "testweb" - setting "storage" sa.Key - setting "conn" (sql.ConnectionString "thedb") - setting "bad" (ArmExpression.literal "ignore_me") - } + Expect.contains + wa.Dependencies + (ResourceId.create (storageAccounts, sa.Name.ResourceName)) + "Storage Account is missing" - let wa = wa |> getResources |> getResource |> List.head + Expect.contains + wa.Dependencies + (ResourceId.create (Sql.databases, ResourceName "test", ResourceName "thedb")) + "Database is missing" + } - Expect.contains - wa.Dependencies - (ResourceId.create (storageAccounts, sa.Name.ResourceName)) - "Storage Account is missing" + test "Implicitly adds a dependency when adding a connection string" { + let sa = storageAccount { name "teststorage" } - Expect.contains - wa.Dependencies - (ResourceId.create (Sql.databases, ResourceName "test", ResourceName "thedb")) - "Database is missing" + let wa = webApp { + name "testweb" + setting "storage" sa.Key } - test "Implicitly adds a dependency when adding a connection string" { - let sa = storageAccount { name "teststorage" } + let wa = wa |> getResources |> getResource |> List.head - let wa = - webApp { - name "testweb" - setting "storage" sa.Key - } + Expect.contains + wa.Dependencies + (ResourceId.create (storageAccounts, sa.Name.ResourceName)) + "Storage Account is missing" + } - let wa = wa |> getResources |> getResource |> List.head + test "Automatic Key Vault integration works correctly" { + let sa = storageAccount { name "teststorage" } - Expect.contains - wa.Dependencies - (ResourceId.create (storageAccounts, sa.Name.ResourceName)) - "Storage Account is missing" + let wa = webApp { + name "testweb" + setting "storage" sa.Key + secret_setting "secret" + setting "literal" "value" + use_keyvault } - test "Automatic Key Vault integration works correctly" { - let sa = storageAccount { name "teststorage" } + let kv = wa |> getResources |> getResource |> List.head + let secrets = wa |> getResources |> getResource + let site = wa |> getResources |> getResource |> List.head + let vault = wa |> getResources |> getResource |> List.head - let wa = - webApp { - name "testweb" - setting "storage" sa.Key - secret_setting "secret" - setting "literal" "value" - use_keyvault - } + let expectedSettings = + Map [ + "storage", + LiteralSetting "@Microsoft.KeyVault(SecretUri=https://testwebvault.vault.azure.net/secrets/storage)" + "secret", + LiteralSetting "@Microsoft.KeyVault(SecretUri=https://testwebvault.vault.azure.net/secrets/secret)" + "literal", LiteralSetting "value" + ] - let kv = wa |> getResources |> getResource |> List.head - let secrets = wa |> getResources |> getResource - let site = wa |> getResources |> getResource |> List.head - let vault = wa |> getResources |> getResource |> List.head - - let expectedSettings = - Map - [ - "storage", - LiteralSetting - "@Microsoft.KeyVault(SecretUri=https://testwebvault.vault.azure.net/secrets/storage)" - "secret", - LiteralSetting - "@Microsoft.KeyVault(SecretUri=https://testwebvault.vault.azure.net/secrets/secret)" - "literal", LiteralSetting "value" - ] - - Expect.equal site.Identity.SystemAssigned Enabled "System Identity should be enabled" - let settings = Expect.wantSome site.AppSettings "AppSettings should be set" - Expect.containsAll settings expectedSettings "Incorrect settings" - - Expect.sequenceEqual - kv.Dependencies - [ ResourceId.create (sites, site.Name) ] - "Key Vault dependencies are wrong" - - Expect.equal kv.Name (ResourceName(site.Name.Value + "vault")) "Key Vault name is wrong" - Expect.equal wa.CommonWebConfig.Identity.SystemAssigned Enabled "System Identity should be turned on" - - Expect.equal - kv.AccessPolicies.[0].ObjectId - wa.SystemIdentity.PrincipalId.ArmExpression - "Policy is incorrect" - - Expect.hasLength secrets 2 "Incorrect number of KV secrets" - - Expect.equal secrets.[0].Name.Value "testwebvault/storage" "Incorrect secret name" - Expect.equal secrets.[0].Value (ExpressionSecret sa.Key) "Incorrect secret value" - - Expect.sequenceEqual - secrets.[0].Dependencies - [ vaults.resourceId "testwebvault"; storageAccounts.resourceId "teststorage" ] - "Incorrect secret dependencies" - - Expect.equal secrets.[1].Name.Value "testwebvault/secret" "Incorrect secret name" - Expect.equal secrets.[1].Value (ParameterSecret(SecureParameter "secret")) "Incorrect secret value" - - Expect.sequenceEqual - secrets.[1].Dependencies - [ vaults.resourceId "testwebvault" ] - "Incorrect secret dependencies" - - Expect.hasLength vault.AccessPolicies 1 "Incorrect number of access policies" - - Expect.sequenceEqual - vault.AccessPolicies.[0].Permissions.Secrets - [ KeyVault.Secret.Get ] - "Incorrect permissions" - } + Expect.equal site.Identity.SystemAssigned Enabled "System Identity should be enabled" + let settings = Expect.wantSome site.AppSettings "AppSettings should be set" + Expect.containsAll settings expectedSettings "Incorrect settings" - test "Managed KV integration works correctly" { - let sa = storageAccount { name "teststorage" } + Expect.sequenceEqual + kv.Dependencies + [ ResourceId.create (sites, site.Name) ] + "Key Vault dependencies are wrong" - let wa = - webApp { - name "testweb" - setting "storage" sa.Key - secret_setting "secret" - setting "literal" "value" - link_to_keyvault (ResourceName "testwebvault") - } + Expect.equal kv.Name (ResourceName(site.Name.Value + "vault")) "Key Vault name is wrong" + Expect.equal wa.CommonWebConfig.Identity.SystemAssigned Enabled "System Identity should be turned on" - let vault = - keyVault { - name "testwebvault" - add_access_policy (AccessPolicy.create (wa.SystemIdentity.PrincipalId, [ KeyVault.Secret.Get ])) - } + Expect.equal + kv.AccessPolicies.[0].ObjectId + wa.SystemIdentity.PrincipalId.ArmExpression + "Policy is incorrect" - let vault = vault |> getResources |> getResource |> List.head - let secrets = wa |> getResources |> getResource - let site = wa |> getResources |> getResource |> List.head - - let expectedSettings = - Map - [ - "storage", - LiteralSetting - "@Microsoft.KeyVault(SecretUri=https://testwebvault.vault.azure.net/secrets/storage)" - "secret", - LiteralSetting - "@Microsoft.KeyVault(SecretUri=https://testwebvault.vault.azure.net/secrets/secret)" - "literal", LiteralSetting "value" - ] - - Expect.equal site.Identity.SystemAssigned Enabled "System Identity should be enabled" - let settings = Expect.wantSome site.AppSettings "AppSettings should be set" - Expect.containsAll settings expectedSettings "Incorrect settings" - - Expect.equal wa.CommonWebConfig.Identity.SystemAssigned Enabled "System Identity should be turned on" - - Expect.hasLength secrets 2 "Incorrect number of KV secrets" - - Expect.equal secrets.[0].Name.Value "testwebvault/secret" "Incorrect secret name" - Expect.equal secrets.[0].Value (ParameterSecret(SecureParameter "secret")) "Incorrect secret value" - - Expect.sequenceEqual - secrets.[0].Dependencies - [ vaults.resourceId "testwebvault" ] - "Incorrect secret dependencies" - - Expect.equal secrets.[1].Name.Value "testwebvault/storage" "Incorrect secret name" - Expect.equal secrets.[1].Value (ExpressionSecret sa.Key) "Incorrect secret value" - - Expect.sequenceEqual - secrets.[1].Dependencies - [ vaults.resourceId "testwebvault"; storageAccounts.resourceId "teststorage" ] - "Incorrect secret dependencies" - } + Expect.hasLength secrets 2 "Incorrect number of KV secrets" - test "Handles identity correctly" { - let wa: Site = webApp { name "testsite" } |> getResourceAtIndex 0 - Expect.isNull wa.Identity "Default managed identity should be null" + Expect.equal secrets.[0].Name.Value "testwebvault/storage" "Incorrect secret name" + Expect.equal secrets.[0].Value (ExpressionSecret sa.Key) "Incorrect secret value" - let wa: Site = - webApp { - name "othertestsite" - system_identity - } - |> getResourceAtIndex 3 + Expect.sequenceEqual + secrets.[0].Dependencies + [ vaults.resourceId "testwebvault"; storageAccounts.resourceId "teststorage" ] + "Incorrect secret dependencies" - Expect.equal - wa.Identity.Type - (Nullable ManagedServiceIdentityType.SystemAssigned) - "Should have system identity" + Expect.equal secrets.[1].Name.Value "testwebvault/secret" "Incorrect secret name" + Expect.equal secrets.[1].Value (ParameterSecret(SecureParameter "secret")) "Incorrect secret value" - Expect.isNull wa.Identity.UserAssignedIdentities "Should have no user assigned identities" + Expect.sequenceEqual + secrets.[1].Dependencies + [ vaults.resourceId "testwebvault" ] + "Incorrect secret dependencies" - let wa: Site = - webApp { - name "thirdtestsite" - system_identity - add_identity (createUserAssignedIdentity "test") - add_identity (createUserAssignedIdentity "test2") - } - |> getResourceAtIndex 3 + Expect.hasLength vault.AccessPolicies 1 "Incorrect number of access policies" - Expect.equal - wa.Identity.Type - (Nullable ManagedServiceIdentityType.SystemAssignedUserAssigned) - "Should have system identity" + Expect.sequenceEqual + vault.AccessPolicies.[0].Permissions.Secrets + [ KeyVault.Secret.Get ] + "Incorrect permissions" + } - Expect.sequenceEqual - (wa.Identity.UserAssignedIdentities |> Seq.map (fun r -> r.Key)) - [ - "[resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', 'test2')]" - "[resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', 'test')]" - ] - "Should have two user assigned identities" + test "Managed KV integration works correctly" { + let sa = storageAccount { name "teststorage" } - Expect.contains - (wa.SiteConfig.AppSettings |> Seq.map (fun s -> s.Name, s.Value)) - ("AZURE_CLIENT_ID", - "[reference(resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', 'test2')).clientId]") - "Missing AZURE_CLIENT_ID" + let wa = webApp { + name "testweb" + setting "storage" sa.Key + secret_setting "secret" + setting "literal" "value" + link_to_keyvault (ResourceName "testwebvault") } - test "Unmanaged server farm is fully qualified in ARM" { - let farm = - ResourceId.create (serverFarms, ResourceName "my-asp-name", "my-asp-resource-group") + let vault = keyVault { + name "testwebvault" + add_access_policy (AccessPolicy.create (wa.SystemIdentity.PrincipalId, [ KeyVault.Secret.Get ])) + } - let wa: Site = - webApp { - name "test" - link_to_unmanaged_service_plan farm - } - |> getResourceAtIndex 2 + let vault = vault |> getResources |> getResource |> List.head + let secrets = wa |> getResources |> getResource + let site = wa |> getResources |> getResource |> List.head - Expect.equal - wa.ServerFarmId - "[resourceId('my-asp-resource-group', 'Microsoft.Web/serverfarms', 'my-asp-name')]" - "" - } + let expectedSettings = + Map [ + "storage", + LiteralSetting "@Microsoft.KeyVault(SecretUri=https://testwebvault.vault.azure.net/secrets/storage)" + "secret", + LiteralSetting "@Microsoft.KeyVault(SecretUri=https://testwebvault.vault.azure.net/secrets/secret)" + "literal", LiteralSetting "value" + ] - test "Adds the Logging extension automatically for .NET Core apps" { - let wa = webApp { name "siteX" } - let extension = wa |> getResources |> getResource |> List.head + Expect.equal site.Identity.SystemAssigned Enabled "System Identity should be enabled" + let settings = Expect.wantSome site.AppSettings "AppSettings should be set" + Expect.containsAll settings expectedSettings "Incorrect settings" - Expect.equal - extension.Name.Value - "Microsoft.AspNetCore.AzureAppServices.SiteExtension" - "Wrong extension" + Expect.equal wa.CommonWebConfig.Identity.SystemAssigned Enabled "System Identity should be turned on" - let wa = - webApp { - name "siteX" - runtime_stack Runtime.Java11 - } + Expect.hasLength secrets 2 "Incorrect number of KV secrets" - let extensions = wa |> getResources |> getResource - Expect.isEmpty extensions "Shouldn't be any extensions" + Expect.equal secrets.[0].Name.Value "testwebvault/secret" "Incorrect secret name" + Expect.equal secrets.[0].Value (ParameterSecret(SecureParameter "secret")) "Incorrect secret value" - let wa = - webApp { - name "siteX" - automatic_logging_extension false - } + Expect.sequenceEqual + secrets.[0].Dependencies + [ vaults.resourceId "testwebvault" ] + "Incorrect secret dependencies" - let extensions = wa |> getResources |> getResource - Expect.isEmpty extensions "Shouldn't be any extensions" - } + Expect.equal secrets.[1].Name.Value "testwebvault/storage" "Incorrect secret name" + Expect.equal secrets.[1].Value (ExpressionSecret sa.Key) "Incorrect secret value" - test "Does not add the logging extension for apps using a docker image" { - let wa = - webApp { - name "siteX" - docker_image "someImage" "someCommand" - } + Expect.sequenceEqual + secrets.[1].Dependencies + [ vaults.resourceId "testwebvault"; storageAccounts.resourceId "teststorage" ] + "Incorrect secret dependencies" + } - let extensions = wa |> getResources |> getResource - Expect.isEmpty extensions "Shouldn't be any extensions" - } + test "Handles identity correctly" { + let wa: Site = webApp { name "testsite" } |> getResourceAtIndex 0 + Expect.isNull wa.Identity "Default managed identity should be null" - test "Can specify different image for slots" { - let wa = - webApp { - name "my-webapp-2651A324" - sku (Sku.Standard "S1") - docker_image "nginx:1.22.1" "" - - add_slots - [ - appSlot { - name "staging" - docker_image "nginx:1.23.1" "" - } - ] - } + let wa: Site = + webApp { + name "othertestsite" + system_identity + } + |> getResourceAtIndex 3 + + Expect.equal + wa.Identity.Type + (Nullable ManagedServiceIdentityType.SystemAssigned) + "Should have system identity" - let (slot: Site) = wa |> getResourceAtIndex 3 - Expect.equal slot.Name "my-webapp-2651A324/staging" "Resource isn't the 'staging' slot" - Expect.equal slot.SiteConfig.LinuxFxVersion "DOCKER|nginx:1.23.1" "Docker image not set on slot" + Expect.isNull wa.Identity.UserAssignedIdentities "Should have no user assigned identities" + + let wa: Site = + webApp { + name "thirdtestsite" + system_identity + add_identity (createUserAssignedIdentity "test") + add_identity (createUserAssignedIdentity "test2") + } + |> getResourceAtIndex 3 + + Expect.equal + wa.Identity.Type + (Nullable ManagedServiceIdentityType.SystemAssignedUserAssigned) + "Should have system identity" + + Expect.sequenceEqual + (wa.Identity.UserAssignedIdentities |> Seq.map (fun r -> r.Key)) + [ + "[resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', 'test2')]" + "[resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', 'test')]" + ] + "Should have two user assigned identities" + + Expect.contains + (wa.SiteConfig.AppSettings |> Seq.map (fun s -> s.Name, s.Value)) + ("AZURE_CLIENT_ID", + "[reference(resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', 'test2')).clientId]") + "Missing AZURE_CLIENT_ID" + } + + test "Unmanaged server farm is fully qualified in ARM" { + let farm = + ResourceId.create (serverFarms, ResourceName "my-asp-name", "my-asp-resource-group") + + let wa: Site = + webApp { + name "test" + link_to_unmanaged_service_plan farm + } + |> getResourceAtIndex 2 + + Expect.equal + wa.ServerFarmId + "[resourceId('my-asp-resource-group', 'Microsoft.Web/serverfarms', 'my-asp-name')]" + "" + } + + test "Adds the Logging extension automatically for .NET Core apps" { + let wa = webApp { name "siteX" } + let extension = wa |> getResources |> getResource |> List.head + + Expect.equal extension.Name.Value "Microsoft.AspNetCore.AzureAppServices.SiteExtension" "Wrong extension" + + let wa = webApp { + name "siteX" + runtime_stack Runtime.Java11 } - test "Handles add_extension correctly" { - let wa = - webApp { - name "siteX" - add_extension "extensionA" - runtime_stack Runtime.Java11 - } + let extensions = wa |> getResources |> getResource + Expect.isEmpty extensions "Shouldn't be any extensions" - let resources = wa |> getResources - let sx = resources |> getResource |> List.head - let r = sx :> IArmResource + let wa = webApp { + name "siteX" + automatic_logging_extension false + } - Expect.equal sx.SiteName (ResourceName "siteX") "Extension knows the site name" - Expect.equal sx.Location Location.WestEurope "Location is correct" - Expect.equal sx.Name (ResourceName "extensionA") "Extension name is correct" + let extensions = wa |> getResources |> getResource + Expect.isEmpty extensions "Shouldn't be any extensions" + } - Expect.equal - r.ResourceId.ArmExpression.Value - "resourceId('Microsoft.Web/sites/siteextensions', 'siteX', 'extensionA')" - "Resource name composed of site name and extension name" + test "Does not add the logging extension for apps using a docker image" { + let wa = webApp { + name "siteX" + docker_image "someImage" "someCommand" } - test "Handles multiple add_extension correctly" { - let wa = - webApp { - name "siteX" - add_extension "extensionA" - add_extension "extensionB" - add_extension "extensionB" - runtime_stack Runtime.Java11 + let extensions = wa |> getResources |> getResource + Expect.isEmpty extensions "Shouldn't be any extensions" + } + + test "Can specify different image for slots" { + let wa = webApp { + name "my-webapp-2651A324" + sku (Sku.Standard "S1") + docker_image "nginx:1.22.1" "" + + add_slots [ + appSlot { + name "staging" + docker_image "nginx:1.23.1" "" } + ] + } - let resources = wa |> getResources |> getResource - - let actual = List.sort resources - - let expected = - [ - { - Location = Location.WestEurope - Name = ResourceName "extensionA" - SiteName = ResourceName "siteX" - } - { - Location = Location.WestEurope - Name = ResourceName "extensionB" - SiteName = ResourceName "siteX" - } - ] - - Expect.sequenceEqual actual expected "Both extensions defined" + let (slot: Site) = wa |> getResourceAtIndex 3 + Expect.equal slot.Name "my-webapp-2651A324/staging" "Resource isn't the 'staging' slot" + Expect.equal slot.SiteConfig.LinuxFxVersion "DOCKER|nginx:1.23.1" "Docker image not set on slot" + } + + test "Handles add_extension correctly" { + let wa = webApp { + name "siteX" + add_extension "extensionA" + runtime_stack Runtime.Java11 } - test "SiteExtension ResourceId constructed correctly" { - let siteName = ResourceName "siteX" - let resourceId = siteExtensions.resourceId siteName + let resources = wa |> getResources + let sx = resources |> getResource |> List.head + let r = sx :> IArmResource + + Expect.equal sx.SiteName (ResourceName "siteX") "Extension knows the site name" + Expect.equal sx.Location Location.WestEurope "Location is correct" + Expect.equal sx.Name (ResourceName "extensionA") "Extension name is correct" + + Expect.equal + r.ResourceId.ArmExpression.Value + "resourceId('Microsoft.Web/sites/siteextensions', 'siteX', 'extensionA')" + "Resource name composed of site name and extension name" + } - Expect.equal - resourceId.ArmExpression.Value - "resourceId('Microsoft.Web/sites/siteextensions', 'siteX')" - "" + test "Handles multiple add_extension correctly" { + let wa = webApp { + name "siteX" + add_extension "extensionA" + add_extension "extensionB" + add_extension "extensionB" + runtime_stack Runtime.Java11 } - test "Deploys AI configuration correctly" { - let hasSetting key message (wa: Site) = - Expect.isTrue (wa.SiteConfig.AppSettings |> Seq.exists (fun k -> k.Name = key)) message + let resources = wa |> getResources |> getResource - let wa: Site = webApp { name "testsite" } |> getResourceAtIndex 3 + let actual = List.sort resources - wa - |> hasSetting "APPINSIGHTS_INSTRUMENTATIONKEY" "Missing Windows instrumentation key" + let expected = [ + { + Location = Location.WestEurope + Name = ResourceName "extensionA" + SiteName = ResourceName "siteX" + } + { + Location = Location.WestEurope + Name = ResourceName "extensionB" + SiteName = ResourceName "siteX" + } + ] - let wa: Site = - webApp { - name "testsite" - operating_system Linux - } - |> getResourceAtIndex 2 + Expect.sequenceEqual actual expected "Both extensions defined" + } - wa - |> hasSetting "APPINSIGHTS_INSTRUMENTATIONKEY" "Missing Linux instrumentation key" + test "SiteExtension ResourceId constructed correctly" { + let siteName = ResourceName "siteX" + let resourceId = siteExtensions.resourceId siteName - let wa: Site = - webApp { - name "testsite" - app_insights_off - } - |> getResourceAtIndex 2 + Expect.equal resourceId.ArmExpression.Value "resourceId('Microsoft.Web/sites/siteextensions', 'siteX')" "" + } - Expect.isEmpty wa.SiteConfig.AppSettings "Should be no settings" - } + test "Deploys AI configuration correctly" { + let hasSetting key message (wa: Site) = + Expect.isTrue (wa.SiteConfig.AppSettings |> Seq.exists (fun k -> k.Name = key)) message - test "Supports always on" { - let template = - webApp { - name "web" - always_on - } + 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 2 - Expect.equal template.CommonWebConfig.AlwaysOn true "AlwaysOn should be true" + wa + |> hasSetting "APPINSIGHTS_INSTRUMENTATIONKEY" "Missing Linux instrumentation key" - let w: Site = webApp { name "testDefault" } |> getResourceAtIndex 3 - Expect.equal w.SiteConfig.AlwaysOn (Nullable false) "always on should be false by default" + let wa: Site = + webApp { + name "testsite" + app_insights_off + } + |> getResourceAtIndex 2 + + Expect.isEmpty wa.SiteConfig.AppSettings "Should be no settings" + } + + test "Supports always on" { + let template = webApp { + name "web" + always_on } - test "Supports 32 and 64 bit worker processes" { - let site: Site = webApp { name "web" } |> getResourceAtIndex 3 - Expect.equal site.SiteConfig.Use32BitWorkerProcess (Nullable()) "Default worker process" + Expect.equal template.CommonWebConfig.AlwaysOn true "AlwaysOn should be true" - let site: Site = - webApp { - name "web2" - worker_process Bits32 - } - |> getResourceAtIndex 3 + let w: Site = webApp { name "testDefault" } |> getResourceAtIndex 3 + Expect.equal w.SiteConfig.AlwaysOn (Nullable false) "always on should be false by default" + } - Expect.equal site.SiteConfig.Use32BitWorkerProcess (Nullable true) "Should use 32 bit worker process" + test "Supports 32 and 64 bit worker processes" { + let site: Site = webApp { name "web" } |> getResourceAtIndex 3 + Expect.equal site.SiteConfig.Use32BitWorkerProcess (Nullable()) "Default worker process" - let site: Site = - webApp { - name "web3" - worker_process Bits64 - } - |> getResourceAtIndex 3 + let site: Site = + webApp { + name "web2" + worker_process Bits32 + } + |> getResourceAtIndex 3 - Expect.equal - site.SiteConfig.Use32BitWorkerProcess - (Nullable false) - "Should not use 32 bit worker process" + Expect.equal site.SiteConfig.Use32BitWorkerProcess (Nullable true) "Should use 32 bit worker process" + + let site: Site = + webApp { + name "web3" + worker_process Bits64 + } + |> getResourceAtIndex 3 + + Expect.equal site.SiteConfig.Use32BitWorkerProcess (Nullable false) "Should not use 32 bit worker process" + } + + test "Supports .NET 6" { + let app = webApp { + name "net6" + runtime_stack Runtime.DotNet60 } - test "Supports .NET 6" { - let app = - webApp { - name "net6" - runtime_stack Runtime.DotNet60 - } + let site = app |> getResources |> getResource |> List.head + Expect.equal site.NetFrameworkVersion.Value "v6.0" "Wrong dotnet version" + Expect.equal site.Metadata.Head ("CURRENT_STACK", "dotnet") "Stack should be dotnet" + } - let site = app |> getResources |> getResource |> List.head - Expect.equal site.NetFrameworkVersion.Value "v6.0" "Wrong dotnet version" - Expect.equal site.Metadata.Head ("CURRENT_STACK", "dotnet") "Stack should be dotnet" + test "Supports .NET 7" { + let app = webApp { + name "net7" + runtime_stack Runtime.DotNet70 } - test "Supports .NET 7" { - let app = - webApp { - name "net7" - runtime_stack Runtime.DotNet70 - } + let site = app |> getResources |> getResource |> List.head + Expect.equal site.NetFrameworkVersion.Value "v7.0" "Wrong dotnet version" + Expect.equal site.Metadata.Head ("CURRENT_STACK", "dotnet") "Stack should be dotnet" + } - let site = app |> getResources |> getResource |> List.head - Expect.equal site.NetFrameworkVersion.Value "v7.0" "Wrong dotnet version" - Expect.equal site.Metadata.Head ("CURRENT_STACK", "dotnet") "Stack should be dotnet" + test "Supports .NET 5 on Linux" { + let app = webApp { + name "net5" + operating_system Linux + runtime_stack Runtime.DotNet50 } - test "Supports .NET 5 on Linux" { - let app = - webApp { - name "net5" - operating_system Linux - runtime_stack Runtime.DotNet50 - } + let site: Site = app |> getResourceAtIndex 2 + Expect.equal site.SiteConfig.LinuxFxVersion "DOTNETCORE|5.0" "Wrong dotnet version" + } - let site: Site = app |> getResourceAtIndex 2 - Expect.equal site.SiteConfig.LinuxFxVersion "DOTNETCORE|5.0" "Wrong dotnet version" + test "WebApp supports adding slots" { + let slot = appSlot { name "warm-up" } + + let site: WebAppConfig = webApp { + name "slots" + add_slot slot } - test "WebApp supports adding slots" { - let slot = appSlot { name "warm-up" } + Expect.isTrue (site.CommonWebConfig.Slots.ContainsKey "warm-up") "Config should contain slot" - let site: WebAppConfig = - webApp { - name "slots" - add_slot slot - } + let slots = + site + |> getResources + |> getResource + |> List.filter (fun x -> x.ResourceType = Arm.Web.slots) + // Default "production" slot is not included as it is created automatically in Azure + Expect.hasLength slots 1 "Should only be 1 slot" + } - Expect.isTrue (site.CommonWebConfig.Slots.ContainsKey "warm-up") "Config should contain slot" + test "WebApp with slot and zip_deploy_slot does not have ZipDeployPath on slot" { + let slot = appSlot { name "warm-up" } - let slots = - site - |> getResources - |> getResource - |> List.filter (fun x -> x.ResourceType = Arm.Web.slots) - // Default "production" slot is not included as it is created automatically in Azure - Expect.hasLength slots 1 "Should only be 1 slot" + let site: WebAppConfig = webApp { + name "slots" + add_slot slot + zip_deploy_slot "warm-up" "test.zip" } - test "WebApp with slot and zip_deploy_slot does not have ZipDeployPath on slot" { - let slot = appSlot { name "warm-up" } + Expect.isTrue (site.CommonWebConfig.Slots.ContainsKey "warm-up") "Config should contain slot" - let site: WebAppConfig = - webApp { - name "slots" - add_slot slot - zip_deploy_slot "warm-up" "test.zip" - } + let slots = + site + |> getResources + |> getResource + |> List.filter (fun x -> x.ResourceType = Arm.Web.slots) + // Default "production" slot is not included as it is created automatically in Azure + Expect.hasLength slots 1 "Should only be 1 slot" + Expect.isNone slots.[0].ZipDeployPath "Zip Deploy Path should be set to None" + } - Expect.isTrue (site.CommonWebConfig.Slots.ContainsKey "warm-up") "Config should contain slot" + test "WebApp with slot that has system assigned identity adds identity to slot" { + let slot = appSlot { + name "warm-up" + system_identity + } - let slots = - site - |> getResources - |> getResource - |> List.filter (fun x -> x.ResourceType = Arm.Web.slots) - // Default "production" slot is not included as it is created automatically in Azure - Expect.hasLength slots 1 "Should only be 1 slot" - Expect.isNone slots.[0].ZipDeployPath "Zip Deploy Path should be set to None" + let site: WebAppConfig = webApp { + name "webapp" + add_slot slot } - test "WebApp with slot that has system assigned identity adds identity to slot" { - let slot = - appSlot { - name "warm-up" - system_identity - } + Expect.isTrue (site.CommonWebConfig.Slots.ContainsKey "warm-up") "Config should contain slot" - let site: WebAppConfig = - webApp { - name "webapp" - add_slot slot - } + let slots = site |> getResources |> getResource + // Default "production" slot is not included as it is created automatically in Azure + Expect.hasLength slots 2 "Should only be 1 slot and 1 site" - Expect.isTrue (site.CommonWebConfig.Slots.ContainsKey "warm-up") "Config should contain slot" + let expected = { + SystemAssigned = Enabled + UserAssigned = [] + } - let slots = site |> getResources |> getResource - // Default "production" slot is not included as it is created automatically in Azure - Expect.hasLength slots 2 "Should only be 1 slot and 1 site" + Expect.equal (slots.[1]).Identity expected "Slot should have slot setting" + } - let expected = - { - SystemAssigned = Enabled - UserAssigned = [] - } + test "WebApp with slot adds settings to slot" { + let slot = appSlot { name "warm-up" } - Expect.equal (slots.[1]).Identity expected "Slot should have slot setting" + let site: WebAppConfig = webApp { + name "slotsettings" + add_slot slot + setting "setting" "some value" } - test "WebApp with slot adds settings to slot" { - let slot = appSlot { name "warm-up" } + Expect.isTrue (site.CommonWebConfig.Slots.ContainsKey "warm-up") "Config should contain slot" - let site: WebAppConfig = - webApp { - name "slotsettings" - add_slot slot - setting "setting" "some value" - } + let slots = + site + |> getResources + |> getResource + |> List.filter (fun x -> x.ResourceType = Arm.Web.slots) + // Default "production" slot is not included as it is created automatically in Azure + Expect.hasLength slots 1 "Should only be 1 slot" - Expect.isTrue (site.CommonWebConfig.Slots.ContainsKey "warm-up") "Config should contain slot" + let settings = Expect.wantSome (slots.[0]).AppSettings "AppSettings should be set" + Expect.isTrue (settings.ContainsKey("setting")) "Slot should have slot setting" + } - let slots = - site - |> getResources - |> getResource - |> List.filter (fun x -> x.ResourceType = Arm.Web.slots) - // Default "production" slot is not included as it is created automatically in Azure - Expect.hasLength slots 1 "Should only be 1 slot" + test "WebApp with slot does not add settings to app service" { + let slot = appSlot { name "warm-up" } - let settings = Expect.wantSome (slots.[0]).AppSettings "AppSettings should be set" - Expect.isTrue (settings.ContainsKey("setting")) "Slot should have slot setting" + let config = webApp { + name "web" + add_slot slot + setting "setting" "some value" + connection_string "DB" } - test "WebApp with slot does not add settings to app service" { - let slot = appSlot { name "warm-up" } + let sites = config |> getResources |> getResource - let config = - webApp { - name "web" - add_slot slot - setting "setting" "some value" - connection_string "DB" - } + // Default "production" slot is not included as it is created automatically in Azure + Expect.hasLength sites 2 "Should only be 1 slot and 1 site" - let sites = config |> getResources |> getResource + Expect.isNone ((sites.[0]).AppSettings) "App service should not have any settings" + Expect.isNone ((sites.[0]).ConnectionStrings) "App service should not have any connection strings" + } - // Default "production" slot is not included as it is created automatically in Azure - Expect.hasLength sites 2 "Should only be 1 slot and 1 site" + test "WebApp adds literal settings to slots" { + let slot = appSlot { name "warm-up" } - Expect.isNone ((sites.[0]).AppSettings) "App service should not have any settings" - Expect.isNone ((sites.[0]).ConnectionStrings) "App service should not have any connection strings" + let site: WebAppConfig = webApp { + name "web" + add_slot slot + run_from_package + website_node_default_version "xxx" + docker_ci + docker_use_azure_registry "registry" } - test "WebApp adds literal settings to slots" { - let slot = appSlot { name "warm-up" } + Expect.isTrue (site.CommonWebConfig.Slots.ContainsKey "warm-up") "Config should contain slot" - let site: WebAppConfig = - webApp { - name "web" - add_slot slot - run_from_package - website_node_default_version "xxx" - docker_ci - docker_use_azure_registry "registry" - } + let sites = site |> getResources |> getResource + let slots = sites |> List.filter (fun x -> x.ResourceType = Arm.Web.slots) + // Default "production" slot is not included as it is created automatically in Azure + Expect.hasLength slots 1 "Should only be 1 slot" - Expect.isTrue (site.CommonWebConfig.Slots.ContainsKey "warm-up") "Config should contain slot" - - let sites = site |> getResources |> getResource - let slots = sites |> List.filter (fun x -> x.ResourceType = Arm.Web.slots) - // Default "production" slot is not included as it is created automatically in Azure - Expect.hasLength slots 1 "Should only be 1 slot" - - let settings = (slots.Item 0).AppSettings |> Option.defaultValue Map.empty - - let expectation = - [ - "APPINSIGHTS_INSTRUMENTATIONKEY" - "APPINSIGHTS_PROFILERFEATURE_VERSION" - "APPINSIGHTS_SNAPSHOTFEATURE_VERSION" - "ApplicationInsightsAgent_EXTENSION_VERSION" - "DiagnosticServices_EXTENSION_VERSION" - "InstrumentationEngine_EXTENSION_VERSION" - "SnapshotDebugger_EXTENSION_VERSION" - "XDT_MicrosoftApplicationInsights_BaseExtensions" - "XDT_MicrosoftApplicationInsights_Mode" - "DOCKER_ENABLE_CI" - "DOCKER_REGISTRY_SERVER_PASSWORD" - "DOCKER_REGISTRY_SERVER_URL" - "DOCKER_REGISTRY_SERVER_USERNAME" - ] - |> List.map (settings.ContainsKey) - - Expect.allEqual expectation true "Slot should have all literal settings" - } + let settings = (slots.Item 0).AppSettings |> Option.defaultValue Map.empty - test "WebApp with different settings on slot and service adds both settings to slot" { - let slot = - appSlot { - name "warm-up" - setting "slot" "slot value" - } + let expectation = + [ + "APPINSIGHTS_INSTRUMENTATIONKEY" + "APPINSIGHTS_PROFILERFEATURE_VERSION" + "APPINSIGHTS_SNAPSHOTFEATURE_VERSION" + "ApplicationInsightsAgent_EXTENSION_VERSION" + "DiagnosticServices_EXTENSION_VERSION" + "InstrumentationEngine_EXTENSION_VERSION" + "SnapshotDebugger_EXTENSION_VERSION" + "XDT_MicrosoftApplicationInsights_BaseExtensions" + "XDT_MicrosoftApplicationInsights_Mode" + "DOCKER_ENABLE_CI" + "DOCKER_REGISTRY_SERVER_PASSWORD" + "DOCKER_REGISTRY_SERVER_URL" + "DOCKER_REGISTRY_SERVER_USERNAME" + ] + |> List.map (settings.ContainsKey) - let site: WebAppConfig = - webApp { - name "web" - add_slot slot - setting "appService" "app service value" - } + Expect.allEqual expectation true "Slot should have all literal settings" + } - Expect.isTrue (site.CommonWebConfig.Slots.ContainsKey "warm-up") "Config should contain slot" + test "WebApp with different settings on slot and service adds both settings to slot" { + let slot = appSlot { + name "warm-up" + setting "slot" "slot value" + } - let slots = - site - |> getResources - |> getResource - |> List.filter (fun x -> x.ResourceType = Arm.Web.slots) - // Default "production" slot is not included as it is created automatically in Azure - Expect.hasLength slots 1 "Should only be 1 slot" - - let settings = (slots.Item 0).AppSettings - Expect.isTrue (settings.Value.ContainsKey("slot")) "Slot should have slot setting" - Expect.isTrue (settings.Value.ContainsKey("appService")) "Slot should have app service setting" + let site: WebAppConfig = webApp { + name "web" + add_slot slot + setting "appService" "app service value" } - test "WebApp with slot, slot settings override app service setting" { - let slot = - appSlot { - name "warm-up" - setting "override" "overridden" - } + Expect.isTrue (site.CommonWebConfig.Slots.ContainsKey "warm-up") "Config should contain slot" - let site: WebAppConfig = - webApp { - name "web" - add_slot slot - setting "override" "some value" - } - - Expect.isTrue (site.CommonWebConfig.Slots.ContainsKey "warm-up") "Config should contain slot" + let slots = + site + |> getResources + |> getResource + |> List.filter (fun x -> x.ResourceType = Arm.Web.slots) + // Default "production" slot is not included as it is created automatically in Azure + Expect.hasLength slots 1 "Should only be 1 slot" - let slots = - site - |> getResources - |> getResource - |> List.filter (fun x -> x.ResourceType = Arm.Web.slots) - // Default "production" slot is not included as it is created automatically in Azure - Expect.hasLength slots 1 "Should only be 1 slot" + let settings = (slots.Item 0).AppSettings + Expect.isTrue (settings.Value.ContainsKey("slot")) "Slot should have slot setting" + Expect.isTrue (settings.Value.ContainsKey("appService")) "Slot should have app service setting" + } - let settings = Expect.wantSome slots.[0].AppSettings "AppSettings should be set" - let (hasValue, value) = settings.TryGetValue("override") + test "WebApp with slot, slot settings override app service setting" { + let slot = appSlot { + name "warm-up" + setting "override" "overridden" + } - Expect.isTrue hasValue "Slot should have app service setting" - Expect.equal value.Value "overridden" "Slot should have correct app service value" + let site: WebAppConfig = webApp { + name "web" + add_slot slot + setting "override" "some value" } - test "WebApp with slot adds connection strings to slot" { - let slot = appSlot { name "warm-up" } + Expect.isTrue (site.CommonWebConfig.Slots.ContainsKey "warm-up") "Config should contain slot" - let site: WebAppConfig = - webApp { - name "web" - add_slot slot - connection_string "connection_string" - } + let slots = + site + |> getResources + |> getResource + |> List.filter (fun x -> x.ResourceType = Arm.Web.slots) + // Default "production" slot is not included as it is created automatically in Azure + Expect.hasLength slots 1 "Should only be 1 slot" - Expect.isTrue (site.CommonWebConfig.Slots.ContainsKey "warm-up") "Config should contain slot" + let settings = Expect.wantSome slots.[0].AppSettings "AppSettings should be set" + let (hasValue, value) = settings.TryGetValue("override") - let slots = - site - |> getResources - |> getResource - |> List.filter (fun x -> x.ResourceType = Arm.Web.slots) - // Default "production" slot is not included as it is created automatically in Azure - Expect.hasLength slots 1 "Should only be 1 slot" + Expect.isTrue hasValue "Slot should have app service setting" + Expect.equal value.Value "overridden" "Slot should have correct app service value" + } - let connStrings = - Expect.wantSome slots.[0].ConnectionStrings "ConnectionStrings should be set" + test "WebApp with slot adds connection strings to slot" { + let slot = appSlot { name "warm-up" } - Expect.isTrue - (connStrings.ContainsKey("connection_string")) - "Slot should have app service connection string" + let site: WebAppConfig = webApp { + name "web" + add_slot slot + connection_string "connection_string" } - test "WebApp with different connection strings on slot and service adds both to slot" { - let slot = - appSlot { - name "warm-up" - connection_string "slot" - } + Expect.isTrue (site.CommonWebConfig.Slots.ContainsKey "warm-up") "Config should contain slot" - let site: WebAppConfig = - webApp { - name "web" - add_slot slot - connection_string "appService" - } + let slots = + site + |> getResources + |> getResource + |> List.filter (fun x -> x.ResourceType = Arm.Web.slots) + // Default "production" slot is not included as it is created automatically in Azure + Expect.hasLength slots 1 "Should only be 1 slot" - Expect.isTrue (site.CommonWebConfig.Slots.ContainsKey "warm-up") "Config should contain slot" + let connStrings = + Expect.wantSome slots.[0].ConnectionStrings "ConnectionStrings should be set" - let slots = - site - |> getResources - |> getResource - |> List.filter (fun x -> x.ResourceType = Arm.Web.slots) - // Default "production" slot is not included as it is created automatically in Azure - Expect.hasLength slots 1 "Should only be 1 slot" + Expect.isTrue + (connStrings.ContainsKey("connection_string")) + "Slot should have app service connection string" + } - let connStrings = - Expect.wantSome slots.[0].ConnectionStrings "ConnectionStrings should be set" + test "WebApp with different connection strings on slot and service adds both to slot" { + let slot = appSlot { + name "warm-up" + connection_string "slot" + } - Expect.hasLength connStrings 2 "Slot should have two connection strings" + let site: WebAppConfig = webApp { + name "web" + add_slot slot + connection_string "appService" } - test "WebApp with slots and identity applies identity to slots" { - let identity18 = userAssignedIdentity { name "im-18" } - let identity21 = userAssignedIdentity { name "im-21" } + Expect.isTrue (site.CommonWebConfig.Slots.ContainsKey "warm-up") "Config should contain slot" - let slot = - appSlot { - name "deploy" - keyvault_identity identity21 - } + let slots = + site + |> getResources + |> getResource + |> List.filter (fun x -> x.ResourceType = Arm.Web.slots) + // Default "production" slot is not included as it is created automatically in Azure + Expect.hasLength slots 1 "Should only be 1 slot" - let site: WebAppConfig = - webApp { - name "web" - add_slot slot - add_identity identity18 - } + let connStrings = + Expect.wantSome slots.[0].ConnectionStrings "ConnectionStrings should be set" - Expect.isTrue (site.CommonWebConfig.Slots.ContainsKey "deploy") "Config should contain slot" + Expect.hasLength connStrings 2 "Slot should have two connection strings" + } - let slots = - site - |> getResources - |> getResource - |> List.filter (fun x -> x.ResourceType = Arm.Web.slots) - // Default "production" slot is not included as it is created automatically in Azure - Expect.hasLength slots 1 "Should only be 1 slot" - - let theSlot = (slots.[0]) - Expect.hasLength (theSlot.Identity.UserAssigned) 2 "Slot should have 2 user-assigned identities" - - Expect.containsAll - (theSlot.Identity.UserAssigned) - [ identity18.UserAssignedIdentity; identity21.UserAssignedIdentity ] - "Slot should have both user assigned identities" - - Expect.equal - theSlot.KeyVaultReferenceIdentity - (Some identity21.UserAssignedIdentity) - "Slot should have correct keyvault identity" + test "WebApp with slots and identity applies identity to slots" { + let identity18 = userAssignedIdentity { name "im-18" } + let identity21 = userAssignedIdentity { name "im-21" } + + let slot = appSlot { + name "deploy" + keyvault_identity identity21 } - test "WebApp with slot can use AutoSwapSlotName" { - let warmupSlot = - appSlot { - name "warm-up" - autoSlotSwapName "production" - } + let site: WebAppConfig = webApp { + name "web" + add_slot slot + add_identity identity18 + } - let site: WebAppConfig = - webApp { - name "slots" - add_slot warmupSlot - } + Expect.isTrue (site.CommonWebConfig.Slots.ContainsKey "deploy") "Config should contain slot" - Expect.isTrue (site.CommonWebConfig.Slots.ContainsKey "warm-up") "Config should contain slot" + let slots = + site + |> getResources + |> getResource + |> List.filter (fun x -> x.ResourceType = Arm.Web.slots) + // Default "production" slot is not included as it is created automatically in Azure + Expect.hasLength slots 1 "Should only be 1 slot" - let slot: Site = site |> getResourceAtIndex 4 + let theSlot = (slots.[0]) + Expect.hasLength (theSlot.Identity.UserAssigned) 2 "Slot should have 2 user-assigned identities" - Expect.equal slot.Name "slots/warm-up" "Should be expected slot" - Expect.equal slot.SiteConfig.AutoSwapSlotName "production" "Should use provided auto swap slot name" + Expect.containsAll + (theSlot.Identity.UserAssigned) + [ identity18.UserAssignedIdentity; identity21.UserAssignedIdentity ] + "Slot should have both user assigned identities" + + Expect.equal + theSlot.KeyVaultReferenceIdentity + (Some identity21.UserAssignedIdentity) + "Slot should have correct keyvault identity" + } + + test "WebApp with slot can use AutoSwapSlotName" { + let warmupSlot = appSlot { + name "warm-up" + autoSlotSwapName "production" } - test "Supports private endpoints" { - let subnet = ResourceId.create (Network.subnets, ResourceName "subnet") + let site: WebAppConfig = webApp { + name "slots" + add_slot warmupSlot + } - let app = - webApp { - name "farmerWebApp" - add_private_endpoint (Managed subnet, "myWebApp-ep") - } + Expect.isTrue (site.CommonWebConfig.Slots.ContainsKey "warm-up") "Config should contain slot" - let ep: Microsoft.Azure.Management.Network.Models.PrivateEndpoint = - app |> getResourceAtIndex 4 + let slot: Site = site |> getResourceAtIndex 4 - Expect.equal ep.Name "myWebApp-ep" "Incorrect name" - Expect.hasLength ep.PrivateLinkServiceConnections.[0].GroupIds 1 "Incorrect group ids length" - Expect.equal ep.PrivateLinkServiceConnections.[0].GroupIds.[0] "sites" "Incorrect group ids" + Expect.equal slot.Name "slots/warm-up" "Should be expected slot" + Expect.equal slot.SiteConfig.AutoSwapSlotName "production" "Should use provided auto swap slot name" + } - Expect.equal - ep.PrivateLinkServiceConnections.[0].PrivateLinkServiceId - "[resourceId('Microsoft.Web/sites', 'farmerWebApp')]" - "Incorrect PrivateLinkServiceId" + test "Supports private endpoints" { + let subnet = ResourceId.create (Network.subnets, ResourceName "subnet") - Expect.equal ep.Subnet.Id (subnet.ArmExpression.Eval()) "Incorrect subnet id" + let app = webApp { + name "farmerWebApp" + add_private_endpoint (Managed subnet, "myWebApp-ep") } - test "Supports keyvault reference identity" { - let app = webApp { name "farmerWebApp" } - let site: Site = app |> getResourceAtIndex 3 - Expect.isNull site.KeyVaultReferenceIdentity "Keyvault identity should not be set" + let ep: Microsoft.Azure.Management.Network.Models.PrivateEndpoint = + app |> getResourceAtIndex 4 - let myId = userAssignedIdentity { name "myFarmerIdentity" } + Expect.equal ep.Name "myWebApp-ep" "Incorrect name" + Expect.hasLength ep.PrivateLinkServiceConnections.[0].GroupIds 1 "Incorrect group ids length" + Expect.equal ep.PrivateLinkServiceConnections.[0].GroupIds.[0] "sites" "Incorrect group ids" - let app = - webApp { - name "farmerWebApp" - keyvault_identity myId - } + Expect.equal + ep.PrivateLinkServiceConnections.[0].PrivateLinkServiceId + "[resourceId('Microsoft.Web/sites', 'farmerWebApp')]" + "Incorrect PrivateLinkServiceId" - let site: Site = app |> getResourceAtIndex 3 + Expect.equal ep.Subnet.Id (subnet.ArmExpression.Eval()) "Incorrect subnet id" + } - Expect.equal - site.KeyVaultReferenceIdentity - "[resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', 'myFarmerIdentity')]" - "Keyvault identity should not be set" - } + test "Supports keyvault reference identity" { + let app = webApp { name "farmerWebApp" } + let site: Site = app |> getResourceAtIndex 3 + Expect.isNull site.KeyVaultReferenceIdentity "Keyvault identity should not be set" - test "Validates name correctly" { - let check (v: string) m = - Expect.equal (WebAppName.Create v) (Error("Web App site names " + m)) + let myId = userAssignedIdentity { name "myFarmerIdentity" } - check "" "cannot be empty" "Name too short" - let longName = Array.init 61 (fun _ -> 'a') |> String - check longName $"max length is 60, but here is 61. The invalid value is '{longName}'" "Name too long" + let app = webApp { + name "farmerWebApp" + keyvault_identity myId + } - check - "zz!z" - "can only contain alphanumeric characters or the dash (-). The invalid value is 'zz!z'" - "Bad character allowed" + let site: Site = app |> getResourceAtIndex 3 - check "-zz" "cannot start with a dash (-). The invalid value is '-zz'" "Start with dash" - check "zz-" "cannot end with a dash (-). The invalid value is 'zz-'" "End with dash" - } + Expect.equal + site.KeyVaultReferenceIdentity + "[resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', 'myFarmerIdentity')]" + "Keyvault identity should not be set" + } - test "Not setting the web app name causes an error" { - Expect.throws - (fun () -> webApp { runtime_stack Runtime.Java11 } |> ignore) - "Not setting web app name should throw" - } + test "Validates name correctly" { + let check (v: string) m = + Expect.equal (WebAppName.Create v) (Error("Web App site names " + m)) - test "Supports health check" { - let resources = - webApp { - name "test" - health_check_path "/status" - } - |> getResources + check "" "cannot be empty" "Name too short" + let longName = Array.init 61 (fun _ -> 'a') |> String + check longName $"max length is 60, but here is 61. The invalid value is '{longName}'" "Name too long" - let wa = resources |> getResource |> List.head + check + "zz!z" + "can only contain alphanumeric characters or the dash (-). The invalid value is 'zz!z'" + "Bad character allowed" - Expect.equal wa.HealthCheckPath (Some "/status") "Health check path should be '/status'" - } + check "-zz" "cannot start with a dash (-). The invalid value is '-zz'" "Start with dash" + check "zz-" "cannot end with a dash (-). The invalid value is 'zz-'" "End with dash" + } - test "Supports secure custom domains with custom certificate" { - let webappName = "test" - let thumbprint = ArmExpression.literal "1111583E8FABEF4C0BEF694CBC41C28FB81CD111" + test "Not setting the web app name causes an error" { + Expect.throws + (fun () -> webApp { runtime_stack Runtime.Java11 } |> ignore) + "Not setting web app name should throw" + } - let resources = - webApp { - name webappName - custom_domain ("customDomain.io", thumbprint) - } - |> getResources + test "Supports health check" { + let resources = + webApp { + name "test" + health_check_path "/status" + } + |> getResources - let wa = resources |> getResource |> List.head - let nested = resources |> getResource - let expectedDomainName = "customDomain.io" + let wa = resources |> getResource |> List.head - // Testing HostnameBinding - let hostnameBinding = - nested.[0].Resources |> getResource |> List.head + Expect.equal wa.HealthCheckPath (Some "/status") "Health check path should be '/status'" + } - let expectedSslState = SslState.SslDisabled - let exepectedSiteId = (Managed(Arm.Web.sites.resourceId wa.Name)) + test "Supports secure custom domains with custom certificate" { + let webappName = "test" + let thumbprint = ArmExpression.literal "1111583E8FABEF4C0BEF694CBC41C28FB81CD111" - Expect.equal - hostnameBinding.DomainName - expectedDomainName - $"HostnameBinding domain name should have {expectedDomainName}" + let resources = + webApp { + name webappName + custom_domain ("customDomain.io", thumbprint) + } + |> getResources - Expect.equal - hostnameBinding.SslState - expectedSslState - $"HostnameBinding should have a {expectedSslState} Ssl state" + let wa = resources |> getResource |> List.head + let nested = resources |> getResource + let expectedDomainName = "customDomain.io" - Expect.equal - hostnameBinding.SiteId - exepectedSiteId - $"HostnameBinding SiteId should be {exepectedSiteId}" + // Testing HostnameBinding + let hostnameBinding = + nested.[0].Resources |> getResource |> List.head - // Testing certificate - let cert = nested.[1].Resources |> getResource |> List.head + let expectedSslState = SslState.SslDisabled + let exepectedSiteId = (Managed(Arm.Web.sites.resourceId wa.Name)) - Expect.equal - cert.DomainName - expectedDomainName - $"Certificate domain name should have {expectedDomainName}" + Expect.equal + hostnameBinding.DomainName + expectedDomainName + $"HostnameBinding domain name should have {expectedDomainName}" - // Testing hostname/certificate link. - let bindingDeployment = nested.[2] + Expect.equal + hostnameBinding.SslState + expectedSslState + $"HostnameBinding should have a {expectedSslState} Ssl state" - let innerResource = - bindingDeployment.Resources |> getResource |> List.head + Expect.equal hostnameBinding.SiteId exepectedSiteId $"HostnameBinding SiteId should be {exepectedSiteId}" - let innerExpectedSslState = SslState.SniBased thumbprint + // Testing certificate + let cert = nested.[1].Resources |> getResource |> List.head - Expect.stringStarts - bindingDeployment.DeploymentName.Value - "[concat" - "resourceGroupDeployment name should start as a valid ARM expression" + Expect.equal cert.DomainName expectedDomainName $"Certificate domain name should have {expectedDomainName}" - Expect.stringEnds - bindingDeployment.DeploymentName.Value - ")]" - "resourceGroupDeployment stage should end as a valid ARM expression" + // Testing hostname/certificate link. + let bindingDeployment = nested.[2] - Expect.equal - bindingDeployment.Resources.Length - 1 - "resourceGroupDeployment stage should only contain one resource" + let innerResource = + bindingDeployment.Resources |> getResource |> List.head - Expect.equal - bindingDeployment.Dependencies.Count - 1 - "resourceGroupDeployment stage should only contain one dependencies" + let innerExpectedSslState = SslState.SniBased thumbprint - Expect.equal - innerResource.SslState - innerExpectedSslState - $"hostnameBinding should have a {innerExpectedSslState} Ssl state inside the resourceGroupDeployment template" - } + Expect.stringStarts + bindingDeployment.DeploymentName.Value + "[concat" + "resourceGroupDeployment name should start as a valid ARM expression" - test "Supports secure custom domains with app service managed certificate" { - let webappName = "test" + Expect.stringEnds + bindingDeployment.DeploymentName.Value + ")]" + "resourceGroupDeployment stage should end as a valid ARM expression" - let resources = - webApp { - name webappName - custom_domain "customDomain.io" - } - |> getResources + Expect.equal + bindingDeployment.Resources.Length + 1 + "resourceGroupDeployment stage should only contain one resource" - let wa = resources |> getResource |> List.head - let nested = resources |> getResource - let expectedDomainName = "customDomain.io" + Expect.equal + bindingDeployment.Dependencies.Count + 1 + "resourceGroupDeployment stage should only contain one dependencies" - // Testing HostnameBinding - let hostnameBinding = - nested.[0].Resources |> getResource |> List.head + Expect.equal + innerResource.SslState + innerExpectedSslState + $"hostnameBinding should have a {innerExpectedSslState} Ssl state inside the resourceGroupDeployment template" + } - let expectedSslState = SslState.SslDisabled - let exepectedSiteId = (Managed(Arm.Web.sites.resourceId wa.Name)) + test "Supports secure custom domains with app service managed certificate" { + let webappName = "test" - Expect.equal - hostnameBinding.DomainName - expectedDomainName - $"HostnameBinding domain name should have {expectedDomainName}" + let resources = + webApp { + name webappName + custom_domain "customDomain.io" + } + |> getResources - Expect.equal - hostnameBinding.SslState - expectedSslState - $"HostnameBinding should have a {expectedSslState} Ssl state" + let wa = resources |> getResource |> List.head + let nested = resources |> getResource + let expectedDomainName = "customDomain.io" - Expect.equal - hostnameBinding.SiteId - exepectedSiteId - $"HostnameBinding SiteId should be {exepectedSiteId}" + // Testing HostnameBinding + let hostnameBinding = + nested.[0].Resources |> getResource |> List.head - // Testing certificate - let cert = nested.[1].Resources |> getResource |> List.head + let expectedSslState = SslState.SslDisabled + let exepectedSiteId = (Managed(Arm.Web.sites.resourceId wa.Name)) - Expect.equal - cert.DomainName - expectedDomainName - $"Certificate domain name should have {expectedDomainName}" + Expect.equal + hostnameBinding.DomainName + expectedDomainName + $"HostnameBinding domain name should have {expectedDomainName}" - // Testing hostname/certificate link. - let bindingDeployment = nested.[2] + Expect.equal + hostnameBinding.SslState + expectedSslState + $"HostnameBinding should have a {expectedSslState} Ssl state" - let innerResource = - bindingDeployment.Resources |> getResource |> List.head + Expect.equal hostnameBinding.SiteId exepectedSiteId $"HostnameBinding SiteId should be {exepectedSiteId}" - let innerExpectedSslState = SslState.SniBased cert.Thumbprint + // Testing certificate + let cert = nested.[1].Resources |> getResource |> List.head - Expect.equal - bindingDeployment.Resources.Length - 1 - "resourceGroupDeployment stage should only contain one resource" + Expect.equal cert.DomainName expectedDomainName $"Certificate domain name should have {expectedDomainName}" - Expect.equal - bindingDeployment.Dependencies.Count - 1 - "resourceGroupDeployment stage should only contain one dependencies" + // Testing hostname/certificate link. + let bindingDeployment = nested.[2] - Expect.equal - innerResource.SslState - innerExpectedSslState - $"hostnameBinding should have a {innerExpectedSslState} Ssl state inside the resourceGroupDeployment template" - } + let innerResource = + bindingDeployment.Resources |> getResource |> List.head - test "Supports insecure custom domains" { - let webappName = "test" + let innerExpectedSslState = SslState.SniBased cert.Thumbprint - let resources = - webApp { - name webappName - custom_domain (DomainConfig.InsecureDomain "customDomain.io") - } - |> getResources + Expect.equal + bindingDeployment.Resources.Length + 1 + "resourceGroupDeployment stage should only contain one resource" - let wa = resources |> getResource |> List.head - - //Testing HostnameBinding - let hostnameBinding = - resources - |> getResource - |> Seq.map (fun x -> getResource (x.Resources)) - |> Seq.concat - |> Seq.head - - let expectedSslState = SslState.SslDisabled - let exepectedSiteId = (Managed(Arm.Web.sites.resourceId wa.Name)) - let expectedDomainName = "customDomain.io" - - Expect.equal - hostnameBinding.DomainName - expectedDomainName - $"HostnameBinding domain name should have {expectedDomainName}" - - Expect.equal - hostnameBinding.SslState - expectedSslState - $"HostnameBinding should have a {expectedSslState} Ssl state" - - Expect.equal - hostnameBinding.SiteId - exepectedSiteId - $"HostnameBinding SiteId should be {exepectedSiteId}" - } + Expect.equal + bindingDeployment.Dependencies.Count + 1 + "resourceGroupDeployment stage should only contain one dependencies" - test "Supports multiple custom domains" { - let webappName = "test" + Expect.equal + innerResource.SslState + innerExpectedSslState + $"hostnameBinding should have a {innerExpectedSslState} Ssl state inside the resourceGroupDeployment template" + } - let resources = - webApp { - name webappName - custom_domain "secure.io" - custom_domain (DomainConfig.InsecureDomain "insecure.io") - } - |> getResources + test "Supports insecure custom domains" { + let webappName = "test" - let wa = resources |> getResource |> List.head + let resources = + webApp { + name webappName + custom_domain (DomainConfig.InsecureDomain "customDomain.io") + } + |> getResources - let exepectedSiteId = (Managed(Arm.Web.sites.resourceId wa.Name)) + let wa = resources |> getResource |> List.head - //Testing HostnameBinding - let hostnameBindings = - resources - |> getResource - |> Seq.map (fun x -> getResource (x.Resources)) - |> Seq.concat + //Testing HostnameBinding + let hostnameBinding = + resources + |> getResource + |> Seq.map (fun x -> getResource (x.Resources)) + |> Seq.concat + |> Seq.head - let secureBinding = - hostnameBindings |> Seq.filter (fun x -> x.DomainName = "secure.io") |> Seq.head + let expectedSslState = SslState.SslDisabled + let exepectedSiteId = (Managed(Arm.Web.sites.resourceId wa.Name)) + let expectedDomainName = "customDomain.io" - let insecureBinding = - hostnameBindings - |> Seq.filter (fun x -> x.DomainName = "insecure.io") - |> Seq.head + Expect.equal + hostnameBinding.DomainName + expectedDomainName + $"HostnameBinding domain name should have {expectedDomainName}" - Expect.equal secureBinding.SiteId exepectedSiteId $"HostnameBinding SiteId should be {exepectedSiteId}" + Expect.equal + hostnameBinding.SslState + expectedSslState + $"HostnameBinding should have a {expectedSslState} Ssl state" - Expect.equal - insecureBinding.SiteId - exepectedSiteId - $"HostnameBinding SiteId should be {exepectedSiteId}" - } + Expect.equal hostnameBinding.SiteId exepectedSiteId $"HostnameBinding SiteId should be {exepectedSiteId}" + } - test "Assigns correct dependencies when deploying multiple custom domains" { - let webappName = "test" + test "Supports multiple custom domains" { + let webappName = "test" - let resources = - webApp { - name webappName - custom_domains [ "secure1.io"; "secure2.io"; "secure3.io" ] - } - |> 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 - |> Seq.map (fun x -> getResource (x.Resources)) - |> Seq.concat - |> Seq.toList - - 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 - - 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}" - - // Testing dependencies. - let deployments = resources |> getResource |> Seq.toList - - let dependenciesOnOtherDeployments = - deployments - |> Seq.map (fun rg -> - rg.Dependencies - |> Seq.filter (fun dep -> deployments |> Seq.exists (fun x -> x.DeploymentName = dep.Name))) - |> Seq.map (fun deps -> deps |> Seq.map (fun dep -> dep.Name)) - |> Seq.toList - - let siteDependency = - deployments[0].Dependencies - |> Set.filter (fun x -> x.Type = wa.ResourceType) - |> Set.map (fun x -> x.Name) - |> Seq.head - - Expect.hasLength deployments 9 "Should have three deploys per custom domain" - Expect.isEmpty dependenciesOnOtherDeployments[0] "First deploy should not depend on another" - Expect.equal siteDependency.Value webappName "First deployment should have a dependency on the site" - - seq { 1..1..8 } - |> Seq.iter (fun x -> - Expect.contains - dependenciesOnOtherDeployments[x] - deployments[x - 1].ResourceId.Name - "Each subsequent deploy should depend on previous deploy") - } + let wa = resources |> getResource |> List.head - test "Supports adding ip restriction for allowed ip" { - let ip = "1.2.3.4/32" + let exepectedSiteId = (Managed(Arm.Web.sites.resourceId wa.Name)) - let resources = - webApp { - name "test" - add_allowed_ip_restriction "test-rule" ip - } - |> getResources + //Testing HostnameBinding + let hostnameBindings = + resources + |> getResource + |> Seq.map (fun x -> getResource (x.Resources)) + |> Seq.concat - let site = resources |> getResource |> List.head + let secureBinding = + hostnameBindings |> Seq.filter (fun x -> x.DomainName = "secure.io") |> Seq.head - let expectedRestriction = - IpSecurityRestriction.Create "test-rule" (IPAddressCidr.parse ip) Allow + let insecureBinding = + hostnameBindings + |> Seq.filter (fun x -> x.DomainName = "insecure.io") + |> Seq.head - Expect.equal - site.IpSecurityRestrictions - [ expectedRestriction ] - "Should add allowed ip security restriction" - } + Expect.equal secureBinding.SiteId exepectedSiteId $"HostnameBinding SiteId should be {exepectedSiteId}" - test "Supports adding ip restriction for denied ip" { - let ip = IPAddressCidr.parse "1.2.3.4/32" + Expect.equal insecureBinding.SiteId exepectedSiteId $"HostnameBinding SiteId should be {exepectedSiteId}" + } - let resources = - webApp { - name "test" - add_denied_ip_restriction "test-rule" ip - } - |> getResources + test "Assigns correct dependencies when deploying multiple custom domains" { + let webappName = "test" - let site = resources |> getResource |> List.head + let resources = + webApp { + name webappName + custom_domains [ "secure1.io"; "secure2.io"; "secure3.io" ] + } + |> getResources + + let wa = resources |> getResource |> List.head + + let exepectedSiteId = (Managed(Arm.Web.sites.resourceId wa.Name)) + + // Testing HostnameBinding + let hostnameBindings = + resources + |> getResource + |> Seq.map (fun x -> getResource (x.Resources)) + |> Seq.concat + |> Seq.toList + + 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 + + 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}" + + // Testing dependencies. + let deployments = resources |> getResource |> Seq.toList + + let dependenciesOnOtherDeployments = + deployments + |> Seq.map (fun rg -> + rg.Dependencies + |> Seq.filter (fun dep -> deployments |> Seq.exists (fun x -> x.DeploymentName = dep.Name))) + |> Seq.map (fun deps -> deps |> Seq.map (fun dep -> dep.Name)) + |> Seq.toList + + let siteDependency = + deployments[0].Dependencies + |> Set.filter (fun x -> x.Type = wa.ResourceType) + |> Set.map (fun x -> x.Name) + |> Seq.head + + Expect.hasLength deployments 9 "Should have three deploys per custom domain" + Expect.isEmpty dependenciesOnOtherDeployments[0] "First deploy should not depend on another" + Expect.equal siteDependency.Value webappName "First deployment should have a dependency on the site" + + seq { 1..1..8 } + |> Seq.iter (fun x -> + Expect.contains + dependenciesOnOtherDeployments[x] + deployments[x - 1].ResourceId.Name + "Each subsequent deploy should depend on previous deploy") + } - let expectedRestriction = IpSecurityRestriction.Create "test-rule" ip Deny + test "Supports adding ip restriction for allowed ip" { + let ip = "1.2.3.4/32" - Expect.equal - site.IpSecurityRestrictions - [ expectedRestriction ] - "Should add denied ip security restriction" - } + let resources = + webApp { + name "test" + add_allowed_ip_restriction "test-rule" ip + } + |> getResources - 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 site = resources |> getResource |> List.head - let warmupSlot = - appSlot { - name "warm-up" - add_allowed_ip_restriction "slot-rule" slotIp - } + let expectedRestriction = + IpSecurityRestriction.Create "test-rule" (IPAddressCidr.parse ip) Allow - let resources = - webApp { - name "test" - add_slot warmupSlot - add_allowed_ip_restriction "site-rule" siteIp - } - |> getResources + Expect.equal + site.IpSecurityRestrictions + [ expectedRestriction ] + "Should add allowed ip security restriction" + } - let slot = - resources - |> getResource - |> List.filter (fun x -> x.ResourceType = Arm.Web.slots) - |> List.head + test "Supports adding ip restriction for denied ip" { + let ip = IPAddressCidr.parse "1.2.3.4/32" - let site = resources |> getResource |> List.head + let resources = + webApp { + name "test" + add_denied_ip_restriction "test-rule" ip + } + |> getResources - let expectedSlotRestriction = IpSecurityRestriction.Create "slot-rule" slotIp Allow - let expectedSiteRestriction = IpSecurityRestriction.Create "site-rule" siteIp Allow + let site = resources |> getResource |> List.head - Expect.equal - slot.IpSecurityRestrictions - [ expectedSlotRestriction ] - "Slot should have correct allowed ip security restriction" + let expectedRestriction = IpSecurityRestriction.Create "test-rule" ip Deny - Expect.equal - site.IpSecurityRestrictions - [ expectedSiteRestriction ] - "Site should have correct allowed ip security restriction" - } + Expect.equal site.IpSecurityRestrictions [ expectedRestriction ] "Should add denied ip security restriction" + } - test "Linux automatically turns off logging extension" { - let wa = - webApp { - name "siteX" - operating_system Linux - } + 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 extensions = wa |> getResources |> getResource - Expect.isEmpty extensions "Should not be any extensions" + let warmupSlot = appSlot { + name "warm-up" + add_allowed_ip_restriction "slot-rule" slotIp } - test "Supports docker ports with WEBSITES_PORT" { - let wa = - webApp { - name "testApp" - docker_port 8080 - } + let resources = + webApp { + name "test" + add_slot warmupSlot + add_allowed_ip_restriction "site-rule" siteIp + } + |> getResources - let port = Expect.wantSome wa.DockerPort "Docker port should be set" - Expect.equal port 8080 "Docker port should 8080" + let slot = + resources + |> getResource + |> List.filter (fun x -> x.ResourceType = Arm.Web.slots) + |> List.head - let site = wa |> getResources |> getResource |> List.head + let site = resources |> getResource |> List.head - let settings = Expect.wantSome site.AppSettings "AppSettings should be set" - let (hasValue, value) = settings.TryGetValue("WEBSITES_PORT") + let expectedSlotRestriction = IpSecurityRestriction.Create "slot-rule" slotIp Allow + let expectedSiteRestriction = IpSecurityRestriction.Create "site-rule" siteIp Allow - Expect.isTrue hasValue "WEBSITES_PORT should be set" - Expect.equal value.Value "8080" "WEBSITES_PORT should be 8080" + Expect.equal + slot.IpSecurityRestrictions + [ expectedSlotRestriction ] + "Slot should have correct allowed ip security restriction" - let defaultWa = webApp { name "testApp" } - Expect.isNone defaultWa.DockerPort "Docker port should not be set" - } + Expect.equal + site.IpSecurityRestrictions + [ expectedSiteRestriction ] + "Site should have correct allowed ip security restriction" + } - test "Web App enables zoneRedundant in service plan" { - let resources = - webApp { - name "test" - zone_redundant Enabled - } - |> getResources + test "Linux automatically turns off logging extension" { + let wa = webApp { + name "siteX" + operating_system Linux + } - let sf = resources |> getResource |> List.head + let extensions = wa |> getResources |> getResource + Expect.isEmpty extensions "Should not be any extensions" + } - Expect.equal sf.ZoneRedundant (Some Enabled) "ZoneRedundant should be enabled" + test "Supports docker ports with WEBSITES_PORT" { + let wa = webApp { + name "testApp" + docker_port 8080 } - 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 port = Expect.wantSome wa.DockerPort "Docker port should be set" + Expect.equal port 8080 "Docker port should 8080" - 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 site = wa |> getResources |> getResource |> List.head - let vnetConnections = resources |> getResource - Expect.hasLength vnetConnections 1 "incorrect number of Vnet connections" - } + let settings = Expect.wantSome site.AppSettings "AppSettings should be set" + let (hasValue, value) = settings.TryGetValue("WEBSITES_PORT") - test "Can integrate with managed vnet" { - let vnetConfig = vnet { name "my-vnet" } + Expect.isTrue hasValue "WEBSITES_PORT should be set" + Expect.equal value.Value "8080" "WEBSITES_PORT should be 8080" - let wa = - webApp { - name "testApp" - sku WebApp.Sku.S1 - link_to_vnet (vnetConfig, ResourceName "my-subnet") - } + 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 resources = wa |> getResources - let site = resources |> getResource |> List.head - let vnet = Expect.wantSome site.LinkToSubnet "LinkToSubnet was not set" + let sf = resources |> getResource |> List.head - Expect.equal - vnet - (ViaManagedVNet((Arm.Network.virtualNetworks.resourceId "my-vnet"), ResourceName "my-subnet")) - "LinkToSubnet was incorrect" + 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 vnetConnections = resources |> getResource - Expect.hasLength vnetConnections 1 "incorrect number of Vnet connections" + let wa = webApp { + name "testApp" + sku WebApp.Sku.S1 + link_to_unmanaged_vnet subnetId } - test "Supports redefining root application directory" { - let wa = - webApp { - name "test" + 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" - add_virtual_applications - [ - virtualApplication { - virtual_path "/" - physical_path "altdirectory" - } - ] - } + 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 site = wa |> getResources |> getResource |> List.head - - let expectedVirtualApplications = - Map - [ - "/", - { - PhysicalPath = "site\\altdirectory" - PreloadEnabled = None - } - ] - - Expect.equal - site.VirtualApplications - expectedVirtualApplications - "Should add virtual application definition for root" + let wa = webApp { + name "testApp" + sku WebApp.Sku.S1 + link_to_vnet (vnetConfig, ResourceName "my-subnet") } - test "Can add startup command without docker" { - let wa: Site = - webApp { - name "test" - startup_command "foo" - } - |> getResourceAtIndex 3 + let resources = wa |> getResources + let site = resources |> getResource |> List.head + let vnet = Expect.wantSome site.LinkToSubnet "LinkToSubnet was not set" - Expect.equal wa.SiteConfig.AppCommandLine "foo" "Command line not set correctly" - } + Expect.equal + vnet + (ViaManagedVNet((Arm.Network.virtualNetworks.resourceId "my-vnet"), ResourceName "my-subnet")) + "LinkToSubnet was incorrect" - test "Supports defining additional virtual applications without changing root" { - let wa = - webApp { - name "test" + let vnetConnections = resources |> getResource + Expect.hasLength vnetConnections 1 "incorrect number of Vnet connections" + } - add_virtual_applications - [ - virtualApplication { - virtual_path "/subapp" - physical_path "wwwsubapp" - } - ] - } + test "Supports redefining root application directory" { + let wa = webApp { + name "test" - let site = wa |> getResources |> getResource |> List.head - - let expectedVirtualApplications = - Map - [ - ("/", - { - PhysicalPath = "site\\wwwroot" - PreloadEnabled = None - }), - 1u - ("/subapp", - { - PhysicalPath = "site\\wwwsubapp" - PreloadEnabled = None - }), - 1u - ] - - Expect.distribution - (site.VirtualApplications |> Seq.map (fun it -> (it.Key, it.Value))) - expectedVirtualApplications - "Should add virtual application definition for /subapp, but keep the root app around" + add_virtual_applications [ + virtualApplication { + virtual_path "/" + physical_path "altdirectory" + } + ] } - test "Supports virtual applications with preload enabled" { - let wa = - webApp { - name "test" + let site = wa |> getResources |> getResource |> List.head - add_virtual_applications - [ - virtualApplication { - virtual_path "/subapp" - physical_path "wwwroot\\subApp" - preloaded - } - ] + let expectedVirtualApplications = + Map [ + "/", + { + PhysicalPath = "site\\altdirectory" + PreloadEnabled = None } + ] - let site = wa |> getResources |> getResource |> List.head - - let expectedVirtualApplications = - Map - [ - ("/subapp", - { - PhysicalPath = "site\\wwwroot\\subApp" - PreloadEnabled = (Some true) - }), - 1u - ] - - Expect.distribution - (site.VirtualApplications |> Seq.map (fun it -> (it.Key, it.Value))) - expectedVirtualApplications - "Should add preloaded virtual application definition" - } - ] + Expect.equal + site.VirtualApplications + expectedVirtualApplications + "Should add virtual application definition for root" + } + + test "Can add startup command without docker" { + let wa: Site = + webApp { + name "test" + startup_command "foo" + } + |> getResourceAtIndex 3 + + Expect.equal wa.SiteConfig.AppCommandLine "foo" "Command line not set correctly" + } + + test "Supports defining additional virtual applications without changing root" { + let wa = webApp { + name "test" + + add_virtual_applications [ + virtualApplication { + virtual_path "/subapp" + physical_path "wwwsubapp" + } + ] + } + + let site = wa |> getResources |> getResource |> List.head + + let expectedVirtualApplications = + Map [ + ("/", + { + PhysicalPath = "site\\wwwroot" + PreloadEnabled = None + }), + 1u + ("/subapp", + { + PhysicalPath = "site\\wwwsubapp" + PreloadEnabled = None + }), + 1u + ] + + Expect.distribution + (site.VirtualApplications |> Seq.map (fun it -> (it.Key, it.Value))) + expectedVirtualApplications + "Should add virtual application definition for /subapp, but keep the root app around" + } + + test "Supports virtual applications with preload enabled" { + let wa = webApp { + name "test" + + add_virtual_applications [ + virtualApplication { + virtual_path "/subapp" + physical_path "wwwroot\\subApp" + preloaded + } + ] + } + + let site = wa |> getResources |> getResource |> List.head + + let expectedVirtualApplications = + Map [ + ("/subapp", + { + PhysicalPath = "site\\wwwroot\\subApp" + PreloadEnabled = (Some true) + }), + 1u + ] + + Expect.distribution + (site.VirtualApplications |> Seq.map (fun it -> (it.Key, it.Value))) + expectedVirtualApplications + "Should add preloaded virtual application definition" + } + ]