components-contrib/bindings/azure/eventgrid/eventgrid.go

283 lines
8.3 KiB
Go

/*
Copyright 2021 The Dapr Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package eventgrid
import (
"context"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"time"
"github.com/Azure/azure-sdk-for-go/services/eventgrid/mgmt/2021-12-01/eventgrid"
"github.com/Azure/go-autorest/autorest/azure/auth"
"github.com/valyala/fasthttp"
"github.com/dapr/components-contrib/bindings"
"github.com/dapr/kit/logger"
)
// AzureEventGrid allows sending/receiving Azure Event Grid events.
type AzureEventGrid struct {
metadata *azureEventGridMetadata
logger logger.Logger
userAgent string
}
type azureEventGridMetadata struct {
// Component Name
Name string
// Required Input Binding Metadata
TenantID string `json:"tenantId"`
SubscriptionID string `json:"subscriptionId"`
ClientID string `json:"clientId"`
ClientSecret string `json:"clientSecret"`
SubscriberEndpoint string `json:"subscriberEndpoint"`
HandshakePort string `json:"handshakePort"`
Scope string `json:"scope"`
// Optional Input Binding Metadata
EventSubscriptionName string `json:"eventSubscriptionName"`
// Required Output Binding Metadata
AccessKey string `json:"accessKey"`
TopicEndpoint string `json:"topicEndpoint"`
}
// NewAzureEventGrid returns a new Azure Event Grid instance.
func NewAzureEventGrid(logger logger.Logger) *AzureEventGrid {
return &AzureEventGrid{logger: logger}
}
// Init performs metadata init.
func (a *AzureEventGrid) Init(metadata bindings.Metadata) error {
a.userAgent = "dapr-" + logger.DaprVersion
m, err := a.parseMetadata(metadata)
if err != nil {
return err
}
a.metadata = m
return nil
}
func (a *AzureEventGrid) Read(handler func(*bindings.ReadResponse) ([]byte, error)) error {
err := a.ensureInputBindingMetadata()
if err != nil {
return err
}
err = a.createSubscription()
if err != nil {
return err
}
m := func(ctx *fasthttp.RequestCtx) {
if string(ctx.Path()) == "/api/events" {
switch string(ctx.Method()) {
case "OPTIONS":
ctx.Response.Header.Add("WebHook-Allowed-Origin", string(ctx.Request.Header.Peek("WebHook-Request-Origin")))
ctx.Response.Header.Add("WebHook-Allowed-Rate", "*")
ctx.Response.Header.SetStatusCode(fasthttp.StatusOK)
_, err = ctx.Response.BodyWriter().Write([]byte(""))
if err != nil {
a.logger.Error(err.Error())
}
case "POST":
bodyBytes := ctx.PostBody()
_, err = handler(&bindings.ReadResponse{
Data: bodyBytes,
})
if err != nil {
a.logger.Error(err.Error())
ctx.Error(err.Error(), fasthttp.StatusInternalServerError)
}
}
}
}
a.logger.Debugf("About to start listening for Event Grid events at http://localhost:%s/api/events", a.metadata.HandshakePort)
err = fasthttp.ListenAndServe(fmt.Sprintf(":%s", a.metadata.HandshakePort), m)
if err != nil {
return err
}
return nil
}
func (a *AzureEventGrid) Operations() []bindings.OperationKind {
return []bindings.OperationKind{bindings.CreateOperation}
}
func (a *AzureEventGrid) Invoke(req *bindings.InvokeRequest) (*bindings.InvokeResponse, error) {
err := a.ensureOutputBindingMetadata()
if err != nil {
a.logger.Error(err.Error())
return nil, err
}
request := fasthttp.AcquireRequest()
defer fasthttp.ReleaseRequest(request)
request.Header.SetMethod(fasthttp.MethodPost)
request.Header.Set("Content-Type", "application/cloudevents+json")
request.Header.Set("aeg-sas-key", a.metadata.AccessKey)
request.Header.Set("User-Agent", a.userAgent)
request.SetRequestURI(a.metadata.TopicEndpoint)
request.SetBody(req.Data)
response := fasthttp.AcquireResponse()
defer fasthttp.ReleaseResponse(response)
client := &fasthttp.Client{WriteTimeout: time.Second * 10}
err = client.Do(request, response)
if err != nil {
a.logger.Error(err.Error())
return nil, err
}
if response.StatusCode() != fasthttp.StatusOK {
body := response.Body()
a.logger.Error(string(body))
return nil, errors.New(string(body))
}
a.logger.Debugf("Successfully posted event to %s", a.metadata.TopicEndpoint)
return nil, nil
}
func (a *AzureEventGrid) ensureInputBindingMetadata() error {
if a.metadata.TenantID == "" {
return errors.New("metadata field 'TenantID' is empty in EventGrid binding")
}
if a.metadata.SubscriptionID == "" {
return errors.New("metadata field 'SubscriptionID' is empty in EventGrid binding")
}
if a.metadata.ClientID == "" {
return errors.New("metadata field 'ClientID' is empty in EventGrid binding")
}
if a.metadata.ClientSecret == "" {
return errors.New("metadata field 'ClientSecret' is empty in EventGrid binding")
}
if a.metadata.SubscriberEndpoint == "" {
return errors.New("metadata field 'SubscriberEndpoint' is empty in EventGrid binding")
}
if a.metadata.HandshakePort == "" {
return errors.New("metadata field 'HandshakePort' is empty in EventGrid binding")
}
if a.metadata.Scope == "" {
return errors.New("metadata field 'Scope' is empty in EventGrid binding")
}
return nil
}
func (a *AzureEventGrid) ensureOutputBindingMetadata() error {
if a.metadata.AccessKey == "" {
msg := fmt.Sprintf("metadata field 'AccessKey' is empty in EventGrid binding (%s)", a.metadata.Name)
return errors.New(msg)
}
if a.metadata.TopicEndpoint == "" {
msg := fmt.Sprintf("metadata field 'TopicEndpoint' is empty in EventGrid binding (%s)", a.metadata.Name)
return errors.New(msg)
}
return nil
}
func (a *AzureEventGrid) parseMetadata(metadata bindings.Metadata) (*azureEventGridMetadata, error) {
b, err := json.Marshal(metadata.Properties)
if err != nil {
return nil, err
}
var eventGridMetadata azureEventGridMetadata
err = json.Unmarshal(b, &eventGridMetadata)
if err != nil {
return nil, err
}
eventGridMetadata.Name = metadata.Name
if eventGridMetadata.HandshakePort == "" {
eventGridMetadata.HandshakePort = "8080"
}
if eventGridMetadata.EventSubscriptionName == "" {
eventGridMetadata.EventSubscriptionName = metadata.Name
}
return &eventGridMetadata, nil
}
func (a *AzureEventGrid) createSubscription() error {
clientCredentialsConfig := auth.NewClientCredentialsConfig(a.metadata.ClientID, a.metadata.ClientSecret, a.metadata.TenantID)
subscriptionClient := eventgrid.NewEventSubscriptionsClient(a.metadata.SubscriptionID)
subscriptionClient.AddToUserAgent(a.userAgent)
authorizer, err := clientCredentialsConfig.Authorizer()
if err != nil {
return err
}
subscriptionClient.Authorizer = authorizer
eventInfo := eventgrid.EventSubscription{
EventSubscriptionProperties: &eventgrid.EventSubscriptionProperties{
Destination: eventgrid.WebHookEventSubscriptionDestination{
EndpointType: eventgrid.EndpointTypeWebHook,
WebHookEventSubscriptionDestinationProperties: &eventgrid.WebHookEventSubscriptionDestinationProperties{
EndpointURL: &a.metadata.SubscriberEndpoint,
},
},
EventDeliverySchema: eventgrid.EventDeliverySchemaCloudEventSchemaV10,
},
}
a.logger.Debugf("Attempting to create or update Event Grid subscription. scope=%s endpointURL=%s", a.metadata.Scope, a.metadata.SubscriberEndpoint)
result, err := subscriptionClient.CreateOrUpdate(context.Background(), a.metadata.Scope, a.metadata.EventSubscriptionName, eventInfo)
if err != nil {
a.logger.Debugf("Failed to create or update Event Grid subscription: %v", err)
return err
}
res := result.FutureAPI.Response()
if res.StatusCode != fasthttp.StatusCreated {
bodyBytes, err := ioutil.ReadAll(res.Body)
if err != nil {
a.logger.Debugf("Failed reading error body when creating or updating Event Grid subscription: %v", err)
return err
}
bodyStr := string(bodyBytes)
a.logger.Debugf("Code HTTP %d in response to create or update Event Grid subscription: %s", res.StatusCode, bodyStr)
return errors.New(bodyStr)
}
a.logger.Debugf("Succeeded to create or update Event Grid subscription. scope=%s endpointURL=%s", a.metadata.Scope, a.metadata.SubscriberEndpoint)
return nil
}