diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index e46c18022..e7849aac1 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,6 +1,11 @@ Release Notes ============= +## 1.9.29 +* Event Grid: Bump API version from `2022-06-15` to `2025-02-15`. +* Event Grid: Add `MonitorAlert` endpoint type for event subscriptions with configurable action groups and severity (Sev0–Sev4). +* Event Grid: Add `event_delivery_schema` keyword to configure the event delivery schema (EventGridSchema, CloudEventSchemaV1_0, CustomInputSchema). + ## 1.9.28 * Container Apps: Add `resources` operation to the `container` builder to set both CPU and memory together using a `ConsumptionPlanResources` discriminated union. This ensures only valid consumption plan resource combinations can be selected at compile time. The individual `cpu_cores` and `memory` operations remain available for use with dedicated plans. diff --git a/docs/content/api-overview/resources/event-grid.md b/docs/content/api-overview/resources/event-grid.md index 7d650fded..530f2d1a3 100644 --- a/docs/content/api-overview/resources/event-grid.md +++ b/docs/content/api-overview/resources/event-grid.md @@ -16,10 +16,12 @@ The Event Grid is a simple but powerful builder that links events from Azure ser |-|-| | topic_name | The name of the topic that will be created. | | source | Optional, defaults to the current resource group. The source of the events. See below for the full list of builder configurations that are supported. | +| event_delivery_schema | Optional. Sets the event delivery schema for all subscriptions on this topic. Valid values: `EventGridSchema` (default), `CloudEventSchemaV1_0`, `CustomInputSchema`. | | add_queue_subscriber | Adds a new storage queue subscriber. Requires the storage account config that will receive the events, the queue name and the list of events to subscribe to. | | add_webhook_subscriber| Adds a new webhook (HTTP) subscriber. Requires the web app config that will receive the event, associated URI local path and the list of events to subscribe to. Also contains an overload that takes in a Web App name and the full Uri of the webhook. | | add_eventhub_subscriber| Adds a new event hub subscriber. Requires the event hub builder config that will receive the events and the list of events to subscribe to. | | add_function_subscriber| Adds a new Azure Functions subscriber. Requires the function app, the handler name and the list of events to subscribe to. | +| add_monitor_alert_subscriber | Adds a new Azure Monitor Alert subscriber. Requires a list of action groups (either `ActionGroupConfig` list or `ResourceId` list), a severity level (`Sev0`–`Sev4`), and the list of events to subscribe to. | ### Supported Sources Farmer supports the following Event Grid sources using Farmer builders: @@ -36,15 +38,58 @@ Farmer supports the following Event Grid sources using Farmer builders: | [IotHub](iot-hub) | SystemEvents.IotHub | | [EventHub](eventhub) | SystemEvents.EventHub | -### Suported Destinations +### Supported Destinations -* EventHub (`add_eventhub_subscriber`), -* StorageQueue (`add_queue_subscriber`), -* WebHook (`add_webhook_subscriber`), -* ServiceBus Queue (`add_servicebus_queue_subscriber`), -* ServiceBus Topic (`add_servicebus_topic_subscriber`). +* EventHub (`add_eventhub_subscriber`) +* StorageQueue (`add_queue_subscriber`) +* WebHook (`add_webhook_subscriber`) +* ServiceBus Queue (`add_servicebus_queue_subscriber`) +* ServiceBus Topic (`add_servicebus_topic_subscriber`) +* Azure Monitor Alert (`add_monitor_alert_subscriber`) + +#### Examples + +The following sample routes Key Vault secret expiry events to Azure Monitor Alerts using the CloudEvent schema: + +```fsharp +open Farmer +open Farmer.Builders +open Farmer.Arm.EventGrid + +let myKeyVault = keyVault { name "mykeyvault" } + +// Reference an existing action group by resource ID +let actionGroupId = + Arm.ActionGroups.actionGroups.resourceId (ResourceName "myActionGroup") + +let kvExpiryGrid = eventGrid { + topic_name "kv-expiry-topic" + source myKeyVault + event_delivery_schema CloudEventSchemaV1_0 + add_monitor_alert_subscriber + [ actionGroupId ] + Sev3 + [ SystemEvents.KeyVault.SecretNearExpiry + SystemEvents.KeyVault.SecretExpired ] +} +``` + +Alternatively, reference a Farmer-managed action group directly: + +```fsharp +let alertGroup = actionGroup { + name "myActionGroup" + short_name "myAG" +} + +let kvExpiryGrid = eventGrid { + topic_name "kv-expiry-topic" + source myKeyVault + event_delivery_schema CloudEventSchemaV1_0 + add_monitor_alert_subscriber [ alertGroup ] Sev3 [ SystemEvents.KeyVault.SecretNearExpiry ] +} +``` -#### Example The following sample creates a source storage account that emits events on the event grid topic, whilst two destinations are created: an event hub and a storage queue, each listening for different events. ```fsharp diff --git a/src/Farmer/Arm/EventGrid.fs b/src/Farmer/Arm/EventGrid.fs index e15d4cbdc..70836b429 100644 --- a/src/Farmer/Arm/EventGrid.fs +++ b/src/Farmer/Arm/EventGrid.fs @@ -4,10 +4,10 @@ module Farmer.Arm.EventGrid open Farmer open EventGrid -let systemTopics = ResourceType("Microsoft.EventGrid/systemTopics", "2022-06-15") +let systemTopics = ResourceType("Microsoft.EventGrid/systemTopics", "2025-02-15") let eventSubscriptions = - ResourceType("Microsoft.EventGrid/systemTopics/eventSubscriptions", "2022-06-15") + ResourceType("Microsoft.EventGrid/systemTopics/eventSubscriptions", "2025-02-15") type TopicType = | TopicType of ResourceType * topic: string @@ -54,6 +54,37 @@ type AzureFunctionEndpointType = { PreferredBatchSizeInKilobytes: uint } +type MonitorAlertSeverity = + | Sev0 + | Sev1 + | Sev2 + | Sev3 + | Sev4 + + member this.ArmValue = + match this with + | Sev0 -> "Sev0" + | Sev1 -> "Sev1" + | Sev2 -> "Sev2" + | Sev3 -> "Sev3" + | Sev4 -> "Sev4" + +type MonitorAlertEndpointType = { + ActionGroups: ResourceId list + Severity: MonitorAlertSeverity +} + +type EventDeliverySchema = + | EventGridSchema + | CloudEventSchemaV1_0 + | CustomInputSchema + + member this.ArmValue = + match this with + | EventGridSchema -> "EventGridSchema" + | CloudEventSchemaV1_0 -> "CloudEventSchemaV1_0" + | CustomInputSchema -> "CustomInputSchema" + type ServiceBusEndpointType = | Queue of Queue: ServiceBusQueueEndpointType | Topic of Topic: ServiceBusTopicEndpointType @@ -64,6 +95,7 @@ type EndpointType = | StorageQueue of queue: ResourceName | ServiceBus of bus: ServiceBusEndpointType | AzureFunction of AzureFunctionEndpointType + | MonitorAlert of MonitorAlertEndpointType type Topic = { Name: ResourceName @@ -96,6 +128,7 @@ type Subscription<'T> = { Destination: ResourceName DestinationEndpoint: EndpointType Events: EventGridEvent<'T> list + EventDeliverySchema: EventDeliverySchema option } with interface IArmResource with @@ -114,6 +147,7 @@ type Subscription<'T> = { | 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)) + | MonitorAlert _ -> None {| eventSubscriptions.Create( @@ -169,11 +203,24 @@ type Subscription<'T> = { queueName = topic.Value |} |} + | MonitorAlert alert -> + {| + endpointType = "MonitorAlert" + properties = {| + actionGroups = [ + for ag in alert.ActionGroups do + ag.Eval() + ] + severity = alert.Severity.ArmValue + |} + |} + |> box filter = {| includedEventTypes = [ for event in this.Events do event.Value ] |} + eventDeliverySchema = this.EventDeliverySchema |> Option.map _.ArmValue |> Option.toObj |} |} \ No newline at end of file diff --git a/src/Farmer/Builders/Builders.EventGrid.fs b/src/Farmer/Builders/Builders.EventGrid.fs index d795339ed..2eccf3063 100644 --- a/src/Farmer/Builders/Builders.EventGrid.fs +++ b/src/Farmer/Builders/Builders.EventGrid.fs @@ -191,6 +191,7 @@ type EventGridConfig<'T> = { Endpoint: EndpointType SystemEvents: EventGridEvent<'T> list |} list + EventDeliverySchema: EventDeliverySchema option Tags: Map } with @@ -213,6 +214,7 @@ type EventGridConfig<'T> = { Destination = sub.Destination DestinationEndpoint = sub.Endpoint Events = sub.SystemEvents + EventDeliverySchema = this.EventDeliverySchema } ] @@ -221,6 +223,7 @@ type EventGridBuilder() = TopicName = state.TopicName Source = source, topic Subscriptions = [] + EventDeliverySchema = None Tags = Map.empty } @@ -243,6 +246,7 @@ type EventGridBuilder() = TopicName = ResourceName.Empty Source = ResourceName("[resourceGroup().name]"), Topics.ResourceGroup Subscriptions = [] + EventDeliverySchema = None Tags = Map.empty } @@ -373,6 +377,50 @@ type EventGridBuilder() = let name = sprintf $"%s{bus.Name.Value}-servicebus-topic" EventGridBuilder.AddSub(state, name, topic.Name, endpoint, events) + [] + member _.EventDeliverySchema(state: EventGridConfig<'T>, schema: EventDeliverySchema) = { + state with + EventDeliverySchema = Some schema + } + + [] + member _.AddMonitorAlertSubscription + ( + state: EventGridConfig<'T>, + actionGroups: ActionGroupConfig list, + severity: MonitorAlertSeverity, + events: EventGridEvent<_> list + ) = + let ids = + actionGroups + |> List.map (fun ag -> Arm.ActionGroups.actionGroups.resourceId ag.Name) + + let endpoint = + MonitorAlert { + ActionGroups = ids + Severity = severity + } + + let destination = actionGroups |> List.head |> (fun ag -> ag.Name) + EventGridBuilder.AddSub(state, destination.Value + "-monitor-alert", destination, endpoint, events) + + [] + member _.AddMonitorAlertSubscription + ( + state: EventGridConfig<'T>, + actionGroupIds: ResourceId list, + severity: MonitorAlertSeverity, + events: EventGridEvent<_> list + ) = + let endpoint = + MonitorAlert { + ActionGroups = actionGroupIds + Severity = severity + } + + let destination = actionGroupIds |> List.head |> (fun id -> id.Name) + EventGridBuilder.AddSub(state, destination.Value + "-monitor-alert", destination, endpoint, events) + [] member _.Tags(state: EventGridConfig<'T>, pairs) = { state with diff --git a/src/Tests/EventGrid.fs b/src/Tests/EventGrid.fs index 4444b07ad..a79a2d9d0 100644 --- a/src/Tests/EventGrid.fs +++ b/src/Tests/EventGrid.fs @@ -4,7 +4,9 @@ open Expecto open Farmer open Farmer.Builders open Farmer.Arm +open Farmer.Arm.EventGrid open Microsoft.Rest +open Newtonsoft.Json.Linq open System let tests = @@ -142,4 +144,131 @@ let tests = Expect.equal sub.Destination t.Name "Incorrect destination" } + test "Creates a monitor alert subscriber with ResourceId list correctly" { + let actionGroupId = + Arm.ActionGroups.actionGroups.resourceId (ResourceName "myActionGroup") + + let kv = keyVault { name "mykv" } + + let grid = eventGrid { + topic_name "my-topic" + source kv + + add_monitor_alert_subscriber [ actionGroupId ] MonitorAlertSeverity.Sev3 [ + SystemEvents.KeyVault.SecretNearExpiry + SystemEvents.KeyVault.SecretExpired + ] + } + + let sub = grid.Subscriptions.[0] + + Expect.equal + sub.Name + (ResourceName "myActionGroup-myActionGroup-monitor-alert") + "Incorrect subscription name" + + Expect.equal + sub.Endpoint + (EndpointType.MonitorAlert { + ActionGroups = [ actionGroupId ] + Severity = MonitorAlertSeverity.Sev3 + }) + "Incorrect endpoint type" + } + test "Creates a monitor alert subscriber with ActionGroupConfig correctly" { + let ag = actionGroup { + name "myActionGroup" + short_name "myAG" + } + + let kv = keyVault { name "mykv" } + + let grid = eventGrid { + topic_name "my-topic" + source kv + add_monitor_alert_subscriber [ ag ] MonitorAlertSeverity.Sev2 [ SystemEvents.KeyVault.SecretExpired ] + } + + let sub = grid.Subscriptions.[0] + + let expectedId = + Arm.ActionGroups.actionGroups.resourceId (ResourceName "myActionGroup") + + Expect.equal + sub.Endpoint + (EndpointType.MonitorAlert { + ActionGroups = [ expectedId ] + Severity = MonitorAlertSeverity.Sev2 + }) + "Incorrect endpoint type" + } + test "Event delivery schema is set on all subscription ARM resources" { + let kv = keyVault { name "mykv" } + + let actionGroupId = + Arm.ActionGroups.actionGroups.resourceId (ResourceName "myActionGroup") + + let grid = eventGrid { + topic_name "my-topic" + source kv + event_delivery_schema CloudEventSchemaV1_0 + + add_monitor_alert_subscriber [ actionGroupId ] MonitorAlertSeverity.Sev3 [ + SystemEvents.KeyVault.SecretNearExpiry + ] + } + + Expect.equal grid.EventDeliverySchema (Some CloudEventSchemaV1_0) "Incorrect delivery schema on config" + + let resources = (grid :> IBuilder).BuildResources Location.WestEurope + + let sub = + resources + |> List.pick (fun r -> + match r with + | :? Subscription as s -> Some s + | _ -> None) + + Expect.equal sub.EventDeliverySchema (Some CloudEventSchemaV1_0) "Incorrect delivery schema on ARM resource" + } + test "Monitor alert subscriber generates correct JSON" { + let kv = keyVault { name "mykv" } + + let actionGroupId = + Arm.ActionGroups.actionGroups.resourceId (ResourceName "myActionGroup") + + let grid = eventGrid { + topic_name "my-topic" + source kv + event_delivery_schema CloudEventSchemaV1_0 + + add_monitor_alert_subscriber [ actionGroupId ] MonitorAlertSeverity.Sev3 [ + SystemEvents.KeyVault.SecretNearExpiry + SystemEvents.KeyVault.SecretExpired + ] + } + + let json = (arm { add_resource grid }).Template |> Writer.toJson + let jobj = JObject.Parse json + + let sub = + jobj.SelectToken "resources[?(@.type=='Microsoft.EventGrid/systemTopics/eventSubscriptions')]" + + Expect.isNotNull sub "Event subscription resource not found" + + let endpointType = sub.SelectToken "properties.destination.endpointType" + Expect.equal (endpointType.Value()) "MonitorAlert" "Incorrect endpointType" + + let severity = sub.SelectToken "properties.destination.properties.severity" + Expect.equal (severity.Value()) "Sev3" "Incorrect severity" + + let schema = sub.SelectToken "properties.eventDeliverySchema" + Expect.equal (schema.Value()) "CloudEventSchemaV1_0" "Incorrect eventDeliverySchema" + + let actionGroups = + sub.SelectToken "properties.destination.properties.actionGroups" :?> JArray + + Expect.isNotNull actionGroups "Action groups not found" + Expect.equal actionGroups.Count 1 "Incorrect action group count" + } ] \ No newline at end of file diff --git a/src/Tests/test-data/event-grid.json b/src/Tests/test-data/event-grid.json index 5413e5ff2..db539decf 100644 --- a/src/Tests/test-data/event-grid.json +++ b/src/Tests/test-data/event-grid.json @@ -59,7 +59,7 @@ "type": "Microsoft.ServiceBus/namespaces/queues" }, { - "apiVersion": "2022-06-15", + "apiVersion": "2025-02-15", "dependsOn": [ "[resourceId('Microsoft.Storage/storageAccounts', 'isaacgriddevprac')]" ], @@ -73,7 +73,7 @@ "type": "Microsoft.EventGrid/systemTopics" }, { - "apiVersion": "2022-06-15", + "apiVersion": "2025-02-15", "dependsOn": [ "[resourceId('Microsoft.EventGrid/systemTopics', 'newblobscreated')]", "[resourceId('Microsoft.ServiceBus/namespaces/queues', 'farmereventpubservicebusns', 'events')]" @@ -96,7 +96,7 @@ "type": "Microsoft.EventGrid/systemTopics/eventSubscriptions" }, { - "apiVersion": "2022-06-15", + "apiVersion": "2025-02-15", "dependsOn": [ "[resourceId('Microsoft.EventGrid/systemTopics', 'newblobscreated')]", "[resourceId('Microsoft.Storage/storageAccounts/queueServices/queues', 'isaacgriddevprac', 'default', 'todo')]"