Merged from master to resolve conflicts

This commit is contained in:
Philip Dimitratos 2017-12-27 14:47:16 -08:00
Родитель a6a77146d4 cd1b51b4f5
Коммит 567f4343cd
32 изменённых файлов: 470 добавлений и 313 удалений

Просмотреть файл

@ -12,3 +12,54 @@ provided by the bot. You will only need to do this once across all repos using o
This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/).
For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or
contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments.
//Todo: figure out what else should be in this file.
#Cloning this repository
This repository has the https://github.com/Azure/Sia-Root git repository embedded as a submodule.
Once you've cloned this repository, use your git client to navigate to "domain" subdirectory, and run
1. git submodule init
2. git submodule update
This will clone the submodule repository. For more information on submodules, see [the Git Tools Documentation for Submodules](https://git-scm.com/book/en/v2/Git-Tools-Submodules)
#Setting Up Your Development Environment
This Gateway is intended to be extended by Sia microservices in other code repositories.
In order to test functionality of the gateway on your local machine:
1. For each 'Service' in appsettings.Development.json:
1. Clone the associated repository (Example: Incident is associated with the SiaManagement repository)
2. Change the default startup from the project name to IIS Express and start
3. You will need to have a separate visual studio instance open for each service the gateway will interact with.
(If you find a good way to do this without having visual studio open, please let the maintainers of this project know)
4. Validate that the localhost port the service is running on matches its configuration in appsettings.Development.json.
You can find IIS Express information by right clicking the IIS Express icon in your system tray.
2. Get appropriate Secrets for authentication
1. Right click on the Sia.Gateway project and select "Manage user secrets"
2. Open usersecrets.template.json to use as a template for your usersecrets json file
3. You will need these configuration values:
1. Your AAD instance (the default is correct for the vast majority of scenarios)
2. Your AAD tenant
3. Your Key Vault vault name
4. Your ApplicationInsights instrumentation key name
5. The secret that is used to authenticate to azure key vault for the gateway (ClientSecret)
* This secret can be found in the azure portal in the AAD App Registration for your Gateway instance
3. Start the gateway
#Ticketing System Connectors
Sia can be configured to to work with a separate ticketing system,
either by leveraging a proxy API or by direct access with a custom connector.
Data from connected ticketing systems can be used to generate events and/or present
additional information and context on the UI.
Sia will still function without a connected ticketing system, and will persist limited
ticket data in the Incident database (see Sia.Data.Incident\Models\Ticket.cs)
For additional information on use, configuration, and creation of ticketing system
connectors, see Sia.Connectors.Tickets\README.md

Просмотреть файл

@ -27,6 +27,14 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sia.Shared.Tests", "domain\
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sia.Data.Incident.Tests", "Sia.Data.Incident.Tests\Sia.Data.Incident.Tests.csproj", "{C3D38E8D-54BC-4D71-9B3C-FD6D641D9763}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "misc", "misc", "{9845797A-7F60-4400-B4C4-D748417E1F64}"
ProjectSection(SolutionItems) = preProject
.gitignore = .gitignore
.gitmodules = .gitmodules
LICENSE = LICENSE
README.md = README.md
EndProjectSection
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU

2
domain

@ -1 +1 @@
Subproject commit ac81ef875645a6f645a409eb736b437a156f052d
Subproject commit 881099688463da7f969024d58ff041cf8806db6a

Просмотреть файл

@ -0,0 +1,40 @@
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Runtime.Loader;
namespace Sia.Gateway.Initialization
{
public static partial class Initialization
{
public static void LoadConnectorFromAssembly(
this IServiceCollection services,
IHostingEnvironment env,
IConfigurationRoot config,
string ticketConnectorAssemblyPath,
string assemblyLoaderType = "Sia.Gateway.Initialization.LoadAssembly"
)
{
var connectorAssembly = AssemblyLoadContext
.Default
.LoadFromAssemblyPath(ticketConnectorAssemblyPath);
var connectorInitializerType = connectorAssembly
.GetType(assemblyLoaderType);
var connectorConfigureServices = connectorInitializerType
.GetMethod(
"AddConnector",
new Type[] {
typeof(IServiceCollection),
typeof(IConfigurationRoot),
typeof(IHostingEnvironment)
}
);
connectorConfigureServices.Invoke(
null,
new object[] { services, config, env }
);
}
}
}

Просмотреть файл

@ -1,9 +0,0 @@
using System.Threading.Tasks;
namespace Sia.Connectors.Tickets
{
public abstract class Client<TTicket>
{
public abstract Task<TTicket> GetAsync(string originId);
}
}

Просмотреть файл

@ -1,18 +1,45 @@
using System;
using Microsoft.Extensions.Logging;
using Sia.Domain;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Sia.Connectors.Tickets
{
public class Connector<TTicket>
public class Connector
{
public Client<TTicket> Client { get; }
public Converter<TTicket> Converter { get; }
public Connector(Client<TTicket> client, Converter<TTicket> converter)
public Connector(TicketingClient client, ILoggerFactory loggerFactory)
{
Client = client;
Converter = converter;
Logger = loggerFactory.CreateLogger<Connector>();
}
protected TicketingClient Client { get; }
protected ILogger Logger { get; }
public virtual async Task AppendDataAsync(Ticket persistedTicket)
{
try
{
persistedTicket.Data = await Client.GetAsync(persistedTicket.OriginId);
}
catch (Exception ex)
{
Logger.LogError(
ex,
"Exception during GetAsync for ticket with Id: {0}",
new object[] { persistedTicket.Id }
);
}
}
public virtual void AppendData(ICollection<Ticket> persistedTickets)
=> Task.WaitAll(
persistedTickets
.Select(tic => AppendDataAsync(tic))
.ToArray()
);
}
}

Просмотреть файл

@ -1,22 +0,0 @@
using AutoMapper;
using System.Collections.Generic;
namespace Sia.Connectors.Tickets
{
public abstract class Converter<TTicket>
{
public abstract ICollection<Data.Incidents.Models.Event> ExtractEvents(TTicket ticket);
public virtual Domain.Incident AssembleIncident(Data.Incidents.Models.Incident databaseRecord, TTicket ticket)
{
if(ticket != null)
{
foreach (var ev in ExtractEvents(ticket))
{
databaseRecord.Events.Add(ev);
}
}
return Mapper.Map<Domain.Incident>(databaseRecord);
}
}
}

Просмотреть файл

@ -0,0 +1,51 @@
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Sia.Shared.Validation;
namespace Sia.Gateway.Initialization
{
public static partial class Initialization
{
public static void AddTicketingConnector(
this IServiceCollection services,
IHostingEnvironment env,
IConfigurationRoot config)
{
if (TryGetConfigValue(
config,
"Connector:Ticket:Path",
out var ticketConnectorAssemblyPath))
{
services
.LoadConnectorFromAssembly(
env,
config,
ticketConnectorAssemblyPath
);
return;
}
if (TryGetConfigValue(
config,
"Connector:Ticket:ProxyEndpoint",
out var proxyEndpoint))
{
services.AddProxyConnector(config, proxyEndpoint);
return;
}
services.AddNoTicketingSystem();
}
private static bool TryGetConfigValue(
this IConfigurationRoot config,
string configName,
out string configValue)
{
ThrowIf.NullOrWhiteSpace(configName, nameof(configName));
configValue = config[configName];
return !string.IsNullOrEmpty(configValue);
}
}
}

Просмотреть файл

@ -1,6 +0,0 @@
namespace Sia.Connectors.Tickets.None
{
public class EmptyTicket
{
}
}

Просмотреть файл

@ -6,12 +6,10 @@ namespace Sia.Gateway.Initialization
{
public static partial class Initialization
{
public static IServiceCollection AddNoTicketingSystem(this IServiceCollection services)
{
return services
.AddSingleton<Converter<EmptyTicket>, NoConverter>()
.AddSingleton<Client<EmptyTicket>, NoClient>()
.AddSingleton<Connector<EmptyTicket>, NoConnector>();
}
public static IServiceCollection AddNoTicketingSystem(
this IServiceCollection services
) => services
.AddSingleton<NoClient>()
.AddSingleton<Connector, NoConnector>();
}
}

Просмотреть файл

@ -2,11 +2,9 @@
namespace Sia.Connectors.Tickets.None
{
public class NoClient : Client<EmptyTicket>
public class NoClient : TicketingClient
{
public override Task<EmptyTicket> GetAsync(string originId)
{
return Task.FromResult(new EmptyTicket());
}
public override Task<object> GetAsync(string originId)
=> Task.FromResult<object>(null);
}
}

Просмотреть файл

@ -1,9 +1,11 @@
namespace Sia.Connectors.Tickets.None
using Microsoft.Extensions.Logging;
namespace Sia.Connectors.Tickets.None
{
public class NoConnector : Connector<EmptyTicket>
public class NoConnector : Connector
{
public NoConnector(Client<EmptyTicket> client, Converter<EmptyTicket> converter)
: base(client, converter)
public NoConnector(NoClient client, ILoggerFactory loggerFactory)
: base(client, loggerFactory)
{
}
}

Просмотреть файл

@ -1,13 +0,0 @@
using Sia.Data.Incidents.Models;
using System.Collections.Generic;
namespace Sia.Connectors.Tickets.None
{
public class NoConverter : Converter<EmptyTicket>
{
public override ICollection<Event> ExtractEvents(EmptyTicket ticket)
{
return new List<Event>();
}
}
}

Просмотреть файл

@ -3,7 +3,7 @@
Sia can be configured to to work with a separate ticketing system,
either by leveraging a proxy API or by direct access using a custom connector.
Data from connected ticketing systems can be used to generate events and/or
Data from connected ticketing systems can be used to
present additional information and context on the UI.
Sia will still function without a connected ticketing system, and will

Просмотреть файл

@ -3,8 +3,8 @@
<PropertyGroup>
<TargetFramework>netcoreapp2.0</TargetFramework>
<PackageId>Microsoft.Sia.Connectors.Tickets</PackageId>
<Version>1.0.6-alpha</Version>
<Authors>pdimit, magpint, jache, chtownes</Authors>
<Version>1.1.8-alpha</Version>
<Authors>Microsoft</Authors>
<Company>Microsoft</Company>
<Product>SRE Incident Assistant</Product>
<Description>Ticketing system connector primitives for SIA</Description>
@ -15,6 +15,7 @@
<ItemGroup>
<PackageReference Include="AutoMapper" Version="6.1.1" />
<PackageReference Include="Microsoft.AspNetCore.Hosting" Version="2.0.1" />
</ItemGroup>
<ItemGroup>

Просмотреть файл

@ -1,5 +1,6 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Sia.Connectors.Tickets;
using Sia.Connectors.Tickets.TicketProxy;
using Sia.Shared.Authentication;
@ -8,23 +9,63 @@ namespace Sia.Gateway.Initialization
{
public static partial class Initialization
{
public static IServiceCollection AddProxyWithoutAuth(this IServiceCollection services, string endpoint)
=> services.AddProxy(new ProxyConnectionInfo(endpoint));
public static IServiceCollection AddProxyWithoutAuth(
this IServiceCollection services,
string endpoint
) => services.AddProxy(new ProxyConnectionInfo(endpoint));
public static IServiceCollection AddProxyWithCert(this IServiceCollection services, string endpoint, string certThumbprint)
=> services.AddProxy(new ProxyConnectionInfo(endpoint, certThumbprint));
public static IServiceCollection AddProxyWithCert(
this IServiceCollection services,
string endpoint,
string certThumbprint
) => services.AddProxy(new ProxyConnectionInfo(endpoint, certThumbprint));
public static IServiceCollection AddProxyWithCertFromKeyVault(this IServiceCollection services, string endpoint, KeyVaultConfiguration config, string certName)
=> services.AddProxy(new ProxyConnectionInfo(endpoint, config, certName));
public static IServiceCollection AddProxyWithCertFromKeyVault(
this IServiceCollection services,
string endpoint,
KeyVaultConfiguration config,
string certName
) => services.AddProxy(new ProxyConnectionInfo(endpoint, config, certName));
private static IServiceCollection AddProxy(this IServiceCollection services, ProxyConnectionInfo proxyConnection)
private static IServiceCollection AddProxy(
this IServiceCollection services,
ProxyConnectionInfo proxyConnection
) => services
.AddScoped(serv => proxyConnection)
.AddScoped<TicketingClient, ProxyClient>()
.AddScoped<Connector, ProxyConnector>();
public static void AddProxyConnector(
this IServiceCollection services,
IConfigurationRoot config,
string proxyEndpoint)
{
return services
.AddScoped<Converter<ProxyTicket>, ProxyConverter>()
.AddScoped<Client<ProxyTicket>>(serv => proxyConnection.GetClient())
.AddScoped<Connector<ProxyTicket>, ProxyConnector>();
var proxyAuthType = config["Connector:Ticket:ProxyAuthType"];
switch (proxyAuthType)
{
case "Certificate":
services.AddProxyWithCert(
proxyEndpoint,
config["Connector:Ticket:ProxyCertThumbprint"]
);
return;
case "VaultCertificate":
services.AddProxyWithCertFromKeyVault(
proxyEndpoint,
new KeyVaultConfiguration(
config["ClientId"],
config["ClientSecret"],
config["Connector:Ticket:VaultName"]
),
config["Connector:Ticket:CertName"]
);
return;
default:
services.AddProxyWithoutAuth(proxyEndpoint);
return;
}
}
}
}

Просмотреть файл

@ -1,26 +1,40 @@
using Newtonsoft.Json;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using Sia.Shared.Authentication;
using System;
using System.Net.Http;
using System.Threading.Tasks;
namespace Sia.Connectors.Tickets.TicketProxy
{
public class ProxyClient : Client<ProxyTicket>
public class ProxyClient : TicketingClient
{
private readonly string _endpoint;
private readonly HttpClient _client;
private readonly ILoggerFactory _loggerFactory;
public ProxyClient(HttpClient singletonClient, string endpoint)
private ProxyConnectionInfo _connectionInfo { get; }
private HttpClient _client { get; set; }
public ProxyClient(
ProxyConnectionInfo connectionInfo,
ILoggerFactory loggerFactory
)
{
_endpoint = endpoint;
_client = singletonClient;
_loggerFactory = loggerFactory;
_connectionInfo = connectionInfo;
}
public override async Task<ProxyTicket> GetAsync(string originId)
public override async Task<object> GetAsync(string originId)
{
string incidentUrl = $"{_endpoint}/{originId}";
if(_client is null)
{
_client = await _connectionInfo.GetClientAsync(_loggerFactory);
}
string incidentUrl = $"{_connectionInfo.Endpoint}/{originId}";
var response = await _client.GetAsync(incidentUrl);
var content = await response.Content.ReadAsStringAsync();
return JsonConvert.DeserializeObject<ProxyTicket>(content);
return JsonConvert.DeserializeObject<ProxyData>(content);
}
}
}

Просмотреть файл

@ -1,20 +1,26 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Sia.Shared.Authentication;
using Sia.Shared.Validation;
using System;
using System.Net.Http;
using System.Threading.Tasks;
namespace Sia.Connectors.Tickets.TicketProxy
{
public class ProxyConnectionInfo
{
private AzureSecretVault _keyVault;
/// <summary>
/// Instantiates ProxyConnectionInfo with no Authentication
/// </summary>
/// <param name="endpoint">Proxy Endpoint</param>
public ProxyConnectionInfo(string endpoint)
: this(endpoint, AuthenticationType.None)
public ProxyConnectionInfo(
string endpoint
) : this(
endpoint,
AuthenticationType.None)
{ }
/// <summary>
@ -22,8 +28,13 @@ namespace Sia.Connectors.Tickets.TicketProxy
/// </summary>
/// <param name="endpoint">Proxy Endpoint</param>
/// <param name="certThumbprint">Thumbprint for searching local certificate store</param>
public ProxyConnectionInfo(string endpoint, string certThumbprint)
: this(endpoint, AuthenticationType.Certificate)
public ProxyConnectionInfo(
string endpoint,
string certThumbprint
) : this(
endpoint,
AuthenticationType.Certificate
)
{
CertIdentifier = ThrowIf.NullOrWhiteSpace(certThumbprint, nameof(certThumbprint));
}
@ -34,39 +45,55 @@ namespace Sia.Connectors.Tickets.TicketProxy
/// <param name="endpoint">Proxy Endpoint</param>
/// <param name="config">Configuration root for initialization</param>
/// <param name="vaultName">Key vault name</param>
public ProxyConnectionInfo(string endpoint, KeyVaultConfiguration config, string vaultName)
: this(endpoint, AuthenticationType.CertificateFromKeyVault)
public ProxyConnectionInfo(
string endpoint,
KeyVaultConfiguration config,
string vaultName
) : this(
endpoint,
AuthenticationType.CertificateFromKeyVault
)
{
_keyVault = new AzureSecretVault(config);
Vault = new AzureSecretVault(config);
CertIdentifier = ThrowIf.NullOrWhiteSpace(vaultName, nameof(vaultName));
}
protected ProxyConnectionInfo(string endpoint, AuthenticationType authType)
protected ProxyConnectionInfo(
string endpoint,
AuthenticationType authType
)
{
Endpoint = ThrowIf.NullOrWhiteSpace(endpoint, nameof(endpoint));
AuthenticationType = authType;
}
public ProxyClient GetClient() => new ProxyClient(ClientFactory.GetClient(), Endpoint);
public AuthenticationType AuthenticationType { get; protected set; }
public string Endpoint { get; protected set; }
public string CertIdentifier { get; protected set; }
protected IHttpClientFactory ClientFactory
public async Task<HttpClient> GetClientAsync (ILoggerFactory loggerFactory)
{
get
if(_client is null)
{
switch (AuthenticationType)
{
case AuthenticationType.Certificate:
return new LocalCertificateRetriever(CertIdentifier);
case AuthenticationType.CertificateFromKeyVault:
return new KeyVaultCertificateRetriever(_keyVault, CertIdentifier);
case AuthenticationType.None:
return new UnauthenticatedClientFactory();
default:
throw new NotImplementedException($"Unrecognized authentication type {AuthenticationType.GetName(typeof(AuthenticationType), AuthenticationType)}");
}
_client = await ClientFactory(loggerFactory).GetClientAsync();
}
return _client;
}
private HttpClient _client;
private readonly AzureSecretVault Vault;
public AuthenticationType AuthenticationType { get; protected set; }
public readonly string Endpoint;
private readonly string CertIdentifier;
protected IHttpClientFactory ClientFactory(ILoggerFactory loggerFactory)
{
switch (AuthenticationType)
{
case AuthenticationType.Certificate:
return new LocalCertificateRetriever(CertIdentifier, loggerFactory);
case AuthenticationType.CertificateFromKeyVault:
return new KeyVaultCertificateRetriever(Vault, CertIdentifier, loggerFactory);
case AuthenticationType.None:
return new UnauthenticatedClientFactory();
default:
throw new NotImplementedException($"Unrecognized authentication type {Enum.GetName(typeof(AuthenticationType), AuthenticationType)}");
}
}
}

Просмотреть файл

@ -1,9 +1,11 @@
namespace Sia.Connectors.Tickets.TicketProxy
using Microsoft.Extensions.Logging;
namespace Sia.Connectors.Tickets.TicketProxy
{
public class ProxyConnector : Connector<ProxyTicket>
public class ProxyConnector : Connector
{
public ProxyConnector(Client<ProxyTicket> client, Converter<ProxyTicket> converter)
: base(client, converter)
public ProxyConnector(ProxyClient client, ILoggerFactory logger)
: base(client, logger)
{
}
}

Просмотреть файл

@ -1,13 +0,0 @@
using Sia.Data.Incidents.Models;
using System.Collections.Generic;
namespace Sia.Connectors.Tickets.TicketProxy
{
public class ProxyConverter : Converter<ProxyTicket>
{
public override ICollection<Event> ExtractEvents(ProxyTicket ticket)
{
return new List<Event>();
}
}
}

Просмотреть файл

@ -1,10 +1,7 @@
namespace Sia.Connectors.Tickets.TicketProxy
{
public class ProxyTicket
public class ProxyData
{
public string OriginId { get; set; }
public long IncidentSystemId { get; set; }
public string OriginUri { get; set; }
public string Title { get; set; }
public string Owner { get; set; }
public string Severity { get; set; }

Просмотреть файл

@ -0,0 +1,9 @@
using System.Threading.Tasks;
namespace Sia.Connectors.Tickets
{
public abstract class TicketingClient
{
public abstract Task<object> GetAsync(string originId);
}
}

Просмотреть файл

@ -28,7 +28,10 @@ namespace Sia.Gateway.Initialization
public static class ServicesStartup
{
public static void AddFirstPartyServices(this IServiceCollection services, IHostingEnvironment env, IConfigurationRoot config)
public static void AddFirstPartyServices(
this IServiceCollection services,
IHostingEnvironment env,
IConfigurationRoot config)
{
ConfigureAuth(services, config);
@ -50,24 +53,6 @@ namespace Sia.Gateway.Initialization
services.AddSingleton(httpClients);
}
private static void AddTicketingConnector(this IServiceCollection services, IHostingEnvironment env, IConfigurationRoot config)
{
if (TryGetConfigValue(config, "Connector:Ticket:Path", out var ticketConnectorAssemblyPath))
{
LoadConnectorFromAssembly(services, env, config, ticketConnectorAssemblyPath);
return;
}
if (TryGetConfigValue(config, "Connector:Ticket:ProxyEndpoint", out var proxyEndpoint))
{
AddProxyConnector(services, config, proxyEndpoint);
return;
}
services.AddIncidentClient(typeof(EmptyTicket));
services.AddNoTicketingSystem();
}
private static bool TryGetConfigValue(this IConfigurationRoot config, string configName, out string configValue)
{
ThrowIf.NullOrWhiteSpace(configName, nameof(configName));
@ -75,57 +60,12 @@ namespace Sia.Gateway.Initialization
return !string.IsNullOrEmpty(configValue);
}
private static void AddProxyConnector(IServiceCollection services, IConfigurationRoot config, string proxyEndpoint)
{
services.AddIncidentClient(typeof(ProxyTicket));
var proxyAuthType = config["Connector:Ticket:ProxyAuthType"];
switch(proxyAuthType)
{
case "Certificate":
services.AddProxyWithCert(proxyEndpoint, config["Connector:Ticket:ProxyCertThumbprint"]);
return;
case "VaultCertificate":
services.AddProxyWithCertFromKeyVault(
proxyEndpoint,
new KeyVaultConfiguration(
config["ClientId"],
config["ClientSecret"],
config["Connector:Ticket:VaultName"]
),
config["Connector:Ticket:CertName"]
);
return;
default:
services.AddProxyWithoutAuth(proxyEndpoint);
return;
}
}
private static void ConfigureAuth(IServiceCollection services, IConfigurationRoot config)
{
var incidentAuthConfig = new AzureActiveDirectoryAuthenticationInfo(config["Playbook:ClientId"], config["ClientId"], config["ClientSecret"], config["AzureAd:Tenant"]);
services.AddSingleton<AzureActiveDirectoryAuthenticationInfo>(i => incidentAuthConfig);
}
private static void LoadConnectorFromAssembly(IServiceCollection services, IHostingEnvironment env, IConfigurationRoot config, string ticketConnectorAssemblyPath)
{
var connectorAssembly = AssemblyLoadContext.Default.LoadFromAssemblyPath(ticketConnectorAssemblyPath);
var connectorInitializerType = connectorAssembly.GetType("Sia.Gateway.Initialization.Initialization");
var ticketType = (Type)connectorInitializerType.GetMethod("TicketType").Invoke(null, null);
services.AddIncidentClient(ticketType);
var connectorConfigureServices = connectorInitializerType.GetMethod("AddConnector", new Type[] { typeof(IServiceCollection), typeof(IConfigurationRoot), typeof(IHostingEnvironment) });
connectorConfigureServices.Invoke(null, new object[] { services, config, env });
}
private static void AddIncidentClient(this IServiceCollection services, Type ticketType)
{
var handlerType = typeof(GetIncidentHandler<>).MakeGenericType(new Type[] { ticketType });
services.AddScoped(typeof(IGetIncidentHandler), handlerType);
services.AddScoped<IAsyncRequestHandler<GetIncidentRequest, Incident>, GetIncidentHandlerWrapper>();
}
public static void AddThirdPartyServices(this IServiceCollection services, IConfigurationRoot config)
{
services.AddSingleton<IActionContextAccessor, ActionContextAccessor>();

Просмотреть файл

@ -1,40 +0,0 @@
//Todo: figure out what else should be in this file.
#Setting Up Your Development Environment
This Gateway is intended to be extended by Sia microservices in other code repositories.
In order to test functionality of the gateway on your local machine:
1. For each 'Service' in appsettings.Development.json:
1. Clone the associated repository (Example: Incident is associated with the SiaManagement repository)
2. Change the default startup from the project name to IIS Express and start
3. You will need to have a separate visual studio instance open for each service the gateway will interact with.
(If you find a good way to do this without having visual studio open, please let the maintainers of this project know)
4. Validate that the localhost port the service is running on matches its configuration in appsettings.Development.json.
You can find IIS Express information by right clicking the IIS Express icon in your system tray.
2. Get appropriate Secrets for authentication
1. Right click on the Sia.Gateway project and select "Manage user secrets"
2. Open usersecrets.template.json to use as a template for your usersecrets json file
3. You will need these configuration values:
1. Your AAD instance (the default is correct for the vast majority of scenarios)
2. Your AAD tenant
3. Your Key Vault vault name
4. Your ApplicationInsights instrumentation key name
5. The secret that is used to authenticate to azure key vault for the gateway (ClientSecret)
* This secret can be found in the azure portal in the AAD App Registration for your Gateway instance
3. Start the gateway
#Ticketing System Connectors
Sia can be configured to to work with a separate ticketing system,
either by leveraging a proxy API or by direct access with a custom connector.
Data from connected ticketing systems can be used to generate events and/or present
additional information and context on the UI.
Sia will still function without a connected ticketing system, and will persist limited
ticket data in the Incident database (see Sia.Data.Incident\Models\Ticket.cs)
For additional information on use, configuration, and creation of ticketing system
connectors, see Sia.Connectors.Tickets\README.md

Просмотреть файл

@ -1,4 +1,5 @@
using MediatR;
using AutoMapper;
using MediatR;
using Microsoft.EntityFrameworkCore;
using Sia.Connectors.Tickets;
using Sia.Data.Incidents;
@ -21,52 +22,26 @@ namespace Sia.Gateway.Requests
public long Id { get; }
}
public class GetIncidentHandler<TTicket> : IGetIncidentHandler
public class GetIncidentHandler
: IncidentConnectorHandler<GetIncidentRequest, Incident>
{
private readonly IncidentContext _context;
private readonly Connector<TTicket> _connector;
public GetIncidentHandler(IncidentContext context, Connector<TTicket> connector)
public GetIncidentHandler(IncidentContext context, Connector connector)
:base(context, connector){}
public override async Task<Incident> Handle(GetIncidentRequest getIncident)
{
_context = context;
_connector = connector;
}
public async Task<Incident> Handle(GetIncidentRequest getIncident)
{
var incidentRecord = await _context.Incidents
.WithEagerLoading()
.FirstOrDefaultAsync(cr => cr.Id == getIncident.Id);
var incidentRecord = await _context
.Incidents
.WithEagerLoading()
.SingleOrDefaultAsync(cr => cr.Id == getIncident.Id);
if (incidentRecord == null) throw new KeyNotFoundException();
var remoteId = incidentRecord
.Tickets
.FirstOrDefault(t => t.IsPrimary)
.OriginId;
var incident = Mapper.Map<Incident>(incidentRecord);
var ticket = await _connector.Client.GetAsync(remoteId);
AttachTickets(incident);
return _connector
.Converter
.AssembleIncident(incidentRecord, ticket);
return incident;
}
}
public interface IGetIncidentHandler
{
Task<Incident> Handle(GetIncidentRequest getIncident);
}
//Why does this exist?
//Purely because I haven't been able to get Mediatr to work with generics
public class GetIncidentHandlerWrapper : IAsyncRequestHandler<GetIncidentRequest, Incident>
{
private readonly IGetIncidentHandler _actualHandler;
public GetIncidentHandlerWrapper(IGetIncidentHandler actualHandler)
{
_actualHandler = actualHandler;
}
public Task<Incident> Handle(GetIncidentRequest message)
=> _actualHandler.Handle(message);
}
}

Просмотреть файл

@ -1,6 +1,7 @@
using AutoMapper.QueryableExtensions;
using MediatR;
using Microsoft.EntityFrameworkCore;
using Sia.Connectors.Tickets;
using Sia.Data.Incidents;
using Sia.Domain;
using Sia.Shared.Authentication;
@ -19,19 +20,19 @@ namespace Sia.Gateway.Requests
}
public class GetIncidentsHandler
: IncidentContextHandler<GetIncidentsRequest, IEnumerable<Incident>>
: IncidentConnectorHandler<GetIncidentsRequest, IEnumerable<Incident>>
{
public GetIncidentsHandler(IncidentContext context)
:base(context)
{
}
public GetIncidentsHandler(
IncidentContext context,
Connector connector
) : base(context, connector) {}
public override async Task<IEnumerable<Incident>> Handle(GetIncidentsRequest request)
{
var incidentRecords = await _context.Incidents
.WithEagerLoading()
.ProjectTo<Incident>()
.ToListAsync();
AttachTickets(incidentRecords);
return incidentRecords;
}
}

Просмотреть файл

@ -2,6 +2,7 @@
using AutoMapper.QueryableExtensions;
using MediatR;
using Microsoft.EntityFrameworkCore;
using Sia.Connectors.Tickets;
using Sia.Data.Incidents;
using Sia.Domain;
using Sia.Domain.ApiModels;
@ -13,10 +14,13 @@ using System.Threading.Tasks;
namespace Sia.Gateway.Requests
{
public class GetIncidentsByTicketCreateIfNeededRequest : AuthenticatedRequest<IEnumerable<Incident>>
public class GetIncidentsByTicketCreateIfNeededRequest
: AuthenticatedRequest<IEnumerable<Incident>>
{
public GetIncidentsByTicketCreateIfNeededRequest(string ticketId, AuthenticatedUserContext userContext)
: base(userContext)
public GetIncidentsByTicketCreateIfNeededRequest(
string ticketId,
AuthenticatedUserContext userContext
) : base(userContext)
{
TicketId = ticketId;
}
@ -25,23 +29,32 @@ namespace Sia.Gateway.Requests
}
public class GetIncidentsByTicketCreateIfNeededRequestHandler : IncidentContextHandler<GetIncidentsByTicketCreateIfNeededRequest, IEnumerable<Incident>>
public class GetIncidentsByTicketCreateIfNeededRequestHandler
: IncidentConnectorHandler<
GetIncidentsByTicketCreateIfNeededRequest,
IEnumerable<Incident>
>
{
public GetIncidentsByTicketCreateIfNeededRequestHandler(IncidentContext context)
:base(context)
{
}
public GetIncidentsByTicketCreateIfNeededRequestHandler(
IncidentContext context,
Connector connector
) :base(context, connector){}
public override async Task<IEnumerable<Incident>> Handle(GetIncidentsByTicketCreateIfNeededRequest message)
public override async Task<IEnumerable<Incident>> Handle(
GetIncidentsByTicketCreateIfNeededRequest message
)
{
var incidents = await _context.Incidents
.WithEagerLoading()
.Where(incident => incident.Tickets.Any(inc => inc.OriginId == message.TicketId))
.Where(incident => incident
.Tickets
.Any(inc => inc.OriginId == message.TicketId))
.ProjectTo<Incident>().ToListAsync();
if (incidents.Any())
{
AttachTickets(incidents);
return incidents;
}
@ -59,7 +72,10 @@ namespace Sia.Gateway.Requests
var result = _context.Incidents.Add(dataIncident);
await _context.SaveChangesAsync();
return new List<Incident> { Mapper.Map<Incident>(result.Entity) };
var incidentDto = Mapper.Map<Incident>(result.Entity);
AttachTickets(incidentDto);
return new List<Incident> { incidentDto };
}
}
}

Просмотреть файл

@ -0,0 +1,32 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Sia.Data.Incidents;
using Sia.Connectors.Tickets;
using MediatR;
using Sia.Domain;
namespace Sia.Gateway.Requests
{
public abstract class IncidentConnectorHandler<TRequest, TResult>
: IncidentContextHandler<TRequest, TResult>
where TRequest : IRequest<TResult>
{
protected readonly Connector _connector;
protected IncidentConnectorHandler(
IncidentContext context,
Connector connector
) : base(context)
{
_connector = connector;
}
protected void AttachTickets(Incident incident)
=> _connector.AppendData(incident.Tickets);
protected void AttachTickets(List<Incident> incidents)
=> incidents.ForEach(inc => AttachTickets(inc));
}
}

Просмотреть файл

@ -24,8 +24,12 @@ namespace Sia.Gateway.Tests.Requests
Id = expectedIncidentId,
Title = expectedIncidentTitle
};
var serviceUnderTest = new GetIncidentHandler<EmptyTicket>(await MockFactory.IncidentContext("Get"), new NoConnector(new NoClient(), new NoConverter()));
var request = new GetIncidentRequest(expectedIncidentId, new DummyAuthenticatedUserContext());
var serviceUnderTest = new GetIncidentHandler(
await MockFactory.IncidentContext("Get"),
new NoConnector(new NoClient(), new StubLoggerFactory()));
var request = new GetIncidentRequest(
expectedIncidentId,
new DummyAuthenticatedUserContext());
var result = await serviceUnderTest.Handle(request);

Просмотреть файл

@ -1,4 +1,5 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Sia.Connectors.Tickets.None;
using Sia.Domain;
using Sia.Gateway.Initialization;
using Sia.Gateway.Requests;
@ -21,7 +22,10 @@ namespace Sia.Gateway.Tests.Requests
[TestMethod]
public async Task Handle_WhenIncidentNotExist_ReturnNewIncident()
{
var serviceUnderTest = new GetIncidentsByTicketCreateIfNeededRequestHandler(await MockFactory.IncidentContext("Get"));
var serviceUnderTest = new GetIncidentsByTicketCreateIfNeededRequestHandler(
await MockFactory.IncidentContext("Get"),
new NoConnector(new NoClient(), new StubLoggerFactory())
);
var request = new GetIncidentsByTicketCreateIfNeededRequest("100", new DummyAuthenticatedUserContext());
@ -36,7 +40,10 @@ namespace Sia.Gateway.Tests.Requests
public async Task Handle_WhenIncidentExists_ReturnCorrectIncidents()
{
var serviceUnderTest = new GetIncidentsByTicketCreateIfNeededRequestHandler(await MockFactory.IncidentContext("Get"));
var serviceUnderTest = new GetIncidentsByTicketCreateIfNeededRequestHandler(
await MockFactory.IncidentContext("Get"),
new NoConnector(new NoClient(), new StubLoggerFactory())
);
var request = new GetIncidentsByTicketCreateIfNeededRequest("44444444", new DummyAuthenticatedUserContext());
var result = (await serviceUnderTest.Handle(request)).ToList();

Просмотреть файл

@ -1,4 +1,5 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Sia.Connectors.Tickets.None;
using Sia.Domain;
using Sia.Gateway.Initialization;
using Sia.Gateway.Requests;
@ -34,7 +35,10 @@ namespace Sia.Gateway.Tests.Requests
Title = expectedIncidentTitles[i]
};
}
var serviceUnderTest = new GetIncidentsHandler(await MockFactory.IncidentContext("Get"));
var serviceUnderTest = new GetIncidentsHandler(
await MockFactory.IncidentContext("Get"),
new NoConnector(new NoClient(), new StubLoggerFactory())
);
var request = new GetIncidentsRequest(new DummyAuthenticatedUserContext());

Просмотреть файл

@ -0,0 +1,15 @@
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Text;
namespace Sia.Gateway.Tests.TestDoubles
{
public class StubLoggerFactory
: ILoggerFactory
{
public void AddProvider(ILoggerProvider provider) => throw new NotImplementedException();
public ILogger CreateLogger(string categoryName) => null;
public void Dispose() => throw new NotImplementedException();
}
}