Skip to content

Commit

Permalink
Provide capability to push events to Azure Event Grid via API (#29)
Browse files Browse the repository at this point in the history
- Provide capability to push events to Azure Event Grid via API
- Provide unit tests
- Provide integration tests
- Validation for configuration

Relates to #24 	and running integration tests in CI will be done via #28 	.

This PR depends on arcus-azure/arcus.eventgrid#65 as it's now using the preview package on MyGet and should be the official one instead.
  • Loading branch information
tomkerkhove committed Jan 28, 2019
1 parent bb5af0b commit f1c19a0
Show file tree
Hide file tree
Showing 27 changed files with 879 additions and 24 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="Arcus.EventGrid.Publishing" Version="2.3.0" />
<PackageReference Include="Microsoft.AspNetCore.App" />
<PackageReference Include="Microsoft.AspNetCore.Razor.Design" Version="2.2.0" PrivateAssets="All" />
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.1.1" />
Expand Down
49 changes: 40 additions & 9 deletions src/Arcus.EventGrid.Proxy.Api/Controllers/v1/EventsController.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
using System.ComponentModel.DataAnnotations;
using System;
using System.ComponentModel.DataAnnotations;
using System.Net;
using System.Threading.Tasks;
using Arcus.EventGrid.Publishing.Interfaces;
using GuardNet;
using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json;
using Swashbuckle.AspNetCore.Annotations;

namespace Arcus.EventGrid.Proxy.Api.Controllers.v1
Expand All @@ -9,24 +14,50 @@ namespace Arcus.EventGrid.Proxy.Api.Controllers.v1
[ApiController]
public class EventsController : ControllerBase
{
private readonly IEventGridPublisher _eventGridPublisher;

public EventsController(IEventGridPublisher eventGridPublisher)
{
Guard.NotNull(eventGridPublisher, nameof(eventGridPublisher));
_eventGridPublisher = eventGridPublisher;
}

/// <summary>
/// Emit Event
/// </summary>
/// <remarks>Sends an event to Azure Event Grid Topic</remarks>
/// <remarks>Sends an event to an Azure Event Grid Topic</remarks>
/// <param name="eventType">Type of the event</param>
/// <param name="eventPayload">Event payload</param>
/// <param name="eventId">Id of the event</param>
/// <param name="eventSubject">Subject of the event</param>
/// <param name="eventTimestamp">Timestamp of the event</param>
/// <param name="dataVersion">Data version</param>
/// <param name="eventTimestamp">Timestamp of the event. Example of preferred format is '2019-01-21T10:25:09.2292418+01:00'</param>
/// <param name="dataVersion">Version of the data payload schema</param>
[Route("{eventType}")]
[HttpPost]
[SwaggerOperation(OperationId = "Events_Emit")]
[SwaggerResponse((int)HttpStatusCode.NoContent, Description = "Event was successfully emitted")]
[SwaggerResponse((int)HttpStatusCode.InternalServerError, Description = "Unable to emit event due to internal error")]
public IActionResult Emit(string eventType, [FromBody, Required] object eventPayload, string eventId, string eventSubject, string eventTimestamp, string dataVersion)
[SwaggerResponse((int) HttpStatusCode.NoContent, Description = "Event was successfully emitted")]
[SwaggerResponse((int) HttpStatusCode.BadRequest, Description = "The request to emit an event was not valid")]
[SwaggerResponse((int) HttpStatusCode.InternalServerError, Description = "Unable to emit event due to internal error")]
public async Task<IActionResult> Emit(string eventType, [FromBody, Required] object eventPayload, string eventId, string eventTimestamp, string eventSubject = "/", string dataVersion = "1.0")
{
return StatusCode((int)HttpStatusCode.NotImplemented);
eventId = string.IsNullOrWhiteSpace(eventId) ? Guid.NewGuid().ToString() : eventId;
eventTimestamp = string.IsNullOrWhiteSpace(eventTimestamp) ? DateTimeOffset.UtcNow.ToString(format: "O") : eventTimestamp;

var rawEventPayload = JsonConvert.SerializeObject(eventPayload);

if (DateTimeOffset.TryParse(eventTimestamp, out var parsedEventTimeStamp) == false)
{
return BadRequest($"Unable to parse specified event timestamp '{eventTimestamp}'");
}

await _eventGridPublisher.PublishRaw(eventId, eventType, rawEventPayload, eventSubject, dataVersion, parsedEventTimeStamp);

Response.Headers.Add(Headers.Response.Events.Id, eventId);
Response.Headers.Add(Headers.Response.Events.Subject, eventSubject);
Response.Headers.Add(Headers.Response.Events.Timestamp, eventTimestamp);
Response.Headers.Add(Headers.Response.Events.DataVersion, dataVersion);

return NoContent();
}
}
}
}
6 changes: 6 additions & 0 deletions src/Arcus.EventGrid.Proxy.Api/EnvironmentVariables.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ public class Ports
{
public const string Http = "ARCUS_HTTP_PORT";
}

