From b08786cae53bf60f43422717c1533711fcc5741f Mon Sep 17 00:00:00 2001 From: Tom Kerkhove Date: Wed, 8 Apr 2020 17:44:42 +0200 Subject: [PATCH] Provide boilerplate for API Signed-off-by: Tom Kerkhove --- README.md | 8 +- .../Controllers/HealthController.cs | 55 ++++++++ .../Dockerfile | 19 +++ .../Program.cs | 55 ++++++++ ...mitor.ResourceDiscovery.Agent.Open-Api.xml | 37 ++++++ .../Promitor.ResourceDiscovery.Agent.csproj | 24 ++++ .../Properties/launchSettings.json | 34 +++++ .../Startup.cs | 122 ++++++++++++++++++ src/Promitor.ResourceDiscovery.sln | 25 ++++ 9 files changed, 378 insertions(+), 1 deletion(-) create mode 100644 src/Promitor.ResourceDiscovery.Agent/Controllers/HealthController.cs create mode 100644 src/Promitor.ResourceDiscovery.Agent/Dockerfile create mode 100644 src/Promitor.ResourceDiscovery.Agent/Program.cs create mode 100644 src/Promitor.ResourceDiscovery.Agent/Promitor.ResourceDiscovery.Agent.Open-Api.xml create mode 100644 src/Promitor.ResourceDiscovery.Agent/Promitor.ResourceDiscovery.Agent.csproj create mode 100644 src/Promitor.ResourceDiscovery.Agent/Properties/launchSettings.json create mode 100644 src/Promitor.ResourceDiscovery.Agent/Startup.cs create mode 100644 src/Promitor.ResourceDiscovery.sln diff --git a/README.md b/README.md index e6df4f9..77522a3 100644 --- a/README.md +++ b/README.md @@ -1 +1,7 @@ -# resource-discovery-sandbox \ No newline at end of file +# Resource Discovery API Sandbox + +Sandbox to build a resource discovery API built on top of Azure Resource Graph. + +## License Information + +This is licensed under The MIT License (MIT). Which means that you can use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the web application. But you always need to state that Tom Kerkhove is the original author of this web application. \ No newline at end of file diff --git a/src/Promitor.ResourceDiscovery.Agent/Controllers/HealthController.cs b/src/Promitor.ResourceDiscovery.Agent/Controllers/HealthController.cs new file mode 100644 index 0000000..0bcad9a --- /dev/null +++ b/src/Promitor.ResourceDiscovery.Agent/Controllers/HealthController.cs @@ -0,0 +1,55 @@ +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Diagnostics.HealthChecks; +using GuardNet; +using Swashbuckle.AspNetCore.Filters; + +namespace Promitor.ResourceDiscovery.Agent.Controllers +{ + /// + /// API endpoint to check the health of the application. + /// + [ApiController] + [Route("api/v1/health")] + public class HealthController : ControllerBase + { + private readonly HealthCheckService _healthCheckService; + + /// + /// Initializes a new instance of the class. + /// + /// The service to provide the health of the API application. + public HealthController(HealthCheckService healthCheckService) + { + Guard.NotNull(healthCheckService, nameof(healthCheckService)); + + _healthCheckService = healthCheckService; + } + + /// + /// Get Health + /// + /// Provides an indication about the health of the API. + /// API is healthy + /// API is unhealthy or in degraded state + [HttpGet(Name = "Health_Get")] + [ProducesResponseType(typeof(HealthReport), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(HealthReport), StatusCodes.Status503ServiceUnavailable)] + [SwaggerResponseHeader(200, "RequestId", "string", "The header that has a request ID that uniquely identifies this operation call")] + [SwaggerResponseHeader(200, "X-Transaction-Id", "string", "The header that has the transaction ID is used to correlate multiple operation calls.")] + public async Task Get() + { + HealthReport healthReport = await _healthCheckService.CheckHealthAsync(); + + if (healthReport?.Status == HealthStatus.Healthy) + { + return Ok(healthReport); + } + else + { + return StatusCode(StatusCodes.Status503ServiceUnavailable, healthReport); + } + } + } +} diff --git a/src/Promitor.ResourceDiscovery.Agent/Dockerfile b/src/Promitor.ResourceDiscovery.Agent/Dockerfile new file mode 100644 index 0000000..baae155 --- /dev/null +++ b/src/Promitor.ResourceDiscovery.Agent/Dockerfile @@ -0,0 +1,19 @@ +FROM mcr.microsoft.com/dotnet/core/aspnet:3.1.3-alpine3.10 AS base +WORKDIR /app +EXPOSE 80 + +FROM mcr.microsoft.com/dotnet/core/sdk:3.1.201-alpine3.10 AS build +WORKDIR /src +COPY ["Promitor.ResourceDiscovery.Agent.csproj", ""] + +COPY . . +WORKDIR "/src/." +RUN dotnet build "Promitor.ResourceDiscovery.Agent.csproj" -c Release -o /app + +FROM build AS publish +RUN dotnet publish "Promitor.ResourceDiscovery.Agent.csproj" -c Release -o /app + +FROM base AS final +WORKDIR /app +COPY --from=publish /app . +ENTRYPOINT ["dotnet", "Promitor.ResourceDiscovery.Agent.dll"] diff --git a/src/Promitor.ResourceDiscovery.Agent/Program.cs b/src/Promitor.ResourceDiscovery.Agent/Program.cs new file mode 100644 index 0000000..7090414 --- /dev/null +++ b/src/Promitor.ResourceDiscovery.Agent/Program.cs @@ -0,0 +1,55 @@ +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace Promitor.ResourceDiscovery.Agent +{ + public class Program + { + public static int Main(string[] args) + { + CreateHostBuilder(args) + .Build() + .Run(); + + return 0; + } + + public static IHostBuilder CreateHostBuilder(string[] args) + { + IConfiguration configuration = CreateConfiguration(args); + IHostBuilder webHostBuilder = CreateHostBuilder(args, configuration); + + return webHostBuilder; + } + + private static IConfiguration CreateConfiguration(string[] args) + { + IConfigurationRoot configuration = + new ConfigurationBuilder() + .AddCommandLine(args) + .AddEnvironmentVariables() + .Build(); + + return configuration; + } + + private static IHostBuilder CreateHostBuilder(string[] args, IConfiguration configuration) + { + string httpEndpointUrl = "http://+:" + configuration["ARCUS_HTTP_PORT"]; + IHostBuilder webHostBuilder = + Host.CreateDefaultBuilder(args) + .ConfigureAppConfiguration(configBuilder => configBuilder.AddConfiguration(configuration)) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.ConfigureKestrel(kestrelServerOptions => kestrelServerOptions.AddServerHeader = false) + .UseUrls(httpEndpointUrl) + .ConfigureLogging(logging => logging.AddConsole()) + .UseStartup(); + }); + + return webHostBuilder; + } + } +} diff --git a/src/Promitor.ResourceDiscovery.Agent/Promitor.ResourceDiscovery.Agent.Open-Api.xml b/src/Promitor.ResourceDiscovery.Agent/Promitor.ResourceDiscovery.Agent.Open-Api.xml new file mode 100644 index 0000000..24983d1 --- /dev/null +++ b/src/Promitor.ResourceDiscovery.Agent/Promitor.ResourceDiscovery.Agent.Open-Api.xml @@ -0,0 +1,37 @@ + + + + Promitor.ResourceDiscovery.Agent + + + + + API endpoint to check the health of the application. + + + + + Initializes a new instance of the class. + + The service to provide the health of the API application. + + + + Get Health + + Provides an indication about the health of the API. + API is healthy + API is unhealthy or in degraded state + + + + Initializes a new instance of the class. + + + + + Gets the configuration of key/value application properties. + + + + diff --git a/src/Promitor.ResourceDiscovery.Agent/Promitor.ResourceDiscovery.Agent.csproj b/src/Promitor.ResourceDiscovery.Agent/Promitor.ResourceDiscovery.Agent.csproj new file mode 100644 index 0000000..a810235 --- /dev/null +++ b/src/Promitor.ResourceDiscovery.Agent/Promitor.ResourceDiscovery.Agent.csproj @@ -0,0 +1,24 @@ + + + + netcoreapp3.1 + true + Promitor.ResourceDiscovery.Agent.Open-Api.xml + Linux + + + + + + + + + + + + + + + + + diff --git a/src/Promitor.ResourceDiscovery.Agent/Properties/launchSettings.json b/src/Promitor.ResourceDiscovery.Agent/Properties/launchSettings.json new file mode 100644 index 0000000..0d6a250 --- /dev/null +++ b/src/Promitor.ResourceDiscovery.Agent/Properties/launchSettings.json @@ -0,0 +1,34 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:54553/", + "sslPort": 44315 + } + }, + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "Promitor.ResourceDiscovery.Agent": { + "commandName": "Project", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "https://localhost:5001;http://localhost:5000" + }, + "Docker": { + "commandName": "Docker", + "launchBrowser": true, + "launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}", + "publishAllPorts": true, + "useSSL": false + } + } +} \ No newline at end of file diff --git a/src/Promitor.ResourceDiscovery.Agent/Startup.cs b/src/Promitor.ResourceDiscovery.Agent/Startup.cs new file mode 100644 index 0000000..65c3f53 --- /dev/null +++ b/src/Promitor.ResourceDiscovery.Agent/Startup.cs @@ -0,0 +1,122 @@ +using System; +using System.IO; +using System.Linq; +using System.Text.Json.Serialization; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Formatters; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.OpenApi.Models; +using Swashbuckle.AspNetCore.Filters; +using Arcus.WebApi.Correlation; + +namespace Promitor.ResourceDiscovery.Agent +{ + public class Startup + { + private const string ApiName = "Promitor - Resource Discovery API"; + + /// + /// Initializes a new instance of the class. + /// + public Startup(IConfiguration configuration) + { + Configuration = configuration; + } + + /// + /// Gets the configuration of key/value application properties. + /// + public IConfiguration Configuration { get; } + + // This method gets called by the runtime. Use this method to add services to the container. + // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940 + public void ConfigureServices(IServiceCollection services) + { + services.AddRouting(options => + { + options.LowercaseUrls = true; + options.LowercaseQueryStrings = true; + }); + services.AddControllers(options => + { + options.ReturnHttpNotAcceptable = true; + options.RespectBrowserAcceptHeader = true; + + RestrictToJsonContentType(options); + AddEnumAsStringRepresentation(options); + + }); + + services.AddHealthChecks(); + services.AddCorrelation(); + +#if DEBUG + var openApiInformation = new OpenApiInfo + { + Title = ApiName, + Version = "v1" + }; + + services.AddSwaggerGen(swaggerGenerationOptions => + { + swaggerGenerationOptions.SwaggerDoc("v1", openApiInformation); + swaggerGenerationOptions.IncludeXmlComments(Path.Combine(AppContext.BaseDirectory, "Promitor.ResourceDiscovery.Agent.Open-Api.xml")); + + swaggerGenerationOptions.OperationFilter("X-Transaction-Id", "Transaction ID is used to correlate multiple operation calls. A new transaction ID will be generated if not specified.", false); + swaggerGenerationOptions.OperationFilter(); + }); +#endif + } + + private static void RestrictToJsonContentType(MvcOptions options) + { + var allButJsonInputFormatters = options.InputFormatters.Where(formatter => !(formatter is SystemTextJsonInputFormatter)); + foreach (IInputFormatter inputFormatter in allButJsonInputFormatters) + { + options.InputFormatters.Remove(inputFormatter); + } + + // Removing for text/plain, see https://docs.microsoft.com/en-us/aspnet/core/web-api/advanced/formatting?view=aspnetcore-3.0#special-case-formatters + options.OutputFormatters.RemoveType(); + } + + private static void AddEnumAsStringRepresentation(MvcOptions options) + { + var onlyJsonInputFormatters = options.InputFormatters.OfType(); + foreach (SystemTextJsonInputFormatter inputFormatter in onlyJsonInputFormatters) + { + inputFormatter.SerializerOptions.Converters.Add(new JsonStringEnumConverter()); + } + + var onlyJsonOutputFormatters = options.OutputFormatters.OfType(); + foreach (SystemTextJsonOutputFormatter outputFormatter in onlyJsonOutputFormatters) + { + outputFormatter.SerializerOptions.Converters.Add(new JsonStringEnumConverter()); + } + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + app.UseMiddleware(); + app.UseCorrelation(); + app.UseRouting(); + + app.UseSwagger(swaggerOptions => + { + swaggerOptions.RouteTemplate = "api/{documentName}/docs.json"; + }); + app.UseSwaggerUI(swaggerUiOptions => + { + swaggerUiOptions.SwaggerEndpoint("/api/v1/docs.json", ApiName); + swaggerUiOptions.RoutePrefix = "api/docs"; + swaggerUiOptions.DocumentTitle = ApiName; + }); + app.UseEndpoints(endpoints => endpoints.MapControllers()); + + } + } +} diff --git a/src/Promitor.ResourceDiscovery.sln b/src/Promitor.ResourceDiscovery.sln new file mode 100644 index 0000000..9163e65 --- /dev/null +++ b/src/Promitor.ResourceDiscovery.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.29609.76 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Promitor.ResourceDiscovery.Agent", "Promitor.ResourceDiscovery.Agent\Promitor.ResourceDiscovery.Agent.csproj", "{DE2E7C21-E261-4A78-8C63-7EEBD2CB3354}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {DE2E7C21-E261-4A78-8C63-7EEBD2CB3354}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DE2E7C21-E261-4A78-8C63-7EEBD2CB3354}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DE2E7C21-E261-4A78-8C63-7EEBD2CB3354}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DE2E7C21-E261-4A78-8C63-7EEBD2CB3354}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {E9FC4DEA-2E5E-40A2-9399-B6AC11ECEC12} + EndGlobalSection +EndGlobal