Skip to content

Commit 145be78

Browse files
feat: DTOSS-8378 Add NEMS MESH retrieval function (#936)
* Added new NemsMeshRetrieval function - work in progress * Moved new nems function into NEMSIntegrationService folder * Added unit tests for new NemsMeshRetrieval function * Moved new NEMS Subscription function to more suitably named folder * Added TF for NemsMeshRetrieval function - using the same setup as RetrieveMeshFile * Corrected mistake with comments in tfvars files * Corrected formatting of tfvars files * Undone last commit attempting to fix tfvars files * removed comments from tfvars files * Created unique app_service_plan_key for NemsMeshRetrieval * Removed superfluous .vscode files from new function * Added new nems mesh retrieval function to docker compose - updated config away from caasfolder name to ensure separation * Update development.tfvars - updated NemsMeshRetrieval to max capacity of 1 * Update README.md - Changed to be more specific to NEMS * returned NemsSubscribe tfvars to how they were before my merge mistake * Removed trailing space from README.md * Corrected indentation in tfvars
1 parent 2e2411b commit 145be78

File tree

16 files changed

+1065
-0
lines changed

16 files changed

+1065
-0
lines changed

application/CohortManager/compose.core.yaml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,25 @@ services:
2222
- MeshKeyPassphrase=${MESHKEYPASSPHRASE}
2323
- ASPNETCORE_URLS=http://*:7059
2424

25+
nems-mesh-retrieval:
26+
container_name: nems-mesh-retrieval
27+
image: cohort-manager-nems-mesh-retrieval
28+
networks: [cohman-network]
29+
build:
30+
context: ./src/Functions/
31+
dockerfile: NemsSubscriptionService/NemsMeshRetrieval/Dockerfile
32+
profiles: [non-essential]
33+
environment:
34+
- AzureWebJobsStorage=${AZURITE_CONNECTION_STRING}
35+
- nemsmeshfolder_STORAGE=${AZURITE_CONNECTION_STRING}
36+
- MeshApiBaseUrl=https://localhost:8700/messageexchange
37+
- BSSMailBox=X26ABC1
38+
- MeshPassword=${MESHPASSWORD}
39+
- MeshSharedKey=${MESHSHAREDKEY}
40+
- MeshKeyName=meshpfx.pfx
41+
- MeshKeyPassphrase=${MESHKEYPASSPHRASE}
42+
- ASPNETCORE_URLS=http://*:7058
43+
2544
receive-caas-file:
2645
container_name: receive-caas-file
2746
image: cohort-manager-receive-caas-file
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
namespace NHS.CohortManager.CaasIntegrationService;
2+
3+
using System.Security.Cryptography.X509Certificates;
4+
5+
public static class CertificateHelper
6+
{
7+
public static X509Certificate2Collection GetCertificatesFromString(string certificatesString)
8+
{
9+
X509Certificate2Collection certs = [];
10+
11+
X509Certificate2[] pemCerts = certificatesString
12+
.Split("-----END CERTIFICATE-----", StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
13+
.Select(pem => pem + "\n-----END CERTIFICATE-----")
14+
.Select(pem =>
15+
{
16+
var base64 = pem
17+
.Replace("-----BEGIN CERTIFICATE-----", "")
18+
.Replace("-----END CERTIFICATE-----", "")
19+
.Replace("\n", "")
20+
.Replace("\r", "")
21+
.Trim();
22+
23+
return new X509Certificate2(Convert.FromBase64String(base64));
24+
})
25+
.ToArray();
26+
27+
certs.AddRange(pemCerts);
28+
29+
return certs;
30+
}
31+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS base
2+
3+
COPY ./Shared /Shared
4+
WORKDIR /Shared
5+
6+
RUN mkdir -p /home/site/wwwroot && \
7+
dotnet publish ./Common/Common.csproj --output /home/site/wwwroot && \
8+
dotnet publish ./Model/Model.csproj --output /home/site/wwwroot && \
9+
dotnet publish ./Data/Data.csproj --output /home/site/wwwroot && \
10+
dotnet publish ./Utilities/Utilities.csproj --output /home/site/wwwroot && \
11+
dotnet publish ./DataServices.Client/DataServices.Client.csproj --output /home/site/wwwroot && \
12+
dotnet publish ./DataServices.Core/DataServices.Core.csproj --output /home/site/wwwroot && \
13+
dotnet publish ./DataServices.Database/DataServices.Database.csproj --output /home/site/wwwroot
14+
15+
FROM base AS function
16+
17+
COPY ./CaasIntegration/RetrieveMeshFile /src/dotnet-function-app
18+
WORKDIR /src/dotnet-function-app
19+
20+
RUN dotnet publish *.csproj --output /home/site/wwwroot
21+
22+
# To enable ssh & remote debugging on app service change the base image to the one below
23+
# FROM mcr.microsoft.com/azure-functions/dotnet-isolated:4-dotnet-isolated8.0-appservice
24+
FROM mcr.microsoft.com/azure-functions/dotnet-isolated:4-dotnet-isolated8.0
25+
ENV AzureWebJobsScriptRoot=/home/site/wwwroot \
26+
AzureFunctionsJobHost__Logging__Console__IsEnabled=true
27+
28+
COPY --from=function ["/home/site/wwwroot", "/home/site/wwwroot"]
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
namespace NHS.Screening.RetrieveMeshFile;
2+
3+
using Microsoft.Azure.Functions.Worker;
4+
using Microsoft.Azure.Functions.Worker.Http;
5+
using Microsoft.Extensions.Diagnostics.HealthChecks;
6+
using System.Net;
7+
using System.Threading.Tasks;
8+
using HealthChecks.Extensions;
9+
10+
public class HealthCheckFunction
11+
{
12+
private readonly HealthCheckService _healthCheckService;
13+
14+
public HealthCheckFunction(HealthCheckService healthCheckService)
15+
{
16+
_healthCheckService = healthCheckService;
17+
}
18+
19+
[Function("health")]
20+
public async Task<HttpResponseData> Run([HttpTrigger(AuthorizationLevel.Anonymous, "get")] HttpRequestData req)
21+
{
22+
return await HealthCheckServiceExtensions.CreateHealthCheckResponseAsync(req, _healthCheckService);
23+
}
24+
}
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
namespace NHS.Screening.NemsMeshRetrieval;
2+
3+
using System;
4+
using System.Diagnostics.CodeAnalysis;
5+
using System.Globalization;
6+
using System.Text;
7+
using System.Text.Json;
8+
using System.Threading.Tasks;
9+
using Common;
10+
using Microsoft.Azure.Functions.Worker;
11+
using Microsoft.Extensions.Logging;
12+
using Microsoft.Extensions.Options;
13+
using Model;
14+
using NHS.MESH.Client.Models;
15+
16+
17+
public class NemsMeshRetrieval
18+
{
19+
private readonly ILogger<NemsMeshRetrieval> _logger;
20+
21+
private readonly IMeshToBlobTransferHandler _meshToBlobTransferHandler;
22+
private readonly string _mailboxId;
23+
private readonly string _blobConnectionString;
24+
private readonly IBlobStorageHelper _blobStorageHelper;
25+
private readonly NemsMeshRetrievalConfig _config;
26+
private const string NextHandShakeTimeConfigKey = "NextHandShakeTime";
27+
private const string ConfigFileName = "MeshState.json";
28+
29+
public NemsMeshRetrieval(ILogger<NemsMeshRetrieval> logger, IMeshToBlobTransferHandler meshToBlobTransferHandler, IBlobStorageHelper blobStorageHelper, IOptions<NemsMeshRetrievalConfig> options)
30+
{
31+
_logger = logger;
32+
_meshToBlobTransferHandler = meshToBlobTransferHandler;
33+
_blobStorageHelper = blobStorageHelper;
34+
_mailboxId = options.Value.BSSMailBox;
35+
_config = options.Value;
36+
_blobConnectionString = _config.nemsmeshfolder_STORAGE;
37+
}
38+
/// <summary>
39+
/// This function polls the MESH Mailbox every 5 minutes, if there is a file posted to the mailbox.
40+
/// If there is a file in there will move the file to the Cohort Manager Blob Storage where it will be picked up by the ReceiveCaasFile Function.
41+
/// </summary>
42+
[Function("RetrieveMeshFile")]
43+
public async Task RunAsync([TimerTrigger("0 */5 * * * *")] TimerInfo myTimer)
44+
{
45+
_logger.LogInformation("C# Timer trigger function executed at: ,{datetime}", DateTime.Now);
46+
47+
static bool messageFilter(MessageMetaData i) => true; // No current filter defined there might be business rules here
48+
49+
static string fileNameFunction(MessageMetaData i) => string.Concat(i.MessageId, "_-_", i.WorkflowID, ".parquet");
50+
51+
try
52+
{
53+
var shouldExecuteHandShake = await ShouldExecuteHandShake();
54+
var result = await _meshToBlobTransferHandler.MoveFilesFromMeshToBlob(messageFilter, fileNameFunction, _mailboxId, _blobConnectionString, "inbound", shouldExecuteHandShake);
55+
56+
if (!result)
57+
{
58+
_logger.LogError("An error was encountered while moving files from Mesh to Blob");
59+
}
60+
}
61+
catch (Exception ex)
62+
{
63+
_logger.LogError(ex, "An error encountered while moving files from Mesh to Blob");
64+
}
65+
66+
if (myTimer.ScheduleStatus is not null)
67+
{
68+
_logger.LogInformation("Next timer schedule at: {scheduleStatus}", myTimer.ScheduleStatus.Next);
69+
}
70+
}
71+
72+
private async Task<bool> ShouldExecuteHandShake()
73+
{
74+
75+
Dictionary<string, string> configValues;
76+
TimeSpan handShakeInterval = new TimeSpan(0, 23, 54, 0);
77+
var meshState = await _blobStorageHelper.GetFileFromBlobStorage(_blobConnectionString, "config", ConfigFileName);
78+
if (meshState == null)
79+
{
80+
81+
_logger.LogInformation("MeshState File did not exist, Creating new MeshState File in blob Storage");
82+
configValues = new Dictionary<string, string>
83+
{
84+
{ NextHandShakeTimeConfigKey, DateTime.UtcNow.Add(handShakeInterval).ToString() }
85+
};
86+
await SetConfigState(configValues);
87+
88+
return true;
89+
90+
}
91+
using (StreamReader reader = new StreamReader(meshState.Data))
92+
{
93+
meshState.Data.Seek(0, SeekOrigin.Begin);
94+
string jsonData = await reader.ReadToEndAsync();
95+
configValues = JsonSerializer.Deserialize<Dictionary<string, string>>(jsonData);
96+
}
97+
98+
string nextHandShakeDateString;
99+
//config value doenst exist
100+
if (!configValues.TryGetValue(NextHandShakeTimeConfigKey, out nextHandShakeDateString))
101+
{
102+
_logger.LogInformation("NextHandShakeTime config item does not exist, creating new config item");
103+
configValues.Add(NextHandShakeTimeConfigKey, DateTime.UtcNow.Add(handShakeInterval).ToString());
104+
await SetConfigState(configValues);
105+
return true;
106+
107+
108+
}
109+
DateTime nextHandShakeDateTime;
110+
//date cannot be parsed
111+
if (!DateTime.TryParse(nextHandShakeDateString, CultureInfo.InvariantCulture, out nextHandShakeDateTime))
112+
{
113+
_logger.LogInformation("Unable to Parse NextHandShakeTime, Updating config value");
114+
configValues[NextHandShakeTimeConfigKey] = DateTime.UtcNow.Add(handShakeInterval).ToString();
115+
SetConfigState(configValues);
116+
return true;
117+
}
118+
119+
if (DateTime.Compare(nextHandShakeDateTime, DateTime.UtcNow) <= 0)
120+
{
121+
_logger.LogInformation("Next HandShakeTime was in the past, will execute handshake");
122+
var NextHandShakeTimeConfig = DateTime.UtcNow.Add(handShakeInterval).ToString();
123+
124+
configValues[NextHandShakeTimeConfigKey] = NextHandShakeTimeConfig;
125+
_logger.LogInformation("Next Handshake scheduled for {NextHandShakeTimeConfig}", NextHandShakeTimeConfig);
126+
127+
return true;
128+
129+
}
130+
_logger.LogInformation("Next handshake scheduled for {nextHandShakeDateTime}", nextHandShakeDateTime);
131+
return false;
132+
}
133+
134+
135+
private async Task<bool> SetConfigState(Dictionary<string, string> state)
136+
{
137+
try
138+
{
139+
string jsonString = JsonSerializer.Serialize(state);
140+
using (var stream = GenerateStreamFromString(jsonString))
141+
{
142+
var blobFile = new BlobFile(stream, ConfigFileName);
143+
var result = await _blobStorageHelper.UploadFileToBlobStorage(_blobConnectionString, "config", blobFile, true);
144+
return result;
145+
}
146+
}
147+
catch (Exception ex)
148+
{
149+
_logger.LogError(ex, "Unable To set Config State");
150+
return false;
151+
}
152+
}
153+
154+
public static Stream GenerateStreamFromString(string s)
155+
{
156+
var stream = new MemoryStream();
157+
var writer = new StreamWriter(stream);
158+
writer.Write(s);
159+
writer.Flush();
160+
stream.Position = 0;
161+
return stream;
162+
}
163+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
<PropertyGroup>
3+
<TargetFramework>net8.0</TargetFramework>
4+
<AzureFunctionsVersion>v4</AzureFunctionsVersion>
5+
<OutputType>Exe</OutputType>
6+
<ImplicitUsings>enable</ImplicitUsings>
7+
<Nullable>enable</Nullable>
8+
</PropertyGroup>
9+
<ItemGroup>
10+
<PackageReference Include="Azure.Security.KeyVault.Certificates" Version="4.6.0" />
11+
<PackageReference Include="Microsoft.Azure.Functions.Worker" Version="1.20.1" />
12+
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Http" Version="3.1.0" />
13+
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore" Version="1.2.0" />
14+
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Timer" Version="4.1.0" />
15+
<PackageReference Include="Microsoft.Azure.Functions.Worker.Sdk" Version="1.16.4" />
16+
<PackageReference Include="Microsoft.ApplicationInsights.WorkerService" Version="2.21.0" />
17+
<PackageReference Include="Microsoft.Azure.Functions.Worker.ApplicationInsights" Version="1.1.0" />
18+
</ItemGroup>
19+
<ItemGroup>
20+
<None Update="host.json">
21+
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
22+
</None>
23+
<None Update="meshpfx.pfx">
24+
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
25+
</None>
26+
<None Update="meshServerSideCerts.crt">
27+
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
28+
</None>
29+
<None Update="local.settings.json">
30+
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
31+
<CopyToPublishDirectory>Never</CopyToPublishDirectory>
32+
</None>
33+
</ItemGroup>
34+
<ItemGroup>
35+
<Using Include="System.Threading.ExecutionContext" Alias="ExecutionContext" />
36+
</ItemGroup>
37+
<ItemGroup>
38+
<ProjectReference Include="..\..\Shared\Common\Common.csproj" />
39+
<ProjectReference Include="..\..\Shared\HealthChecks\HealthChecks.csproj" />
40+
</ItemGroup>
41+
</Project>
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
Microsoft Visual Studio Solution File, Format Version 12.00
2+
# Visual Studio Version 17
3+
VisualStudioVersion = 17.5.2.0
4+
MinimumVisualStudioVersion = 10.0.40219.1
5+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NemsMeshRetrieval", "NemsMeshRetrieval.csproj", "{F1E9566B-AA7E-8866-C03C-CD0ECA0DDBA0}"
6+
EndProject
7+
Global
8+
GlobalSection(SolutionConfigurationPlatforms) = preSolution
9+
Debug|Any CPU = Debug|Any CPU
10+
Release|Any CPU = Release|Any CPU
11+
EndGlobalSection
12+
GlobalSection(ProjectConfigurationPlatforms) = postSolution
13+
{F1E9566B-AA7E-8866-C03C-CD0ECA0DDBA0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
14+
{F1E9566B-AA7E-8866-C03C-CD0ECA0DDBA0}.Debug|Any CPU.Build.0 = Debug|Any CPU
15+
{F1E9566B-AA7E-8866-C03C-CD0ECA0DDBA0}.Release|Any CPU.ActiveCfg = Release|Any CPU
16+
{F1E9566B-AA7E-8866-C03C-CD0ECA0DDBA0}.Release|Any CPU.Build.0 = Release|Any CPU
17+
EndGlobalSection
18+
GlobalSection(SolutionProperties) = preSolution
19+
HideSolutionNode = FALSE
20+
EndGlobalSection
21+
GlobalSection(ExtensibilityGlobals) = postSolution
22+
SolutionGuid = {F51834F7-DFAB-493A-A66D-55B77976EA94}
23+
EndGlobalSection
24+
EndGlobal
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
namespace NHS.Screening.NemsMeshRetrieval;
2+
3+
using System.ComponentModel.DataAnnotations;
4+
5+
public class NemsMeshRetrievalConfig
6+
{
7+
public string MeshApiBaseUrl { get; set; }
8+
[Required]
9+
public string BSSMailBox { get; set; }
10+
[Required]
11+
public string MeshPassword { get; set; }
12+
[Required]
13+
public string MeshSharedKey {get; set;}
14+
public string MeshKeyPassphrase {get; set;}
15+
public string MeshKeyName {get; set;}
16+
public string KeyVaultConnectionString {get; set;}
17+
[Required]
18+
public string nemsmeshfolder_STORAGE {get; set;}
19+
public string ServerSideCerts { get; set; }
20+
public string MeshCertName { get; set; }
21+
public bool? BypassServerCertificateValidation {get;set;}
22+
}

0 commit comments

Comments
 (0)