public class EventGrid
{
public const string TopicEndpoint = "ARCUS_EVENTGRID_TOPICENDPOINT";
public const string AuthKey = "ARCUS_EVENTGRID_AUTHKEY";
}
}
}
}
16 changes: 16 additions & 0 deletions src/Arcus.EventGrid.Proxy.Api/Headers.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
namespace Arcus.EventGrid.Proxy.Api
{
public class Headers
{
public class Response
{
public class Events
{
public const string Id = "X-Event-Id";
public const string Subject = "X-Event-Subject";
public const string Timestamp = "X-Event-Timestamp";
public const string DataVersion = "X-Event-Data-Version";
}
}
}
}
43 changes: 40 additions & 3 deletions src/Arcus.EventGrid.Proxy.Api/Open-Api-Docs.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 2 additions & 5 deletions src/Arcus.EventGrid.Proxy.Api/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,7 @@ public static IWebHost BuildWebHost(string[] args)
var httpEndpointUrl = $"http://+:{httpPort}";

return WebHost.CreateDefaultBuilder(args)
.UseKestrel(kestrelServerOptions =>
{
kestrelServerOptions.AddServerHeader = false;
})
.UseKestrel(kestrelServerOptions => { kestrelServerOptions.AddServerHeader = false; })
.UseUrls(httpEndpointUrl)
.UseStartup<Startup>()
.Build();
Expand All @@ -53,4 +50,4 @@ private static int DetermineHttpPort()
return 80;
}
}
}
}
44 changes: 37 additions & 7 deletions src/Arcus.EventGrid.Proxy.Api/Startup.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
using Arcus.EventGrid.Proxy.Api.Extensions;
using System;
using System.Linq;
using Arcus.EventGrid.Proxy.Api.Validation;
using Arcus.EventGrid.Publishing;
using Arcus.EventGrid.Publishing.Interfaces;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;

namespace Arcus.EventGrid.Proxy.Api
{
Expand All @@ -20,14 +27,27 @@ public Startup(IConfiguration configuration)
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc()
.AddJsonOptions(jsonOptions =>
{
jsonOptions.SerializerSettings.Converters.Add(new Newtonsoft.Json.Converters.StringEnumConverter());
jsonOptions.SerializerSettings.NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore;
})
.SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
.AddJsonOptions(jsonOptions =>
{
jsonOptions.SerializerSettings.Converters.Add(new StringEnumConverter());
jsonOptions.SerializerSettings.NullValueHandling = NullValueHandling.Ignore;
})
.SetCompatibilityVersion(CompatibilityVersion.Version_2_2);

services.UseOpenApiSpecifications();
services.AddSingleton(BuildEventGridPublisher);

ValidateConfiguration();
}

private void ValidateConfiguration()
{
var validationOutcomes = RuntimeValidator.Run(Configuration);

if (validationOutcomes.Any(validationOutcome => validationOutcome.Successful == false))
{
throw new Exception("Unable to start up due to invalid configuration");
}
}

// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
Expand All @@ -41,5 +61,15 @@ public void Configure(IApplicationBuilder app, IHostingEnvironment env)
app.UseMvc();
app.UseOpenApiDocsWithExplorer();
}

