Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 7 additions & 11 deletions .azuredevops/pipelines/build-v2.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,20 +30,22 @@ steps:
displayName: Build mock-register-unit-tests image
inputs:
command: build
Dockerfile: ./Source/Dockerfile.unit-tests
Dockerfile: ./Source/Dockerfile
buildContext: ./Source
repository: mock-register-unit-tests
tags: latest
arguments: --target unit-tests

# Build mock-register-integration-tests
- task: Docker@2
displayName: Build mock-register-integration-tests image
inputs:
command: build
Dockerfile: ./Source/Dockerfile.integration-tests
Dockerfile: ./Source/Dockerfile
buildContext: ./Source
repository: mock-register-integration-tests
tags: latest
arguments: --target integration-tests

# List docker images
- task: Docker@2
Expand Down Expand Up @@ -75,7 +77,7 @@ steps:
# Run integration tests
#****************************************************************************************************************
- script: |
docker compose --file $(Build.SourcesDirectory)/Source/docker-compose.IntegrationTests.yml up --abort-on-container-exit --exit-code-from mock-register-integration-tests
docker compose --file $(Build.SourcesDirectory)/Source/docker-compose.IntegrationTests.yml up --abort-on-container-exit --exit-code-from mock-register-integration-tests
displayName: 'Integration Tests - Up'
condition: always()

Expand Down Expand Up @@ -208,17 +210,11 @@ steps:
performMultiLevelLookup: true

- task: CmdLine@2
displayName: 'Install dotnet-ef'
displayName: 'Restore dotnet tooling'
condition: always()
inputs:
script: 'dotnet tool install --version 8.0.3 --global dotnet-ef'
script: 'dotnet tool restore'

- task: CmdLine@2
displayName: 'Check dotnet-ef version'
condition: always()
inputs:
script: 'dotnet-ef'

- script: |
cd Source/CDR.Register.Repository
dotnet ef migrations bundle --context RegisterDatabaseContext --verbose --self-contained
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
trigger:
- develop
- main


variables:
- group: PT-Pipeline-Common

pool:
vmImage: windows-2019
vmImage: $(Pipeline_Host_Image)

steps:

Expand Down
12 changes: 12 additions & 0 deletions .config/dotnet-tools.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"version": 1,
"isRoot": true,
"tools": {
"dotnet-ef": {
"version": "8.0.20",
"commands": [
"dotnet-ef"
]
}
}
}
4 changes: 2 additions & 2 deletions .github/workflows/dotnet.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,12 +50,12 @@ jobs:
# Build mock-register-unit-tests image
- name: Build the mock-register-unit-tests image
run: |
docker build ./mock-register/Source --file ./mock-register/Source/Dockerfile.unit-tests --tag mock-register-unit-tests:latest
docker build ./mock-register/Source --file ./mock-register/Source/Dockerfile --target unit-tests --tag mock-register-unit-tests:latest

# Build mock-register-integration-tests image
- name: Build the mock-register-integration-tests image
run: |
docker build ./mock-register/Source --file ./mock-register/Source/Dockerfile.integration-tests --tag mock-register-integration-tests:latest
docker build ./mock-register/Source --file ./mock-register/Source/Dockerfile --target integration-tests --tag mock-register-integration-tests:latest

