From 79ccbf75c15d041ab218ec648e9cc3af2759783d Mon Sep 17 00:00:00 2001 From: isaac Date: Sat, 6 Aug 2022 00:38:32 +0100 Subject: [PATCH 1/8] Work on Container Apps / Dapr / AI integration --- RELEASE_NOTES.md | 4 + src/Farmer/Arm/App.fs | 76 ++++++--- src/Farmer/Builders/Builders.ContainerApps.fs | 95 +++++++---- src/Farmer/Farmer.fsproj | 2 +- src/Tests/ContainerApps.fs | 147 ++++++++++++++---- 5 files changed, 242 insertions(+), 82 deletions(-) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index c2b6a449c..9f107c3f1 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,5 +1,9 @@ Release Notes ============= +## vNext +* Container Apps: Fix a bug whereby Dapr was not correctly turned on. +* Container Apps: Support for collections of env vars, fix ACR credentials linking. +* Container Apps: Add Dapr / App Insights integration. ## 1.7.7 * Private Endpoints: Adds `privateEndpoint` builder and option to set custom network interface name. diff --git a/src/Farmer/Arm/App.fs b/src/Farmer/Arm/App.fs index 35cc22590..1c3f88495 100644 --- a/src/Farmer/Arm/App.fs +++ b/src/Farmer/Arm/App.fs @@ -2,10 +2,10 @@ module Farmer.Arm.App open Farmer.ContainerApp -open Farmer.Identity open Farmer -let containerApps = ResourceType("Microsoft.App/containerApps", "2022-03-01") +let containerApps = + ResourceType("Microsoft.App/containerApps", "2022-03-01") let managedEnvironments = ResourceType("Microsoft.App/managedEnvironments", "2022-03-01") @@ -38,7 +38,8 @@ type ManagedEnvironmentStorage = } interface IArmResource with - member this.ResourceId = storages.resourceId this.Name + member this.ResourceId = + storages.resourceId this.Name member this.JsonModel = {| storages.Create( @@ -64,7 +65,12 @@ type ManagedEnvironmentStorage = { Name = ResourceName name Environment = env - Dependencies = Set.ofList [ env; Storage.storageAccounts.resourceId accountName.ResourceName ] + Dependencies = + Set.ofList + [ + env + Storage.storageAccounts.resourceId accountName.ResourceName + ] AzureFile = {| ShareName = share @@ -97,19 +103,23 @@ type ContainerApp = member private this.dependencies = [ - yield this.Environment + this.Environment yield! this.Dependencies yield! this.Volumes |> Seq.choose (function | KeyValue (name, Volume.AzureFileShare (_)) -> - storages.resourceId (this.Environment.Name, ResourceName name) |> Some + 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 + member private this.ResourceId = + containerApps.resourceId this.Name + + member this.SystemIdentity = + SystemIdentity this.ResourceId interface IParameters with member this.SecureParameters = @@ -125,11 +135,10 @@ type ContainerApp = ] interface IArmResource with - member this.ResourceId = containerApps.resourceId this.Name + member this.ResourceId = + containerApps.resourceId this.Name member this.JsonModel = - let usernameSecretName (resourceId: ResourceId) = $"{resourceId.Name.Value}-username" - {| containerApps.Create(this.Name, this.Location, this.dependencies) with kind = "containerapp" identity = @@ -153,7 +162,12 @@ type ContainerApp = |} | ImageRegistryAuthentication.ListCredentials resourceId -> {| - name = usernameSecretName resourceId + name = + ArmExpression + .create( + $"listCredentials({resourceId.ArmExpression.Value}, '2019-05-01').username" + ) + .Eval() value = ArmExpression .create( @@ -183,14 +197,24 @@ type ContainerApp = |} | ImageRegistryAuthentication.ListCredentials resourceId -> {| - server = $"{resourceId.Name.Value}.azurecr.io" + 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 = usernameSecretName resourceId + passwordSecretRef = + ArmExpression + .create( + $"listCredentials({resourceId.ArmExpression.Value}, '2019-05-01').username" + ) + .Eval() |} |] ingress = @@ -232,8 +256,8 @@ type ContainerApp = | SecureEnvExpression armExpr -> {| name = env.Key - value = null - secretref = armExpr.Eval() + value = armExpr.Eval() + secretref = null |} | SecureEnvValue _ -> {| @@ -267,8 +291,14 @@ type ContainerApp = |] scale = {| - minReplicas = this.Replicas |> Option.map (fun c -> c.Min) |> Option.toNullable - maxReplicas = this.Replicas |> Option.map (fun c -> c.Max) |> Option.toNullable + 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 @@ -415,7 +445,7 @@ type ContainerApp = appId = settings.AppId |} :> obj - | None -> {| enabled = false |} :> obj + | None -> {| enabled = false |} volumes = [ for key, value in Map.toSeq this.Volumes do @@ -446,12 +476,14 @@ type ManagedEnvironment = Location: Location InternalLoadBalancerState: FeatureFlag LogAnalytics: ResourceId + AppInsightsInstrumentationKey: ArmExpression option Dependencies: Set Tags: Map } interface IArmResource with - member this.ResourceId = managedEnvironments.resourceId this.Name + member this.ResourceId = + managedEnvironments.resourceId this.Name member this.JsonModel = {| managedEnvironments.Create(this.Name, this.Location, this.Dependencies, this.Tags) with @@ -460,6 +492,10 @@ type ManagedEnvironment = {| ``type`` = "managed" internalLoadBalancerEnabled = this.InternalLoadBalancerState.AsBoolean + daprAIInstrumentationKey = + this.AppInsightsInstrumentationKey + |> Option.map (fun key -> key.Eval()) + |> Option.toObj appLogsConfiguration = {| destination = "log-analytics" diff --git a/src/Farmer/Builders/Builders.ContainerApps.fs b/src/Farmer/Builders/Builders.ContainerApps.fs index c40e410c4..dbe497585 100644 --- a/src/Farmer/Builders/Builders.ContainerApps.fs +++ b/src/Farmer/Builders/Builders.ContainerApps.fs @@ -48,7 +48,8 @@ type ContainerAppConfig = Dependencies: Set } - member this.ResourceId = containerApps.resourceId this.Name + member this.ResourceId = + containerApps.resourceId this.Name member this.LatestRevisionFqdn = ArmExpression @@ -60,23 +61,29 @@ type ContainerEnvironmentConfig = Name: ResourceName InternalLoadBalancerState: FeatureFlag ContainerApps: ContainerAppConfig list + AppInsights: AppInsightsConfig option LogAnalytics: ResourceRef Dependencies: Set Tags: Map } interface IBuilder with - member this.ResourceId = managedEnvironments.resourceId this.Name + member this.ResourceId = + managedEnvironments.resourceId this.Name member this.BuildResources location = [ - let logAnalyticsResourceId = this.LogAnalytics.resourceId this + 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 } @@ -108,9 +115,21 @@ type ContainerEnvironmentConfig = Replicas = containerApp.Replicas DaprConfig = containerApp.DaprConfig Secrets = containerApp.Secrets - EnvironmentVariables = containerApp.EnvironmentVariables + 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) + Containers = + containerApp.Containers + |> List.map (fun c -> c.BuildContainer) Location = location Volumes = containerApp.Volumes Dependencies = containerApp.Dependencies @@ -132,8 +151,8 @@ type ContainerEnvironmentBuilder() = Name = ResourceName.Empty InternalLoadBalancerState = Disabled ContainerApps = [] - LogAnalytics = - ResourceRef.derived (fun cfg -> Arm.LogAnalytics.workspaces.resourceId (cfg.Name - "workspace")) + AppInsights = None + LogAnalytics = derived (fun cfg -> Arm.LogAnalytics.workspaces.resourceId (cfg.Name - "workspace")) Dependencies = Set.empty Tags = Map.empty } @@ -142,11 +161,18 @@ type ContainerEnvironmentBuilder() = [] member _.Name(state: ContainerEnvironmentConfig, name: string) = { state with Name = ResourceName name } + /// Adds the instrumentation key to each container app and configures for Dapr. + [] + 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 - LogAnalytics = ResourceRef.unmanaged (Arm.LogAnalytics.workspaces.resourceId logAnalytics.Name) + 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. @@ -170,14 +196,15 @@ type ContainerEnvironmentBuilder() = ContainerApps = containerApps @ state.ContainerApps } - /// Support for adding tags to this Container App Environment. interface ITaggable with + /// Adds a tag to this Container App Environment. member _.Add state tags = { state with Tags = state.Tags |> Map.merge tags } - /// Support for adding dependencies to this Container App Environment. + interface IDependable with + /// Adds an explicit dependency to this Container App Environment. member _.Add state newDeps = { state with Dependencies = state.Dependencies + newDeps @@ -310,7 +337,8 @@ type ContainerAppBuilder() = let state = this.AddEnvironmentVariable(state, $"scalerule-{name}-queue-name", queueName) - let secretRef = $"scalerule-{name}-connection" + let secretRef = + $"scalerule-{name}-connection" let state: ContainerAppConfig = this.AddSecretExpression(state, secretRef, storageAccount.Key) @@ -376,7 +404,7 @@ type ContainerAppBuilder() = [] member _.SetDaprAppId(state: ContainerAppConfig, appId) = { state with - DaprConfig = state.DaprConfig |> Option.map (fun c -> {| c with AppId = appId |}) + DaprConfig = Some {| AppId = appId |} } /// Sets the minimum and maximum replicas to scale the container app. @@ -397,7 +425,8 @@ type ContainerAppBuilder() = { state with ImageRegistryCredentials = state.ImageRegistryCredentials - @ (credentials |> List.map ImageRegistryAuthentication.Credential) + @ (credentials + |> List.map ImageRegistryAuthentication.Credential) } /// Reference container registries to import their admin credential at deployment time. @@ -406,7 +435,8 @@ type ContainerAppBuilder() = { state with ImageRegistryCredentials = state.ImageRegistryCredentials - @ (resourceIds |> List.map ImageRegistryAuthentication.ListCredentials) + @ (resourceIds + |> List.map ImageRegistryAuthentication.ListCredentials) } /// Adds one or more containers to the container app. @@ -426,7 +456,8 @@ type ContainerAppBuilder() = /// Adds an application secret to the Azure Container App. [] member _.AddSecretParameter(state: ContainerAppConfig, key) = - let key = (ContainerAppSettingKey.Create key).OkValue + let key = + (ContainerAppSettingKey.Create key).OkValue { state with Secrets = state.Secrets.Add(key, ParameterSecret(SecureParameter key.Value)) @@ -435,13 +466,15 @@ type ContainerAppBuilder() = /// Adds an application secrets to the Azure Container App. [] - member __.AddSecretParameters(state: ContainerAppConfig, keys: #seq<_>) = - keys |> Seq.fold (fun s k -> __.AddSecretParameter(s, k)) state + member this.AddSecretParameters(state: ContainerAppConfig, keys: #seq<_>) = + keys + |> Seq.fold (fun s k -> this.AddSecretParameter(s, k)) state /// Adds an application secret to the Azure Container App. [] member _.AddSecretExpression(state: ContainerAppConfig, key, expression) = - let key = (ContainerAppSettingKey.Create key).OkValue + let key = + (ContainerAppSettingKey.Create key).OkValue { state with Secrets = state.Secrets.Add(key, ExpressionSecret expression) @@ -454,8 +487,9 @@ type ContainerAppBuilder() = /// Adds an application secrets to the Azure Container App. [] - member __.AddSecretExpressions(state: ContainerAppConfig, xs: #seq<_>) = - xs |> Seq.fold (fun s (k, e) -> __.AddSecretExpression(s, k, e)) state + member this.AddSecretExpressions(state: ContainerAppConfig, xs: #seq<_>) = + xs + |> Seq.fold (fun s (k, e) -> this.AddSecretExpression(s, k, e)) state /// Adds a public environment variable to the Azure Container App environment variables. @@ -467,8 +501,9 @@ type ContainerAppBuilder() = /// Adds a public environment variables to the Azure Container App environment variables. [] - member __.AddEnvironmentVariables(state: ContainerAppConfig, vars: #seq<_>) = - vars |> Seq.fold (fun s (k, v) -> __.AddEnvironmentVariable(s, k, v)) state + member this.AddEnvironmentVariables(state: ContainerAppConfig, vars: #seq<_>) = + vars + |> Seq.fold (fun s (k, v) -> this.AddEnvironmentVariable(s, k, v)) state [] member this.AddSimpleContainer(state: ContainerAppConfig, dockerImage, dockerVersion) = @@ -493,8 +528,8 @@ type ContainerAppBuilder() = { state with Volumes = updatedVolumes } - /// Support for adding dependencies to this Container App. interface IDependable with + /// Adds an explicit dependency to this Container App. member _.Add state newDeps = { state with Dependencies = state.Dependencies + newDeps @@ -533,7 +568,8 @@ type ContainerBuilder() = if numCores > 2. then raiseFarmer $"'{state.ContainerName}' exceeds maximum CPU cores of 2.0 for containers in containerApps." - let roundedCpuCount = System.Math.Round(numCores, 2) * 1. + let roundedCpuCount = + System.Math.Round(numCores, 2) * 1. { state with Resources = @@ -545,7 +581,9 @@ type ContainerBuilder() = [] member _.EphemeralStorage(state: ContainerConfig, size: float) = let size = size / 1. - let roundedSize = System.Math.Round(size, 2) * 1. + + let roundedSize = + System.Math.Round(size, 2) * 1. { state with Resources = @@ -561,7 +599,8 @@ type ContainerBuilder() = if memory > 4. then raiseFarmer $"'{state.ContainerName}' exceeds maximum memory of 4.0 Gb for containers in containerApps." - let roundedMemory = System.Math.Round(memory, 2) * 1. + let roundedMemory = + System.Math.Round(memory, 2) * 1. { state with Resources = @@ -578,6 +617,8 @@ type ContainerBuilder() = |> Seq.fold (fun s (volumeName, mountPath) -> s |> Map.add volumeName mountPath) state.VolumeMounts } -let containerEnvironment = ContainerEnvironmentBuilder() +let containerEnvironment = + ContainerEnvironmentBuilder() + let containerApp = ContainerAppBuilder() let container = ContainerBuilder() diff --git a/src/Farmer/Farmer.fsproj b/src/Farmer/Farmer.fsproj index b59877cff..b1ad88329 100644 --- a/src/Farmer/Farmer.fsproj +++ b/src/Farmer/Farmer.fsproj @@ -123,6 +123,7 @@ + @@ -137,7 +138,6 @@ - diff --git a/src/Tests/ContainerApps.fs b/src/Tests/ContainerApps.fs index 09d583d13..87b333bde 100644 --- a/src/Tests/ContainerApps.fs +++ b/src/Tests/ContainerApps.fs @@ -6,15 +6,23 @@ open Farmer.Builders open Newtonsoft.Json.Linq open Farmer.ContainerApp open Farmer.Identity +open Farmer.Arm + +let msi = + createUserAssignedIdentity "appUser" -let msi = createUserAssignedIdentity "appUser" let containerRegistryName = "myregistry" let storageAccountName = "storagename" let fullContainerAppDeployment = - let containerLogs = logAnalytics { name "containerlogs" } - let containerRegistryDomain = $"{containerRegistryName}.azurecr.io" - let acr = containerRegistry { name containerRegistryName } + let containerLogs = + logAnalytics { name "containerlogs" } + + let containerRegistryDomain = + $"{containerRegistryName}.azurecr.io" + + let acr = + containerRegistry { name containerRegistryName } let storage = storageAccount { @@ -35,7 +43,11 @@ let fullContainerAppDeployment = name "http" add_identity msi active_revision_mode Single - add_registry_credentials [ registry containerRegistryDomain containerRegistryName ] + + add_registry_credentials + [ + registry containerRegistryDomain containerRegistryName + ] add_containers [ @@ -65,8 +77,16 @@ let fullContainerAppDeployment = add_http_scale_rule "http-scaler" { ConcurrentRequests = 10 } add_cpu_scale_rule "cpu-scaler" { Utilisation = 50 } add_secret_parameters [ "servicebusconnectionkey" ] - add_env_variables [ "ServiceBusQueueName", "wishrequests" ] - add_secret_expressions [ "containerlogs", containerLogs.PrimarySharedKey ] + + add_env_variables + [ + "ServiceBusQueueName", "wishrequests" + ] + + add_secret_expressions + [ + "containerlogs", containerLogs.PrimarySharedKey + ] } containerApp { name "servicebus" @@ -88,7 +108,12 @@ let fullContainerAppDeployment = container { name "servicebus" private_docker_image containerRegistryDomain "servicebus" version - add_volume_mounts [ "empty-v", "/tmp"; "certs-v", "/certs" ] + + add_volume_mounts + [ + "empty-v", "/tmp" + "certs-v", "/certs" + ] } ] @@ -113,12 +138,18 @@ let tests = testList "Container Apps" [ - let jsonTemplate = fullContainerAppDeployment.Template |> Writer.toJson + let jsonTemplate = + fullContainerAppDeployment.Template + |> Writer.toJson + let jobj = JObject.Parse jsonTemplate test "Container automatically creates a log analytics workspace" { - let env: IBuilder = containerEnvironment { name "testca" } - let resources = env.BuildResources Location.NorthEurope + let env: IBuilder = + containerEnvironment { name "testca" } + + let resources = + env.BuildResources Location.NorthEurope Expect.exists resources @@ -144,13 +175,19 @@ let tests = |> List.find (fun r -> r.ResourceId.Name.Value = "multienv") :?> Farmer.Arm.App.ContainerApp - containerApp.EnvironmentVariables.["ServiceBusQueueName"] |> ignore - containerApp.EnvironmentVariables.["servicebusconnectionkey"] |> ignore - containerApp.EnvironmentVariables.["containerlogs"] |> ignore + containerApp.EnvironmentVariables.["ServiceBusQueueName"] + |> ignore + + containerApp.EnvironmentVariables.["servicebusconnectionkey"] + |> ignore + + containerApp.EnvironmentVariables.["containerlogs"] + |> ignore } test "Full container managed environments" { - let kubeEnv = jobj.SelectToken("resources[?(@.name=='kubecontainerenv')]") + let kubeEnv = + jobj.SelectToken("resources[?(@.name=='kubecontainerenv')]") Expect.equal (kubeEnv.["type"] |> string) @@ -176,13 +213,15 @@ let tests = ) Expect.equal - (kubeEnvLogAnalyticsCustomerId.["customerId"] |> string) + (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')]") + let httpContainerApp = + jobj.SelectToken("resources[?(@.name=='http')]") Expect.equal (httpContainerApp.["type"] |> string) @@ -190,16 +229,28 @@ let tests = "Incorrect type for containerApps" Expect.equal (httpContainerApp.["kind"] |> string) "containerapp" "Incorrect kind for containerApps" - let ingress = httpContainerApp.SelectToken("properties.configuration.ingress") - Expect.isTrue (ingress.SelectToken("external") |> string |> bool.Parse) "Incorrect external ingress" + + let ingress = + httpContainerApp.SelectToken("properties.configuration.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" - 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.equal - (firstRegistry.SelectToken("passwordSecretRef") |> string) + (firstRegistry.SelectToken("passwordSecretRef") + |> string) "myregistry" "Incorrect registry password secretRef" @@ -213,7 +264,9 @@ let tests = "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" @@ -223,11 +276,14 @@ let tests = "Incorrect password parameter for registry password secret" Expect.equal - (httpContainerApp.SelectToken("properties.managedEnvironmentId") |> string) + (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 @@ -239,36 +295,48 @@ let tests = Expect.equal (httpContainer.["name"] |> string) "http" "Incorrect container name" Expect.equal - (httpContainer.SelectToken("resources.cpu") |> float) + (httpContainer.SelectToken("resources.cpu") + |> float) 0.25 "Incorrect container cpu resources" Expect.equal - (httpContainer.SelectToken("resources.memory") |> string) + (httpContainer.SelectToken("resources.memory") + |> string) "0.50Gi" "Incorrect container memory resources" Expect.equal - (httpContainer.SelectToken("resources.ephemeralStorage") |> string) + (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" - let serviceBusContainerApp = jobj.SelectToken("resources[?(@.name=='servicebus')]") - let volumes = serviceBusContainerApp.SelectToken("properties.template.volumes") + let serviceBusContainerApp = + jobj.SelectToken("resources[?(@.name=='servicebus')]") + + let volumes = + serviceBusContainerApp.SelectToken("properties.template.volumes") + Expect.hasLength volumes 2 "Expecting 2 volumes" let serviceBusContainer = - serviceBusContainerApp.SelectToken("properties.template.containers") |> Seq.head + serviceBusContainerApp.SelectToken("properties.template.containers") + |> Seq.head - let serviceBusVolumeMounts = serviceBusContainer.SelectToken("volumeMounts") + let serviceBusVolumeMounts = + serviceBusContainer.SelectToken("volumeMounts") Expect.equal - (serviceBusVolumeMounts.[1].["volumeName"] |> string) + (serviceBusVolumeMounts.[1].["volumeName"] + |> string) "empty-v" "Incorrect container volume mount" @@ -278,7 +346,8 @@ let tests = "Incorrect container volume mount" Expect.equal - (serviceBusVolumeMounts.[0].["volumeName"] |> string) + (serviceBusVolumeMounts.[0].["volumeName"] + |> string) "certs-v" "Incorrect container volume mount" @@ -336,4 +405,14 @@ let tests = .OkValue) "Container app did not have linked ACR's secret" } + + ftest "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" + } + ] From 6a75c6ba661009245044a1d7f6d523b2becec5a0 Mon Sep 17 00:00:00 2001 From: isaac Date: Sat, 6 Aug 2022 13:19:54 +0100 Subject: [PATCH 2/8] Update docs. --- .../api-overview/resources/container-apps.md | 1 + src/Farmer/Arm/App.fs | 38 ++++--------- src/Farmer/Builders/Builders.ContainerApps.fs | 53 ++++++------------- src/Tests/ContainerApps.fs | 4 +- 4 files changed, 29 insertions(+), 67 deletions(-) diff --git a/docs/content/api-overview/resources/container-apps.md b/docs/content/api-overview/resources/container-apps.md index 41c93097e..df95ef27d 100644 --- a/docs/content/api-overview/resources/container-apps.md +++ b/docs/content/api-overview/resources/container-apps.md @@ -22,6 +22,7 @@ The Container Environment builder (`containerEnvironment`) defines settings for | Keyword | Purpose | |-|-| | name | Sets the name of the container environment. | +| app_insights_instance | Specifies an App Insights instance. All apps will receive the instrumentation key as a setting, and Dapr will be configured to send logs there. | | log_analytics_instance | Specifies a Log Analytics workspace where container logs should be sent. If none is provided, one will automatically be created. | | internal_load_balancer_state | Sets whether an internal load balancer should be used for load balancing traffic to container app replicas. | | add_container | Adds a single container app to the environment. | diff --git a/src/Farmer/Arm/App.fs b/src/Farmer/Arm/App.fs index 1c3f88495..924c32937 100644 --- a/src/Farmer/Arm/App.fs +++ b/src/Farmer/Arm/App.fs @@ -4,8 +4,7 @@ module Farmer.Arm.App open Farmer.ContainerApp open Farmer -let containerApps = - ResourceType("Microsoft.App/containerApps", "2022-03-01") +let containerApps = ResourceType("Microsoft.App/containerApps", "2022-03-01") let managedEnvironments = ResourceType("Microsoft.App/managedEnvironments", "2022-03-01") @@ -38,8 +37,7 @@ type ManagedEnvironmentStorage = } interface IArmResource with - member this.ResourceId = - storages.resourceId this.Name + member this.ResourceId = storages.resourceId this.Name member this.JsonModel = {| storages.Create( @@ -65,12 +63,7 @@ type ManagedEnvironmentStorage = { Name = ResourceName name Environment = env - Dependencies = - Set.ofList - [ - env - Storage.storageAccounts.resourceId accountName.ResourceName - ] + Dependencies = Set.ofList [ env; Storage.storageAccounts.resourceId accountName.ResourceName ] AzureFile = {| ShareName = share @@ -109,17 +102,14 @@ type ContainerApp = this.Volumes |> Seq.choose (function | KeyValue (name, Volume.AzureFileShare (_)) -> - storages.resourceId (this.Environment.Name, ResourceName name) - |> Some + storages.resourceId (this.Environment.Name, ResourceName name) |> Some | _ -> None) yield! this.Identity.Dependencies ] - member private this.ResourceId = - containerApps.resourceId this.Name + member private this.ResourceId = containerApps.resourceId this.Name - member this.SystemIdentity = - SystemIdentity this.ResourceId + member this.SystemIdentity = SystemIdentity this.ResourceId interface IParameters with member this.SecureParameters = @@ -135,8 +125,7 @@ type ContainerApp = ] interface IArmResource with - member this.ResourceId = - containerApps.resourceId this.Name + member this.ResourceId = containerApps.resourceId this.Name member this.JsonModel = {| containerApps.Create(this.Name, this.Location, this.dependencies) with @@ -291,14 +280,8 @@ type ContainerApp = |] scale = {| - minReplicas = - this.Replicas - |> Option.map (fun c -> c.Min) - |> Option.toNullable - maxReplicas = - this.Replicas - |> Option.map (fun c -> c.Max) - |> Option.toNullable + 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 @@ -482,8 +465,7 @@ type ManagedEnvironment = } interface IArmResource with - member this.ResourceId = - managedEnvironments.resourceId this.Name + member this.ResourceId = managedEnvironments.resourceId this.Name member this.JsonModel = {| managedEnvironments.Create(this.Name, this.Location, this.Dependencies, this.Tags) with diff --git a/src/Farmer/Builders/Builders.ContainerApps.fs b/src/Farmer/Builders/Builders.ContainerApps.fs index dbe497585..3878965c0 100644 --- a/src/Farmer/Builders/Builders.ContainerApps.fs +++ b/src/Farmer/Builders/Builders.ContainerApps.fs @@ -48,8 +48,7 @@ type ContainerAppConfig = Dependencies: Set } - member this.ResourceId = - containerApps.resourceId this.Name + member this.ResourceId = containerApps.resourceId this.Name member this.LatestRevisionFqdn = ArmExpression @@ -68,22 +67,18 @@ type ContainerEnvironmentConfig = } interface IBuilder with - member this.ResourceId = - managedEnvironments.resourceId this.Name + member this.ResourceId = managedEnvironments.resourceId this.Name member this.BuildResources location = [ - let logAnalyticsResourceId = - this.LogAnalytics.resourceId this + 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) + AppInsightsInstrumentationKey = this.AppInsights |> Option.map (fun r -> r.InstrumentationKey) Dependencies = this.Dependencies.Add logAnalyticsResourceId Tags = this.Tags } @@ -127,9 +122,7 @@ type ContainerEnvironmentConfig = ) | None -> env ImageRegistryCredentials = containerApp.ImageRegistryCredentials - Containers = - containerApp.Containers - |> List.map (fun c -> c.BuildContainer) + Containers = containerApp.Containers |> List.map (fun c -> c.BuildContainer) Location = location Volumes = containerApp.Volumes Dependencies = containerApp.Dependencies @@ -337,8 +330,7 @@ type ContainerAppBuilder() = let state = this.AddEnvironmentVariable(state, $"scalerule-{name}-queue-name", queueName) - let secretRef = - $"scalerule-{name}-connection" + let secretRef = $"scalerule-{name}-connection" let state: ContainerAppConfig = this.AddSecretExpression(state, secretRef, storageAccount.Key) @@ -425,8 +417,7 @@ type ContainerAppBuilder() = { state with ImageRegistryCredentials = state.ImageRegistryCredentials - @ (credentials - |> List.map ImageRegistryAuthentication.Credential) + @ (credentials |> List.map ImageRegistryAuthentication.Credential) } /// Reference container registries to import their admin credential at deployment time. @@ -435,8 +426,7 @@ type ContainerAppBuilder() = { state with ImageRegistryCredentials = state.ImageRegistryCredentials - @ (resourceIds - |> List.map ImageRegistryAuthentication.ListCredentials) + @ (resourceIds |> List.map ImageRegistryAuthentication.ListCredentials) } /// Adds one or more containers to the container app. @@ -456,8 +446,7 @@ type ContainerAppBuilder() = /// Adds an application secret to the Azure Container App. [] member _.AddSecretParameter(state: ContainerAppConfig, key) = - let key = - (ContainerAppSettingKey.Create key).OkValue + let key = (ContainerAppSettingKey.Create key).OkValue { state with Secrets = state.Secrets.Add(key, ParameterSecret(SecureParameter key.Value)) @@ -467,14 +456,12 @@ type ContainerAppBuilder() = /// Adds an application secrets to the Azure Container App. [] member this.AddSecretParameters(state: ContainerAppConfig, keys: #seq<_>) = - keys - |> Seq.fold (fun s k -> this.AddSecretParameter(s, k)) state + keys |> Seq.fold (fun s k -> this.AddSecretParameter(s, k)) state /// Adds an application secret to the Azure Container App. [] member _.AddSecretExpression(state: ContainerAppConfig, key, expression) = - let key = - (ContainerAppSettingKey.Create key).OkValue + let key = (ContainerAppSettingKey.Create key).OkValue { state with Secrets = state.Secrets.Add(key, ExpressionSecret expression) @@ -488,8 +475,7 @@ type ContainerAppBuilder() = /// Adds an application secrets to the Azure Container App. [] member this.AddSecretExpressions(state: ContainerAppConfig, xs: #seq<_>) = - xs - |> Seq.fold (fun s (k, e) -> this.AddSecretExpression(s, k, e)) state + xs |> Seq.fold (fun s (k, e) -> this.AddSecretExpression(s, k, e)) state /// Adds a public environment variable to the Azure Container App environment variables. @@ -502,8 +488,7 @@ type ContainerAppBuilder() = /// Adds a public environment variables to the Azure Container App environment variables. [] member this.AddEnvironmentVariables(state: ContainerAppConfig, vars: #seq<_>) = - vars - |> Seq.fold (fun s (k, v) -> this.AddEnvironmentVariable(s, k, v)) state + vars |> Seq.fold (fun s (k, v) -> this.AddEnvironmentVariable(s, k, v)) state [] member this.AddSimpleContainer(state: ContainerAppConfig, dockerImage, dockerVersion) = @@ -568,8 +553,7 @@ type ContainerBuilder() = if numCores > 2. then raiseFarmer $"'{state.ContainerName}' exceeds maximum CPU cores of 2.0 for containers in containerApps." - let roundedCpuCount = - System.Math.Round(numCores, 2) * 1. + let roundedCpuCount = System.Math.Round(numCores, 2) * 1. { state with Resources = @@ -582,8 +566,7 @@ type ContainerBuilder() = member _.EphemeralStorage(state: ContainerConfig, size: float) = let size = size / 1. - let roundedSize = - System.Math.Round(size, 2) * 1. + let roundedSize = System.Math.Round(size, 2) * 1. { state with Resources = @@ -599,8 +582,7 @@ type ContainerBuilder() = if memory > 4. then raiseFarmer $"'{state.ContainerName}' exceeds maximum memory of 4.0 Gb for containers in containerApps." - let roundedMemory = - System.Math.Round(memory, 2) * 1. + let roundedMemory = System.Math.Round(memory, 2) * 1. { state with Resources = @@ -617,8 +599,7 @@ type ContainerBuilder() = |> Seq.fold (fun s (volumeName, mountPath) -> s |> Map.add volumeName mountPath) state.VolumeMounts } -let containerEnvironment = - ContainerEnvironmentBuilder() +let containerEnvironment = ContainerEnvironmentBuilder() let containerApp = ContainerAppBuilder() let container = ContainerBuilder() diff --git a/src/Tests/ContainerApps.fs b/src/Tests/ContainerApps.fs index 87b333bde..2fe497147 100644 --- a/src/Tests/ContainerApps.fs +++ b/src/Tests/ContainerApps.fs @@ -8,9 +8,7 @@ open Farmer.ContainerApp open Farmer.Identity open Farmer.Arm -let msi = - createUserAssignedIdentity "appUser" - +let msi = createUserAssignedIdentity "appUser" let containerRegistryName = "myregistry" let storageAccountName = "storagename" From 604b30b315f64d81472493f758b8977c2ca99bbb Mon Sep 17 00:00:00 2001 From: isaac Date: Sat, 6 Aug 2022 13:26:37 +0100 Subject: [PATCH 3/8] Update formatting --- src/Farmer/Arm/Network.fs | 4 -- src/Tests/ContainerApps.fs | 112 +++++++++++-------------------------- 2 files changed, 33 insertions(+), 83 deletions(-) diff --git a/src/Farmer/Arm/Network.fs b/src/Farmer/Arm/Network.fs index b7004ca0b..767fae5ff 100644 --- a/src/Farmer/Arm/Network.fs +++ b/src/Farmer/Arm/Network.fs @@ -502,9 +502,6 @@ type NetworkInterface = .Eval() |} |} - - - |}) |} @@ -521,7 +518,6 @@ type NetworkInterface = |} |} - type NetworkProfile = { Name: ResourceName diff --git a/src/Tests/ContainerApps.fs b/src/Tests/ContainerApps.fs index 2fe497147..43ce9ffd4 100644 --- a/src/Tests/ContainerApps.fs +++ b/src/Tests/ContainerApps.fs @@ -13,14 +13,11 @@ let containerRegistryName = "myregistry" let storageAccountName = "storagename" let fullContainerAppDeployment = - let containerLogs = - logAnalytics { name "containerlogs" } + let containerLogs = logAnalytics { name "containerlogs" } - let containerRegistryDomain = - $"{containerRegistryName}.azurecr.io" + let containerRegistryDomain = $"{containerRegistryName}.azurecr.io" - let acr = - containerRegistry { name containerRegistryName } + let acr = containerRegistry { name containerRegistryName } let storage = storageAccount { @@ -42,10 +39,7 @@ let fullContainerAppDeployment = add_identity msi active_revision_mode Single - add_registry_credentials - [ - registry containerRegistryDomain containerRegistryName - ] + add_registry_credentials [ registry containerRegistryDomain containerRegistryName ] add_containers [ @@ -76,15 +70,9 @@ let fullContainerAppDeployment = add_cpu_scale_rule "cpu-scaler" { Utilisation = 50 } add_secret_parameters [ "servicebusconnectionkey" ] - add_env_variables - [ - "ServiceBusQueueName", "wishrequests" - ] + add_env_variables [ "ServiceBusQueueName", "wishrequests" ] - add_secret_expressions - [ - "containerlogs", containerLogs.PrimarySharedKey - ] + add_secret_expressions [ "containerlogs", containerLogs.PrimarySharedKey ] } containerApp { name "servicebus" @@ -107,11 +95,7 @@ let fullContainerAppDeployment = name "servicebus" private_docker_image containerRegistryDomain "servicebus" version - add_volume_mounts - [ - "empty-v", "/tmp" - "certs-v", "/certs" - ] + add_volume_mounts [ "empty-v", "/tmp"; "certs-v", "/certs" ] } ] @@ -136,18 +120,14 @@ let tests = testList "Container Apps" [ - let jsonTemplate = - fullContainerAppDeployment.Template - |> Writer.toJson + let jsonTemplate = fullContainerAppDeployment.Template |> Writer.toJson let jobj = JObject.Parse jsonTemplate test "Container automatically creates a log analytics workspace" { - let env: IBuilder = - containerEnvironment { name "testca" } + let env: IBuilder = containerEnvironment { name "testca" } - let resources = - env.BuildResources Location.NorthEurope + let resources = env.BuildResources Location.NorthEurope Expect.exists resources @@ -173,19 +153,15 @@ let tests = |> 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')]") + let kubeEnv = jobj.SelectToken("resources[?(@.name=='kubecontainerenv')]") Expect.equal (kubeEnv.["type"] |> string) @@ -211,15 +187,13 @@ let tests = ) Expect.equal - (kubeEnvLogAnalyticsCustomerId.["customerId"] - |> string) + (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')]") + let httpContainerApp = jobj.SelectToken("resources[?(@.name=='http')]") Expect.equal (httpContainerApp.["type"] |> string) @@ -228,27 +202,20 @@ let tests = 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" - 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.equal - (firstRegistry.SelectToken("passwordSecretRef") - |> string) + (firstRegistry.SelectToken("passwordSecretRef") |> string) "myregistry" "Incorrect registry password secretRef" @@ -262,8 +229,7 @@ let tests = "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" @@ -274,13 +240,11 @@ let tests = "Incorrect password parameter for registry password secret" Expect.equal - (httpContainerApp.SelectToken("properties.managedEnvironmentId") - |> string) + (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 @@ -293,48 +257,39 @@ let tests = Expect.equal (httpContainer.["name"] |> string) "http" "Incorrect container name" Expect.equal - (httpContainer.SelectToken("resources.cpu") - |> float) + (httpContainer.SelectToken("resources.cpu") |> float) 0.25 "Incorrect container cpu resources" Expect.equal - (httpContainer.SelectToken("resources.memory") - |> string) + (httpContainer.SelectToken("resources.memory") |> string) "0.50Gi" "Incorrect container memory resources" Expect.equal - (httpContainer.SelectToken("resources.ephemeralStorage") - |> string) + (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" - 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" let serviceBusContainer = - serviceBusContainerApp.SelectToken("properties.template.containers") - |> Seq.head + serviceBusContainerApp.SelectToken("properties.template.containers") |> Seq.head - let serviceBusVolumeMounts = - serviceBusContainer.SelectToken("volumeMounts") + let serviceBusVolumeMounts = serviceBusContainer.SelectToken("volumeMounts") Expect.equal - (serviceBusVolumeMounts.[1].["volumeName"] - |> string) + (serviceBusVolumeMounts.[1].["volumeName"] |> string) "empty-v" "Incorrect container volume mount" @@ -344,8 +299,7 @@ let tests = "Incorrect container volume mount" Expect.equal - (serviceBusVolumeMounts.[0].["volumeName"] - |> string) + (serviceBusVolumeMounts.[0].["volumeName"] |> string) "certs-v" "Incorrect container volume mount" From 8ac2b3e81ce0ac4052fc2d7dbf33323ecd04a2c3 Mon Sep 17 00:00:00 2001 From: isaac Date: Sat, 6 Aug 2022 13:41:21 +0100 Subject: [PATCH 4/8] Turn off focused test (!) --- src/Tests/ContainerApps.fs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Tests/ContainerApps.fs b/src/Tests/ContainerApps.fs index 43ce9ffd4..d4f9b3205 100644 --- a/src/Tests/ContainerApps.fs +++ b/src/Tests/ContainerApps.fs @@ -358,7 +358,7 @@ let tests = "Container app did not have linked ACR's secret" } - ftest "Turns on Dapr" { + test "Turns on Dapr" { let containerApp = fullContainerAppDeployment.Template.Resources |> List.find (fun r -> r.ResourceId.Name.Value = "http") @@ -366,5 +366,4 @@ let tests = Expect.isSome containerApp.DaprConfig "Dapr config was not set" } - ] From 3b9607e07fbdaa1cb4bb7f4c8784a6f60132fb79 Mon Sep 17 00:00:00 2001 From: isaac Date: Sat, 6 Aug 2022 14:02:25 +0100 Subject: [PATCH 5/8] AI / Dapr test. --- src/Tests/ContainerApps.fs | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/src/Tests/ContainerApps.fs b/src/Tests/ContainerApps.fs index d4f9b3205..c0eb214ca 100644 --- a/src/Tests/ContainerApps.fs +++ b/src/Tests/ContainerApps.fs @@ -15,6 +15,12 @@ let storageAccountName = "storagename" let fullContainerAppDeployment = let containerLogs = logAnalytics { name "containerlogs" } + let insights = + appInsights { + name "appinsights" + log_analytics_workspace containerLogs + } + let containerRegistryDomain = $"{containerRegistryName}.azurecr.io" let acr = containerRegistry { name containerRegistryName } @@ -31,6 +37,7 @@ let fullContainerAppDeployment = containerEnvironment { name "kubecontainerenv" log_analytics_instance containerLogs + app_insights_instance insights add_containers [ @@ -366,4 +373,26 @@ let tests = 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" + } ] From bc30a125db1964d87adcb7e2446c2dd60f873a65 Mon Sep 17 00:00:00 2001 From: Isaac Abraham Date: Fri, 10 Feb 2023 20:39:38 +0000 Subject: [PATCH 6/8] Tiny cleanup --- src/Farmer/Arm/App.fs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Farmer/Arm/App.fs b/src/Farmer/Arm/App.fs index 2b90328be..875fd1e54 100644 --- a/src/Farmer/Arm/App.fs +++ b/src/Farmer/Arm/App.fs @@ -171,10 +171,10 @@ type ContainerApp = {| name = cred.Server value = - if cred.Identity.Dependencies.Length > 0 then - cred.Identity.Dependencies.Head.ArmExpression.Eval() - else - String.Empty + match cred.Identity.Dependencies with + | [] -> String.Empty + | primaryDependency :: _ -> + primaryDependency.ArmExpression.Eval() |} for setting in this.Secrets do {| From aa9c572620fa273657f14e15cfee432e0140b23c Mon Sep 17 00:00:00 2001 From: Isaac Abraham Date: Fri, 10 Feb 2023 21:18:50 +0000 Subject: [PATCH 7/8] Fix password secret ref --- src/Farmer/Arm/App.fs | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/src/Farmer/Arm/App.fs b/src/Farmer/Arm/App.fs index 875fd1e54..438061b4c 100644 --- a/src/Farmer/Arm/App.fs +++ b/src/Farmer/Arm/App.fs @@ -142,6 +142,9 @@ type ContainerApp = {| managedEnvironmentId = this.Environment.Eval() configuration = + let buildPasswordRef (resourceId: ResourceId) = + $"password-for-%s{resourceId.Name.Value}-registry" + {| secrets = [| @@ -154,12 +157,7 @@ type ContainerApp = |} | ImageRegistryAuthentication.ListCredentials resourceId -> {| - name = - ArmExpression - .create( - $"listCredentials({resourceId.ArmExpression.Value}, '2019-05-01').username" - ) - .Eval() + name = buildPasswordRef resourceId value = ArmExpression .create( @@ -211,12 +209,7 @@ type ContainerApp = $"listCredentials({resourceId.ArmExpression.Value}, '2019-05-01').username" ) .Eval() - passwordSecretRef = - ArmExpression - .create( - $"listCredentials({resourceId.ArmExpression.Value}, '2019-05-01').username" - ) - .Eval() + passwordSecretRef = buildPasswordRef resourceId identity = null |} | ImageRegistryAuthentication.ManagedIdentityCredential cred -> From 4ccce46a8a00b303abe5ac625fa4a53ab54efb15 Mon Sep 17 00:00:00 2001 From: Isaac Abraham Date: Fri, 10 Feb 2023 21:47:39 +0000 Subject: [PATCH 8/8] Docs --- docs/content/api-overview/resources/container-apps.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/content/api-overview/resources/container-apps.md b/docs/content/api-overview/resources/container-apps.md index 9caa8688f..5a2662b98 100644 --- a/docs/content/api-overview/resources/container-apps.md +++ b/docs/content/api-overview/resources/container-apps.md @@ -27,6 +27,7 @@ The Container Environment builder (`containerEnvironment`) defines settings for | internal_load_balancer_state | Sets whether an internal load balancer should be used for load balancing traffic to container app replicas. | | add_container | Adds a single container app to the environment. | | add_containers | Adds one or more container apps to the environment. | +| app_insights_instance | Links an App Insights instance to this environment. All containers will be configured to use this AI instance, as well as DAPR. | > Also supports Tagging and Dependencies.