private IEventGridPublisher BuildEventGridPublisher(IServiceProvider serviceProvider)
{
var rawTopicEndpoint = Configuration[EnvironmentVariables.Runtime.EventGrid.TopicEndpoint];
var authenticationKey = Configuration[EnvironmentVariables.Runtime.EventGrid.AuthKey];
var topicUri = new Uri(rawTopicEndpoint);
return EventGridPublisherBuilder.ForTopic(topicUri)
.UsingAuthenticationKey(authenticationKey)
.Build();
}
}
}
}
45 changes: 45 additions & 0 deletions src/Arcus.EventGrid.Proxy.Api/Validation/RuntimeValidator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
using System;
using System.Collections.Generic;
using Arcus.EventGrid.Proxy.Api.Validation.Steps;
using Arcus.EventGrid.Proxy.Api.Validation.Steps.Interfaces;
using Microsoft.Extensions.Configuration;

namespace Arcus.EventGrid.Proxy.Api.Validation
{
public class RuntimeValidator
{
public static List<ValidationResult> Run(IConfiguration configuration)
{
var validationSteps = new List<IValidationStep>
{
new EventGridAuthKeyValidationStep(configuration),
new EventGridTopicEndpointValidationStep(configuration)
};

Console.WriteLine("Starting validation of runtime configuration...");
List<ValidationResult> validationOutcomes = new List<ValidationResult>();
foreach (var validationStep in validationSteps)
{
var validationOutcome = validationStep.Execute();
validationOutcomes.Add(validationOutcome);
}

validationOutcomes.ForEach(LogValidationResults);
Console.WriteLine("Validation of runtime configuration completed.");

return validationOutcomes;
}

private static void LogValidationResults(ValidationResult validationResult)
{
var validationOutcomeMessage = validationResult.Successful ? "successful" : "failed";
var validationInfoMessage = $"Validation step '{validationResult.StepName}' {validationOutcomeMessage}.";
if (validationResult.Successful == false)
{
validationInfoMessage += $" Error: {validationResult.ErrorMessage}";
}

Console.WriteLine(validationInfoMessage);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
using Arcus.EventGrid.Proxy.Api.Validation.Steps.Interfaces;
using Microsoft.Extensions.Configuration;

namespace Arcus.EventGrid.Proxy.Api.Validation.Steps
{
public class EventGridAuthKeyValidationStep : IValidationStep
{
private readonly IConfiguration _configuration;
public string StepName { get; } = "Event Grid Authentication Key";

public EventGridAuthKeyValidationStep(IConfiguration configuration)
{
_configuration = configuration;
}

public ValidationResult Execute()
{
var authKey = _configuration[EnvironmentVariables.Runtime.EventGrid.AuthKey];

if (string.IsNullOrWhiteSpace(authKey))
{
return ValidationResult.Failure(StepName, "No authentication key was specified");
}

return ValidationResult.Success(StepName);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
using System;
using Arcus.EventGrid.Proxy.Api.Validation.Steps.Interfaces;
using Microsoft.Extensions.Configuration;

namespace Arcus.EventGrid.Proxy.Api.Validation.Steps
{
public class EventGridTopicEndpointValidationStep : IValidationStep
{
private readonly IConfiguration _configuration;

public EventGridTopicEndpointValidationStep(IConfiguration configuration)
{
_configuration = configuration;
}

public string StepName { get; } = "Event Grid Topic Endpoint";

public ValidationResult Execute()
{
var rawTopicEndpoint = _configuration[EnvironmentVariables.Runtime.EventGrid.TopicEndpoint];

if (string.IsNullOrWhiteSpace(rawTopicEndpoint))
{
return ValidationResult.Failure(StepName, "No topic endpoint was specified");
}

try
{
var uri = new Uri(rawTopicEndpoint);

return ValidationResult.Success(StepName);
}
catch
{
return ValidationResult.Failure(StepName, "No valid topic endpoint was specified");
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
namespace Arcus.EventGrid.Proxy.Api.Validation.Steps.Interfaces
{
public interface IValidationStep
{
/// <summary>
/// Name of the validation step
/// </summary>
string StepName { get; }

/// <summary>
/// Executes the validation step
/// </summary>
ValidationResult Execute();
}
}
Loading

0 comments on commit f1c19a0

Please sign in to comment.