# List docker images
- name: List Docker images
Expand Down
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,21 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]
## [2.2.3] - 2025-12-03
### Added
- Added health check endpoints for APIs
### Changed
- Converted to [Central Package Management](https://learn.microsoft.com/en-us/nuget/consume-packages/central-package-management)
- Use [Multi-stage builds](https://docs.docker.com/build/building/multi-stage/) for tests
- Updated NuGet packages to address vulnerabilities

## [2.2.2] - 2025-10-15
### Added
- Enabled OpenTelemetry as a logging destination

# Fixed
- Fixed broken unit tests
- Fixed startup issues when Request/Response logging was disabled

## [2.2.1] - 2025-06-19
### Changed
Expand Down
27 changes: 27 additions & 0 deletions Help/container/HELP.md
Original file line number Diff line number Diff line change
Expand Up @@ -186,5 +186,32 @@ If the host names are changed, then the data stored in the Mock Register should

This can be achieved by using the Admin API, as outlined in the solution README.

## Connecting to the database
> The solutions above utilise MS SQL database for storage. In the examples below we use [MS SQL Server Management Studio (SMSS)](https://learn.microsoft.com/en-us/ssms/), but the approach should be similar for other tooling.

You will need the following authentication details:
| | |
| -- | -- |
| Server type | Database Engine |
| Server name | localhost |
| Authentication | SQL Server Authentication |
| Login | `sa` |
| Password | `Pa{}w0rd2019` |

Should you opt to use another tool, then the following would be useful

| | |
| -- | -- |
| Connection String | `Server=localhost;Database=cdr-register;User Id='SA';Password='Pa{}w0rd2019';MultipleActiveResultSets=True;TrustServerCertificate=True;Encrypt=False` |

## Logging
Once you have connected to the `cdr-register` database above you can view the various database tables that contain logs or view the console output using the following command.

```shell
docker logs mock-register
```

Optionally, logging to OpenTelemetry compatible destinations is also supported by modifying the `docker run` commands to supply additional environment variables. Additional guidance can be found in the [readme](../../README.md#logging) file.

## Host on your own infrastructure
The mock solutions can also be hosted on your own infrastructure, such as virtual machines or kubernetes clusters, in your private data centre or in the public cloud.
3 changes: 3 additions & 0 deletions Help/debugging/HELP.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ The following steps outline describe how to launch the Mock Register solution us

[<img src="./images/MS-Visual-Studio-Select-multiple-projects.png" width='625' alt="Projects selected to be started"/>](./images/MS-Visual-Studio-Select-multiple-projects.png)

> Depending on the version of Visual Studio you have you may also see a pre-existing `Mock Register with Gateways` profile that can be used
!["Mock Register with Gateways" profile](./images/MS-Visual-Studio-Start-Profile.png)

2. Click "Start" to start the Mock Register solution.

[<img src="./images/MS-Visual-Studio-Start.png" width='625' alt="Start the projects"/>](./images/MS-Visual-Studio-Start.png)
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,24 @@ The following technologies have been used to build the Mock Register:
- The TLS and mTLS Gateways have been implemented using `Ocelot`.
- The Repository utilises a `SQL` instance.

### Logging
By default the application logs to console as well as into tables within the application database.

However, OpenTelemetry can be configured by setting the [environment variables](https://opentelemetry.io/docs/specs/otel/protocol/exporter/#configuration-options) appropriately.

> The example below uses [Seq](https://datalust.co/seq) for simplicity we do not endorse any particular product. Choose an [OpenTelemetry vendor](https://opentelemetry.io/ecosystem/vendors/) is suitable for your needs.

For example, you may set up a local OTLP ingestion endpoint
`docker run -e ACCEPT_EULA=Y --rm -p 4318:80 5341:5341 datalust/seq`
and then set the following

| Environment variable | Value |
| --- | --- |
| `OTEL_EXPORTER_OTLP_ENDPOINT` | `http://localhost:5341/ingest/otlp` |
| `OTEL_EXPORTER_OTLP_PROTOCOL` | `http/protobuf` |

After which you should be able to [view telemetry](http://localhost:4318/).

# Testing

A [Polyglot notebook](https://code.visualstudio.com/docs/languages/polyglot) has been created for the Mock Register's APIs as a tool for demonstrating how these APIs are used.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>$(TargetFrameworkVersion)</TargetFramework>
<Version>$(Version)</Version>
<FileVersion>$(Version)</FileVersion>
<AssemblyVersion>$(Version)</AssemblyVersion>
<TargetFramework>$(TargetFrameworkVersion)</TargetFramework>
<Version>$(Version)</Version>
<FileVersion>$(Version)</FileVersion>
<AssemblyVersion>$(Version)</AssemblyVersion>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
<ItemGroup>
Expand All @@ -20,23 +20,25 @@
</Content>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Data.SqlClient" Version="5.2.2" />
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="8.0.1" />
<PackageReference Include="Ocelot" Version="23.3.6" />
<PackageReference Include="Serilog.AspNetCore" Version="8.0.3" />
<PackageReference Include="Serilog.Enrichers.Process" Version="3.0.0" />
<PackageReference Include="Serilog.Enrichers.Thread" Version="4.0.0" />
<PackageReference Include="Serilog.Settings.Configuration" Version="8.0.4" />
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
<PackageReference Include="Serilog.Sinks.File" Version="6.0.0" />
<PackageReference Include="Serilog.Sinks.MSSqlServer" Version="7.0.0" />
<PackageReference Include="SonarAnalyzer.CSharp" Version="10.5.0.109200">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="StyleCop.Analyzers.Unstable" Version="1.2.0.556">
<PackageReference Include="Ocelot" />
<PackageReference Include="Serilog.AspNetCore" />
<PackageReference Include="Serilog.Enrichers.Process" />
<PackageReference Include="Serilog.Enrichers.Thread" />
<PackageReference Include="Serilog.Settings.Configuration" />
<PackageReference Include="Serilog.Sinks.Console">
<TreatAsUsed>true</TreatAsUsed>
</PackageReference>
<PackageReference Include="Serilog.Sinks.File">
<TreatAsUsed>true</TreatAsUsed>
</PackageReference>
<PackageReference Include="Serilog.Sinks.MSSqlServer" />
<PackageReference Include="SonarAnalyzer.CSharp">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="StyleCop.Analyzers.Unstable">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
</Project>
83 changes: 83 additions & 0 deletions Source/CDR.Register.API.Gateway.TLS/DownstreamHttpHealthCheck.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Diagnostics.HealthChecks;

namespace CDR.Register.API.Gateway.TLS;

/// <summary>
/// Health checks to ensure all configured mock Authorisation servers are available.
/// </summary>
public class DownstreamHttpHealthCheck : IHealthCheck
{
private readonly IHttpClientFactory _httpClientFactory;
#pragma warning disable S1075 // URIs should not be hardcoded
private readonly Dictionary<string, Uri> _endpoints = new()
{
{ "InfoSec", new Uri("https://localhost:7002") },
{ "Discovery", new Uri("https://localhost:7003") },
{ "Status", new Uri("https://localhost:7004") },
{ "SSA", new Uri("https://localhost:7005") },
{ "Admin", new Uri("https://localhost:7006") },
};
#pragma warning restore S1075 // URIs should not be hardcoded

/// <summary>
/// Initializes a new instance of the <see cref="DownstreamHttpHealthCheck"/> class.
/// </summary>
/// <param name="httpClientFactory">The http client factory.</param>
public DownstreamHttpHealthCheck(IHttpClientFactory httpClientFactory)
{
this._httpClientFactory = httpClientFactory;
}

/// <inheritdoc />
public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
{
using var client = this._httpClientFactory.CreateClient(nameof(DownstreamHttpHealthCheck));
client.Timeout = context.Registration.Timeout;

var checks = this._endpoints.Select(x => CheckHealth(client, x.Key, x.Value));

var results = await Task.WhenAll(checks);

if (results.All(x => x.Exception == null && x.Response.IsSuccessStatusCode))
{
return new HealthCheckResult(HealthStatus.Healthy);
}

var data = results.Where(x => x.Exception != null || !x.Response.IsSuccessStatusCode).ToDictionary(x => x.Name, x => (object)new { x.Endpoint, x.Response?.StatusCode, x.Response?.ReasonPhrase, x.Exception?.Message });

var agg = new AggregateException("One or more health checks failed.", results.Where(x => x.Exception is not null).Select(x => x.Exception));

return new HealthCheckResult(HealthStatus.Degraded, "Not all APIs are available", agg, new ReadOnlyDictionary<string, object>(data));
}

/// <summary>
/// Checks that the health endpoint for the supplied <paramref name="apiBaseUri"/> is available.
/// </summary>
/// <param name="client">The client.</param>
/// <param name="name">The name of the API.</param>
/// <param name="apiBaseUri">The URI of the downstream API.</param>
/// <returns>The result of the check.</returns>
private static async Task<(string Name, Uri Endpoint, HttpResponseMessage Response, Exception Exception)> CheckHealth(HttpClient client, string name, Uri apiBaseUri)
{
HttpResponseMessage result = null;
var url = new Uri(apiBaseUri, "health");

try
{
result = await client.GetAsync(url);
}
catch (HttpRequestException ex)
{
return (name, url, result, ex);
}

return (name, url, result, null);
}
}
32 changes: 31 additions & 1 deletion Source/CDR.Register.API.Gateway.TLS/Startup.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
using Microsoft.AspNetCore.Builder;
using System.Linq;
using System.Net;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Microsoft.Extensions.Hosting;
using Newtonsoft.Json;
using Ocelot.DependencyInjection;
using Ocelot.Middleware;
using Serilog;
Expand All @@ -22,12 +26,38 @@ public Startup(IConfiguration configuration)
// This method gets called by the runtime. Use this method to add services to the container.
public static void ConfigureServices(IServiceCollection services)
{
services.AddHttpClient();
services.AddHealthChecks().AddCheck<DownstreamHttpHealthCheck>("Register APIs");
services.AddOcelot();
}

// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseHealthChecks("/health", new Microsoft.AspNetCore.Diagnostics.HealthChecks.HealthCheckOptions
{
AllowCachingResponses = false,
ResponseWriter = (HttpContext context, HealthReport result) =>
{
context.Response.ContentType = "application/json";

var item = new
{
Status = result.Status.ToString(),
Results = result.Entries.ToDictionary(e => e.Key, e => new { Status = e.Value.Status.ToString(), e.Value.Description, e.Value.Data }),
};

var json = JsonConvert.SerializeObject(item, Formatting.Indented, new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore });

if (result.Status == HealthStatus.Degraded)
{
context.Response.StatusCode = (int)HttpStatusCode.FailedDependency;
}

return context.Response.WriteAsync(json);
},
});

if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
Expand Down
Loading
Loading