From 73bdb8b9cd69964f48571f0908c49f8a8a57cf18 Mon Sep 17 00:00:00 2001 From: Tomasz 'Tom' Kandula Date: Sun, 11 Feb 2024 12:02:47 +0100 Subject: [PATCH 01/18] feat: add mailcow api key to settings --- .github/workflows/dev_build_test.yml | 1 + .github/workflows/master_build_test_publish.yml | 1 + .github/workflows/stage_build_test_publish.yml | 1 + EmailSender.Configuration/appsettings.Development.json | 3 ++- EmailSender.Configuration/appsettings.Production.json | 3 ++- EmailSender.Configuration/appsettings.Staging.json | 3 ++- 6 files changed, 9 insertions(+), 3 deletions(-) diff --git a/.github/workflows/dev_build_test.yml b/.github/workflows/dev_build_test.yml index b83c6ed..f244ef8 100644 --- a/.github/workflows/dev_build_test.yml +++ b/.github/workflows/dev_build_test.yml @@ -30,6 +30,7 @@ jobs: DbConnect: "${{ secrets.DBCONNECT_TEST }}" AZ_Storage_ContainerName: "${{ secrets.STORAGE_CONTAINER_NAME_TEST }}" AZ_Storage_ConnectionString: "${{ secrets.STORAGE_CONNECTION_STRING }}" + Mailcow_API_Key: "${{ secrets.MAILCOW_API_KEY }}" - name: Setup dotnet uses: actions/setup-dotnet@v4 diff --git a/.github/workflows/master_build_test_publish.yml b/.github/workflows/master_build_test_publish.yml index aed5a23..9f1f8cb 100644 --- a/.github/workflows/master_build_test_publish.yml +++ b/.github/workflows/master_build_test_publish.yml @@ -60,6 +60,7 @@ jobs: DbConnect: "${{ secrets.DBCONNECT_PROD }}" AZ_Storage_ContainerName: "${{ secrets.STORAGE_CONTAINER_NAME_PROD }}" AZ_Storage_ConnectionString: "${{ secrets.STORAGE_CONNECTION_STRING }}" + Mailcow_API_Key: "${{ secrets.MAILCOW_API_KEY }}" - name: Production | Build Docker (prepare) uses: azure/docker-login@v1.0.1 diff --git a/.github/workflows/stage_build_test_publish.yml b/.github/workflows/stage_build_test_publish.yml index bead69e..68e1dc1 100644 --- a/.github/workflows/stage_build_test_publish.yml +++ b/.github/workflows/stage_build_test_publish.yml @@ -23,6 +23,7 @@ jobs: DbConnect: "${{ secrets.DBCONNECT_STAGE }}" AZ_Storage_ContainerName: "${{ secrets.STORAGE_CONTAINER_NAME_STAGE }}" AZ_Storage_ConnectionString: "${{ secrets.STORAGE_CONNECTION_STRING }}" + Mailcow_API_Key: "${{ secrets.MAILCOW_API_KEY }}" - name: Staging | Build Docker (prepare) uses: azure/docker-login@v1.0.1 diff --git a/EmailSender.Configuration/appsettings.Development.json b/EmailSender.Configuration/appsettings.Development.json index 66b3129..6e82a8c 100644 --- a/EmailSender.Configuration/appsettings.Development.json +++ b/EmailSender.Configuration/appsettings.Development.json @@ -2,5 +2,6 @@ "AllowedHosts": "*", "DbConnect": "", "AZ_Storage_ContainerName": "", - "AZ_Storage_ConnectionString": "" + "AZ_Storage_ConnectionString": "", + "Mailcow_API_Key": "" } \ No newline at end of file diff --git a/EmailSender.Configuration/appsettings.Production.json b/EmailSender.Configuration/appsettings.Production.json index 66b3129..6e82a8c 100644 --- a/EmailSender.Configuration/appsettings.Production.json +++ b/EmailSender.Configuration/appsettings.Production.json @@ -2,5 +2,6 @@ "AllowedHosts": "*", "DbConnect": "", "AZ_Storage_ContainerName": "", - "AZ_Storage_ConnectionString": "" + "AZ_Storage_ConnectionString": "", + "Mailcow_API_Key": "" } \ No newline at end of file diff --git a/EmailSender.Configuration/appsettings.Staging.json b/EmailSender.Configuration/appsettings.Staging.json index 66b3129..6e82a8c 100644 --- a/EmailSender.Configuration/appsettings.Staging.json +++ b/EmailSender.Configuration/appsettings.Staging.json @@ -2,5 +2,6 @@ "AllowedHosts": "*", "DbConnect": "", "AZ_Storage_ContainerName": "", - "AZ_Storage_ConnectionString": "" + "AZ_Storage_ConnectionString": "", + "Mailcow_API_Key": "" } \ No newline at end of file From 8aa5f635ce9ad14e9ce8211846b977de65dd5cc4 Mon Sep 17 00:00:00 2001 From: Tomasz 'Tom' Kandula Date: Sun, 11 Feb 2024 12:03:10 +0100 Subject: [PATCH 02/18] feat: add query for getting mailcow status --- .../Mailcow/GetMailcowStatusQuery.cs | 5 +++++ .../Mailcow/GetMailcowStatusQueryHandler.cs | 13 +++++++++++++ .../Mailcow/GetMailcowStatusQueryResult.cs | 10 ++++++++++ 3 files changed, 28 insertions(+) create mode 100644 EmailSender.Backend/EmailSender.Backend.Application/Mailcow/GetMailcowStatusQuery.cs create mode 100644 EmailSender.Backend/EmailSender.Backend.Application/Mailcow/GetMailcowStatusQueryHandler.cs create mode 100644 EmailSender.Backend/EmailSender.Backend.Application/Mailcow/GetMailcowStatusQueryResult.cs diff --git a/EmailSender.Backend/EmailSender.Backend.Application/Mailcow/GetMailcowStatusQuery.cs b/EmailSender.Backend/EmailSender.Backend.Application/Mailcow/GetMailcowStatusQuery.cs new file mode 100644 index 0000000..4492f92 --- /dev/null +++ b/EmailSender.Backend/EmailSender.Backend.Application/Mailcow/GetMailcowStatusQuery.cs @@ -0,0 +1,5 @@ +using MediatR; + +namespace EmailSender.Backend.Application.Mailcow; + +public class GetMailcowStatusQuery : IRequest { } diff --git a/EmailSender.Backend/EmailSender.Backend.Application/Mailcow/GetMailcowStatusQueryHandler.cs b/EmailSender.Backend/EmailSender.Backend.Application/Mailcow/GetMailcowStatusQueryHandler.cs new file mode 100644 index 0000000..de8649f --- /dev/null +++ b/EmailSender.Backend/EmailSender.Backend.Application/Mailcow/GetMailcowStatusQueryHandler.cs @@ -0,0 +1,13 @@ +namespace EmailSender.Backend.Application.Mailcow; + +public class GetMailcowStatusQueryHandler : RequestHandler +{ + public override async Task Handle(GetMailcowStatusQuery request, CancellationToken cancellationToken) + { + + + + + return new GetMailcowStatusQueryResult(); + } +} \ No newline at end of file diff --git a/EmailSender.Backend/EmailSender.Backend.Application/Mailcow/GetMailcowStatusQueryResult.cs b/EmailSender.Backend/EmailSender.Backend.Application/Mailcow/GetMailcowStatusQueryResult.cs new file mode 100644 index 0000000..26f150c --- /dev/null +++ b/EmailSender.Backend/EmailSender.Backend.Application/Mailcow/GetMailcowStatusQueryResult.cs @@ -0,0 +1,10 @@ +using EmailSender.Backend.Application.Mailcow.Models; + +namespace EmailSender.Backend.Application.Mailcow; + +public class GetMailcowStatusQueryResult +{ + public bool IsRunning { get; set; } + + public IList? Results { get; set; } +} \ No newline at end of file From 6b4e5ed1ff58c626d6765b724de58a91243c3f1f Mon Sep 17 00:00:00 2001 From: Tomasz 'Tom' Kandula Date: Sun, 11 Feb 2024 12:03:21 +0100 Subject: [PATCH 03/18] feat: add model --- .../Mailcow/Models/Status.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 EmailSender.Backend/EmailSender.Backend.Application/Mailcow/Models/Status.cs diff --git a/EmailSender.Backend/EmailSender.Backend.Application/Mailcow/Models/Status.cs b/EmailSender.Backend/EmailSender.Backend.Application/Mailcow/Models/Status.cs new file mode 100644 index 0000000..5e74d13 --- /dev/null +++ b/EmailSender.Backend/EmailSender.Backend.Application/Mailcow/Models/Status.cs @@ -0,0 +1,10 @@ +namespace EmailSender.Backend.Application.Mailcow.Models; + +public class Status +{ + public string? Container { get; set; } + + public string? State { get; set; } + + public bool? StartedAt { get; set; } +} \ No newline at end of file From 2ec7697e2e6ddddcaf5bbc75bf31c2c735c69424 Mon Sep 17 00:00:00 2001 From: Tomasz 'Tom' Kandula Date: Sun, 11 Feb 2024 12:03:33 +0100 Subject: [PATCH 04/18] feat: add controller for mailcow --- .../Controllers/MailcowController.cs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 EmailSender.WebApi/Controllers/MailcowController.cs diff --git a/EmailSender.WebApi/Controllers/MailcowController.cs b/EmailSender.WebApi/Controllers/MailcowController.cs new file mode 100644 index 0000000..3ff293b --- /dev/null +++ b/EmailSender.WebApi/Controllers/MailcowController.cs @@ -0,0 +1,16 @@ +using EmailSender.Backend.Application.Mailcow; +using MediatR; +using Microsoft.AspNetCore.Mvc; + +namespace EmailSender.WebApi.Controllers; + +[ApiVersion("1.0")] +public class MailcowController : BaseController +{ + public MailcowController(IMediator mediator) : base(mediator) { } + + [HttpGet] + [ProducesResponseType(typeof(GetMailcowStatusQueryResult), StatusCodes.Status200OK)] + public async Task GetStatus() + => await Mediator.Send(new GetMailcowStatusQuery()); +} \ No newline at end of file From d206bb7a08b6b36c64ced3a5981b673a06d42d7d Mon Sep 17 00:00:00 2001 From: Tomasz 'Tom' Kandula Date: Sun, 11 Feb 2024 12:12:36 +0100 Subject: [PATCH 05/18] feat: add new error codes --- .../Resources/ErrorCodes.Designer.cs | 18 ++++++++++++++++++ .../Resources/ErrorCodes.resx | 9 +++++++++ 2 files changed, 27 insertions(+) diff --git a/EmailSender.Backend/EmailSender.Backend.Shared/Resources/ErrorCodes.Designer.cs b/EmailSender.Backend/EmailSender.Backend.Shared/Resources/ErrorCodes.Designer.cs index 794717a..8bda3f1 100644 --- a/EmailSender.Backend/EmailSender.Backend.Shared/Resources/ErrorCodes.Designer.cs +++ b/EmailSender.Backend/EmailSender.Backend.Shared/Resources/ErrorCodes.Designer.cs @@ -134,5 +134,23 @@ public static string INSUFFICIENT_PRIVILEGES { return ResourceManager.GetString("INSUFFICIENT_PRIVILEGES", resourceCulture); } } + + public static string HTTP_REQUEST_FAILED { + get { + return ResourceManager.GetString("HTTP_REQUEST_FAILED", resourceCulture); + } + } + + public static string CANNOT_PARSE { + get { + return ResourceManager.GetString("CANNOT_PARSE", resourceCulture); + } + } + + public static string ARGUMENT_EMPTY_OR_NULL { + get { + return ResourceManager.GetString("ARGUMENT_EMPTY_OR_NULL", resourceCulture); + } + } } } diff --git a/EmailSender.Backend/EmailSender.Backend.Shared/Resources/ErrorCodes.resx b/EmailSender.Backend/EmailSender.Backend.Shared/Resources/ErrorCodes.resx index 7d756cc..02e5fa1 100644 --- a/EmailSender.Backend/EmailSender.Backend.Shared/Resources/ErrorCodes.resx +++ b/EmailSender.Backend/EmailSender.Backend.Shared/Resources/ErrorCodes.resx @@ -63,4 +63,13 @@ Cannot execute action. Insufficient privileges + + HTTP request has failed + + + Cannot parse given string + + + Argument cannot be null or empty + \ No newline at end of file From 5a2a257d645f649f7f17f0cd6e86a4b1af424f7b Mon Sep 17 00:00:00 2001 From: Tomasz 'Tom' Kandula Date: Sun, 11 Feb 2024 12:12:59 +0100 Subject: [PATCH 06/18] feat: add http client service --- .../Abstractions/IAuthentication.cs | 3 + .../Abstractions/IHttpClientService.cs | 13 + .../Abstractions/IHttpClientServiceFactory.cs | 8 + .../Abstractions/IPayloadContent.cs | 3 + ...ilSender.Services.HttpClientService.csproj | 18 ++ .../HttpClientService.cs | 230 ++++++++++++++++++ .../HttpClientServiceFactory.cs | 14 ++ .../Models/BasicAuthentication.cs | 12 + .../Models/BearerAuthentication.cs | 10 + .../Models/Configuration.cs | 23 ++ .../Models/ContentDictionary.cs | 10 + .../Models/ContentString.cs | 10 + .../Models/ExecutionResult.cs | 10 + .../Models/HttpContentResult.cs | 12 + EmailSender.sln | 7 + 15 files changed, 383 insertions(+) create mode 100644 EmailSender.Services/EmailSender.Services.HttpClientService/Abstractions/IAuthentication.cs create mode 100644 EmailSender.Services/EmailSender.Services.HttpClientService/Abstractions/IHttpClientService.cs create mode 100644 EmailSender.Services/EmailSender.Services.HttpClientService/Abstractions/IHttpClientServiceFactory.cs create mode 100644 EmailSender.Services/EmailSender.Services.HttpClientService/Abstractions/IPayloadContent.cs create mode 100644 EmailSender.Services/EmailSender.Services.HttpClientService/EmailSender.Services.HttpClientService.csproj create mode 100644 EmailSender.Services/EmailSender.Services.HttpClientService/HttpClientService.cs create mode 100644 EmailSender.Services/EmailSender.Services.HttpClientService/HttpClientServiceFactory.cs create mode 100644 EmailSender.Services/EmailSender.Services.HttpClientService/Models/BasicAuthentication.cs create mode 100644 EmailSender.Services/EmailSender.Services.HttpClientService/Models/BearerAuthentication.cs create mode 100644 EmailSender.Services/EmailSender.Services.HttpClientService/Models/Configuration.cs create mode 100644 EmailSender.Services/EmailSender.Services.HttpClientService/Models/ContentDictionary.cs create mode 100644 EmailSender.Services/EmailSender.Services.HttpClientService/Models/ContentString.cs create mode 100644 EmailSender.Services/EmailSender.Services.HttpClientService/Models/ExecutionResult.cs create mode 100644 EmailSender.Services/EmailSender.Services.HttpClientService/Models/HttpContentResult.cs diff --git a/EmailSender.Services/EmailSender.Services.HttpClientService/Abstractions/IAuthentication.cs b/EmailSender.Services/EmailSender.Services.HttpClientService/Abstractions/IAuthentication.cs new file mode 100644 index 0000000..3c829dc --- /dev/null +++ b/EmailSender.Services/EmailSender.Services.HttpClientService/Abstractions/IAuthentication.cs @@ -0,0 +1,3 @@ +namespace EmailSender.Services.HttpClientService.Abstractions; + +public interface IAuthentication { } \ No newline at end of file diff --git a/EmailSender.Services/EmailSender.Services.HttpClientService/Abstractions/IHttpClientService.cs b/EmailSender.Services/EmailSender.Services.HttpClientService/Abstractions/IHttpClientService.cs new file mode 100644 index 0000000..c693bd2 --- /dev/null +++ b/EmailSender.Services/EmailSender.Services.HttpClientService/Abstractions/IHttpClientService.cs @@ -0,0 +1,13 @@ +using EmailSender.Services.HttpClientService.Models; +using Microsoft.AspNetCore.Http; + +namespace EmailSender.Services.HttpClientService.Abstractions; + +public interface IHttpClientService +{ + Task ProxyRequest(Configuration configuration, HttpResponse response, CancellationToken cancellationToken = default); + + Task Execute(Configuration configuration, CancellationToken cancellationToken = default); + + Task Execute(Configuration configuration, CancellationToken cancellationToken = default); +} \ No newline at end of file diff --git a/EmailSender.Services/EmailSender.Services.HttpClientService/Abstractions/IHttpClientServiceFactory.cs b/EmailSender.Services/EmailSender.Services.HttpClientService/Abstractions/IHttpClientServiceFactory.cs new file mode 100644 index 0000000..9f54d2a --- /dev/null +++ b/EmailSender.Services/EmailSender.Services.HttpClientService/Abstractions/IHttpClientServiceFactory.cs @@ -0,0 +1,8 @@ +using EmailSender.Backend.Core.Utilities.LoggerService; + +namespace EmailSender.Services.HttpClientService.Abstractions; + +public interface IHttpClientServiceFactory +{ + IHttpClientService Create(bool allowAutoRedirect, ILoggerService loggerService); +} \ No newline at end of file diff --git a/EmailSender.Services/EmailSender.Services.HttpClientService/Abstractions/IPayloadContent.cs b/EmailSender.Services/EmailSender.Services.HttpClientService/Abstractions/IPayloadContent.cs new file mode 100644 index 0000000..efaefd9 --- /dev/null +++ b/EmailSender.Services/EmailSender.Services.HttpClientService/Abstractions/IPayloadContent.cs @@ -0,0 +1,3 @@ +namespace EmailSender.Services.HttpClientService.Abstractions; + +public interface IPayloadContent { } diff --git a/EmailSender.Services/EmailSender.Services.HttpClientService/EmailSender.Services.HttpClientService.csproj b/EmailSender.Services/EmailSender.Services.HttpClientService/EmailSender.Services.HttpClientService.csproj new file mode 100644 index 0000000..cba3877 --- /dev/null +++ b/EmailSender.Services/EmailSender.Services.HttpClientService/EmailSender.Services.HttpClientService.csproj @@ -0,0 +1,18 @@ + + + + net6.0 + 10 + enable + enable + true + true + true + + + + + + + + diff --git a/EmailSender.Services/EmailSender.Services.HttpClientService/HttpClientService.cs b/EmailSender.Services/EmailSender.Services.HttpClientService/HttpClientService.cs new file mode 100644 index 0000000..5493a3d --- /dev/null +++ b/EmailSender.Services/EmailSender.Services.HttpClientService/HttpClientService.cs @@ -0,0 +1,230 @@ +using System.Net; +using System.Net.Http.Headers; +using System.Text; +using EmailSender.Backend.Core.Exceptions; +using EmailSender.Backend.Core.Utilities.LoggerService; +using EmailSender.Backend.Shared.Resources; +using EmailSender.Services.HttpClientService.Abstractions; +using EmailSender.Services.HttpClientService.Models; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.WebUtilities; +using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; + +namespace EmailSender.Services.HttpClientService; + +public class HttpClientService : IHttpClientService +{ + private const string Header = "Authorization"; + + private const string Basic = "Basic"; + + private const string Bearer = "Bearer"; + + private readonly HttpClient _httpClient; + + private readonly ILoggerService _loggerService; + + public HttpClientService(HttpClient httpClient, ILoggerService loggerService) + { + _httpClient = httpClient; + _loggerService = loggerService; + } + + public async Task ProxyRequest(Configuration configuration, HttpResponse response, CancellationToken cancellationToken = default) + { + var preparedRequest = PrepareProxyRequest(configuration); + await ExecuteRangeRequest(response, preparedRequest, cancellationToken); + } + + public async Task Execute(Configuration configuration, CancellationToken cancellationToken = default) + { + VerifyConfigurationArgument(configuration); + + var response = await GetResponse(configuration, cancellationToken); + var contentType = response.Content.Headers.ContentType; + var content = await response.Content.ReadAsByteArrayAsync(cancellationToken); + + if (response.IsSuccessStatusCode || response.StatusCode == HttpStatusCode.Redirect) + return new ExecutionResult + { + StatusCode = response.StatusCode, + ContentType = contentType, + Content = content + }; + + var stringContent = Encoding.ASCII.GetString(content); + _loggerService.LogError($"{ErrorCodes.HTTP_REQUEST_FAILED}. Full response: {stringContent}."); + throw new BusinessException(nameof(ErrorCodes.HTTP_REQUEST_FAILED), ErrorCodes.HTTP_REQUEST_FAILED); + } + + public async Task Execute(Configuration configuration, CancellationToken cancellationToken = default) + { + VerifyConfigurationArgument(configuration); + + var response = await GetResponse(configuration, cancellationToken); + var content = await response.Content.ReadAsByteArrayAsync(cancellationToken); + var stringContent = Encoding.ASCII.GetString(content); + + try + { + if (response.IsSuccessStatusCode || response.StatusCode == HttpStatusCode.Redirect) + return JsonConvert.DeserializeObject(stringContent, GetSettings())!; + } + catch (Exception exception) + { + _loggerService.LogError($"{ErrorCodes.CANNOT_PARSE}. Exception: {exception}. Full response: {stringContent}."); + throw new BusinessException(nameof(ErrorCodes.CANNOT_PARSE), ErrorCodes.CANNOT_PARSE); + } + + _loggerService.LogError($"{ErrorCodes.HTTP_REQUEST_FAILED}. Full response: {stringContent}."); + throw new BusinessException(nameof(ErrorCodes.HTTP_REQUEST_FAILED), ErrorCodes.HTTP_REQUEST_FAILED); + } + + private static HttpContent PrepareContent(IPayloadContent content) + { + switch (content) + { + case ContentDictionary contentDictionary: + return new FormUrlEncodedContent(contentDictionary.Payload); + + case ContentString contentString: + var serialized = JsonConvert.SerializeObject(contentString.Payload, GetSettings()); + return new StringContent(serialized, Encoding.Default, "application/json"); + + default: + throw new BusinessException("Unsupported content type."); + } + } + + private async Task GetResponse(Configuration configuration, CancellationToken cancellationToken = default) + { + var requestUri = configuration.Url; + if (configuration.QueryParameters is not null && configuration.QueryParameters.Any()) + requestUri = QueryHelpers.AddQueryString(configuration.Url, configuration.QueryParameters); + + using var request = new HttpRequestMessage(new HttpMethod(configuration.Method), requestUri); + + if (configuration.Headers != null) + { + foreach (var (name, value) in configuration.Headers) + { + request.Headers.TryAddWithoutValidation(name, value); + } + } + + if (configuration.PayloadContent is not null) + request.Content = PrepareContent(configuration.PayloadContent); + + if (configuration.Authentication != null) + ApplyAuthentication(request, configuration.Authentication); + + return await _httpClient.SendAsync(request, cancellationToken); + } + + private static JsonSerializerSettings GetSettings() + { + return new JsonSerializerSettings + { + ContractResolver = new DefaultContractResolver + { + NamingStrategy = new CamelCaseNamingStrategy() + }, + NullValueHandling = NullValueHandling.Ignore + }; + } + + private static void ApplyAuthentication(HttpRequestMessage request, IAuthentication authentication) + { + switch (authentication) + { + case BasicAuthentication basicAuthentication: + var basic = SetAuthentication(basicAuthentication.Login, basicAuthentication.Password); + request.Headers.TryAddWithoutValidation(Header, basic); + break; + + case BearerAuthentication bearerAuthentication: + var bearer = SetAuthentication(bearerAuthentication.Token); + request.Headers.TryAddWithoutValidation(Header, bearer); + break; + } + } + + private static string SetAuthentication(string login, string password) + { + if (string.IsNullOrEmpty(login)) + { + const string message = $"Argument '{nameof(login)}' cannot be null or empty."; + throw new BusinessException(nameof(ErrorCodes.ARGUMENT_EMPTY_OR_NULL),message); + } + + var base64 = Convert.ToBase64String(Encoding.ASCII.GetBytes($"{login}:{password}")); + return $"{Basic} {base64}"; + } + + private static string SetAuthentication(string token) + { + if (!string.IsNullOrEmpty(token)) return $"{Bearer} {token}"; + + const string message = $"Argument '{nameof(token)}' cannot be null or empty."; + throw new BusinessException(nameof(ErrorCodes.ARGUMENT_EMPTY_OR_NULL), message); + } + + private static void VerifyConfigurationArgument(Configuration configuration) + { + string message; + + if (string.IsNullOrEmpty(configuration.Method)) + { + message = $"Argument '{nameof(configuration.Method)}' cannot be null or empty."; + throw new BusinessException(nameof(ErrorCodes.ARGUMENT_EMPTY_OR_NULL), message); + } + + if (!string.IsNullOrEmpty(configuration.Url)) return; + + message = $"Argument '{nameof(configuration.Url)}' cannot be null or empty."; + throw new BusinessException(nameof(ErrorCodes.ARGUMENT_EMPTY_OR_NULL), message); + } + + private static HttpRequestMessage PrepareProxyRequest(Configuration configuration) + { + var url = configuration.Url; + if (configuration.QueryParameters is not null && configuration.QueryParameters.Any()) + url = QueryHelpers.AddQueryString(url, configuration.QueryParameters); + + var request = new HttpRequestMessage + { + Method = new HttpMethod(configuration.Method), + RequestUri = new Uri(url, UriKind.RelativeOrAbsolute), + }; + + if (configuration.Range is not null && configuration.Range.Value.Count != 0) + request.Headers.Range = RangeHeaderValue.Parse(configuration.Range); + + if (configuration.Authentication != null) + ApplyAuthentication(request, configuration.Authentication); + + return request; + } + + private async Task ExecuteRangeRequest(HttpResponse response, HttpRequestMessage request, CancellationToken cancellationToken = default) + { + var origin = await _httpClient.SendAsync(request, cancellationToken); + response.StatusCode = (int)origin.StatusCode; + + foreach (var header in origin.Headers) + { + response.Headers[header.Key] = header.Value.ToArray(); + } + + foreach (var header in origin.Content.Headers) + { + response.Headers[header.Key] = header.Value.ToArray(); + } + + response.Headers.Remove("server"); + response.Headers.Remove("transfer-encoding"); + + await origin.Content.CopyToAsync(response.Body, cancellationToken); + } +} \ No newline at end of file diff --git a/EmailSender.Services/EmailSender.Services.HttpClientService/HttpClientServiceFactory.cs b/EmailSender.Services/EmailSender.Services.HttpClientService/HttpClientServiceFactory.cs new file mode 100644 index 0000000..66a75b0 --- /dev/null +++ b/EmailSender.Services/EmailSender.Services.HttpClientService/HttpClientServiceFactory.cs @@ -0,0 +1,14 @@ +using EmailSender.Backend.Core.Utilities.LoggerService; +using EmailSender.Services.HttpClientService.Abstractions; + +namespace EmailSender.Services.HttpClientService; + +public class HttpClientServiceFactory : IHttpClientServiceFactory +{ + public IHttpClientService Create(bool allowAutoRedirect, ILoggerService loggerService) + { + var handler = new HttpClientHandler { AllowAutoRedirect = allowAutoRedirect }; + var client = new HttpClient(handler); + return new HttpClientService(client, loggerService); + } +} \ No newline at end of file diff --git a/EmailSender.Services/EmailSender.Services.HttpClientService/Models/BasicAuthentication.cs b/EmailSender.Services/EmailSender.Services.HttpClientService/Models/BasicAuthentication.cs new file mode 100644 index 0000000..15bbb56 --- /dev/null +++ b/EmailSender.Services/EmailSender.Services.HttpClientService/Models/BasicAuthentication.cs @@ -0,0 +1,12 @@ +using System.Diagnostics.CodeAnalysis; +using EmailSender.Services.HttpClientService.Abstractions; + +namespace EmailSender.Services.HttpClientService.Models; + +[ExcludeFromCodeCoverage] +public class BasicAuthentication : IAuthentication +{ + public string Login { get; set; } = ""; + + public string Password { get; set; } = ""; +} \ No newline at end of file diff --git a/EmailSender.Services/EmailSender.Services.HttpClientService/Models/BearerAuthentication.cs b/EmailSender.Services/EmailSender.Services.HttpClientService/Models/BearerAuthentication.cs new file mode 100644 index 0000000..e54a95b --- /dev/null +++ b/EmailSender.Services/EmailSender.Services.HttpClientService/Models/BearerAuthentication.cs @@ -0,0 +1,10 @@ +using System.Diagnostics.CodeAnalysis; +using EmailSender.Services.HttpClientService.Abstractions; + +namespace EmailSender.Services.HttpClientService.Models; + +[ExcludeFromCodeCoverage] +public class BearerAuthentication : IAuthentication +{ + public string Token { get; set; } = ""; +} \ No newline at end of file diff --git a/EmailSender.Services/EmailSender.Services.HttpClientService/Models/Configuration.cs b/EmailSender.Services/EmailSender.Services.HttpClientService/Models/Configuration.cs new file mode 100644 index 0000000..0f5e508 --- /dev/null +++ b/EmailSender.Services/EmailSender.Services.HttpClientService/Models/Configuration.cs @@ -0,0 +1,23 @@ +using System.Diagnostics.CodeAnalysis; +using EmailSender.Services.HttpClientService.Abstractions; +using Microsoft.Extensions.Primitives; + +namespace EmailSender.Services.HttpClientService.Models; + +[ExcludeFromCodeCoverage] +public class Configuration +{ + public string Url { get; set; } = ""; + + public string Method { get; set; } = ""; + + public StringValues? Range { get; set; } + + public IDictionary? Headers { get; set; } + + public IDictionary? QueryParameters { get; set; } + + public IAuthentication? Authentication { get; set; } + + public IPayloadContent? PayloadContent { get; set; } +} \ No newline at end of file diff --git a/EmailSender.Services/EmailSender.Services.HttpClientService/Models/ContentDictionary.cs b/EmailSender.Services/EmailSender.Services.HttpClientService/Models/ContentDictionary.cs new file mode 100644 index 0000000..039b492 --- /dev/null +++ b/EmailSender.Services/EmailSender.Services.HttpClientService/Models/ContentDictionary.cs @@ -0,0 +1,10 @@ +using System.Diagnostics.CodeAnalysis; +using EmailSender.Services.HttpClientService.Abstractions; + +namespace EmailSender.Services.HttpClientService.Models; + +[ExcludeFromCodeCoverage] +public class ContentDictionary : IPayloadContent +{ + public IDictionary Payload { get; set; } = new Dictionary(); +} \ No newline at end of file diff --git a/EmailSender.Services/EmailSender.Services.HttpClientService/Models/ContentString.cs b/EmailSender.Services/EmailSender.Services.HttpClientService/Models/ContentString.cs new file mode 100644 index 0000000..b8682a0 --- /dev/null +++ b/EmailSender.Services/EmailSender.Services.HttpClientService/Models/ContentString.cs @@ -0,0 +1,10 @@ +using System.Diagnostics.CodeAnalysis; +using EmailSender.Services.HttpClientService.Abstractions; + +namespace EmailSender.Services.HttpClientService.Models; + +[ExcludeFromCodeCoverage] +public class ContentString : IPayloadContent +{ + public object? Payload { get; set; } +} \ No newline at end of file diff --git a/EmailSender.Services/EmailSender.Services.HttpClientService/Models/ExecutionResult.cs b/EmailSender.Services/EmailSender.Services.HttpClientService/Models/ExecutionResult.cs new file mode 100644 index 0000000..965a2a5 --- /dev/null +++ b/EmailSender.Services/EmailSender.Services.HttpClientService/Models/ExecutionResult.cs @@ -0,0 +1,10 @@ +using System.Diagnostics.CodeAnalysis; +using System.Net; + +namespace EmailSender.Services.HttpClientService.Models; + +[ExcludeFromCodeCoverage] +public class ExecutionResult : HttpContentResult +{ + public HttpStatusCode StatusCode { get; set; } +} \ No newline at end of file diff --git a/EmailSender.Services/EmailSender.Services.HttpClientService/Models/HttpContentResult.cs b/EmailSender.Services/EmailSender.Services.HttpClientService/Models/HttpContentResult.cs new file mode 100644 index 0000000..80c8a78 --- /dev/null +++ b/EmailSender.Services/EmailSender.Services.HttpClientService/Models/HttpContentResult.cs @@ -0,0 +1,12 @@ +using System.Diagnostics.CodeAnalysis; +using System.Net.Http.Headers; + +namespace EmailSender.Services.HttpClientService.Models; + +[ExcludeFromCodeCoverage] +public class HttpContentResult +{ + public MediaTypeHeaderValue? ContentType { get; set; } + + public byte[]? Content { get; set; } +} \ No newline at end of file diff --git a/EmailSender.sln b/EmailSender.sln index 05f6d0a..a1a06ab 100644 --- a/EmailSender.sln +++ b/EmailSender.sln @@ -63,6 +63,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "4. Docker", "4. Docker", "{ dockerfile = dockerfile EndProjectSection EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EmailSender.Services.HttpClientService", "EmailSender.Services\EmailSender.Services.HttpClientService\EmailSender.Services.HttpClientService.csproj", "{4C4140B2-25EF-4E97-A767-46CAE4638853}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -121,6 +123,10 @@ Global {FE78528E-BAF5-4544-8CF9-F8C9C6B05F92}.Debug|Any CPU.Build.0 = Debug|Any CPU {FE78528E-BAF5-4544-8CF9-F8C9C6B05F92}.Release|Any CPU.ActiveCfg = Release|Any CPU {FE78528E-BAF5-4544-8CF9-F8C9C6B05F92}.Release|Any CPU.Build.0 = Release|Any CPU + {4C4140B2-25EF-4E97-A767-46CAE4638853}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4C4140B2-25EF-4E97-A767-46CAE4638853}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4C4140B2-25EF-4E97-A767-46CAE4638853}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4C4140B2-25EF-4E97-A767-46CAE4638853}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {3891C8E1-909A-4D60-874B-1A149D8A3605} = {C2719A80-86A0-4E87-A498-3EEDD5C9179E} @@ -136,5 +142,6 @@ Global {ED5DD42A-5B36-4815-B817-9FAF068AEA54} = {21288831-4321-4486-94E9-F845B0686DEA} {FE78528E-BAF5-4544-8CF9-F8C9C6B05F92} = {C2719A80-86A0-4E87-A498-3EEDD5C9179E} {5E005153-349C-4653-B1F6-D6EBEFEFFB3A} = {14D3E1FE-C526-4670-BED4-5DED8546EA83} + {4C4140B2-25EF-4E97-A767-46CAE4638853} = {21288831-4321-4486-94E9-F845B0686DEA} EndGlobalSection EndGlobal From fa6a66a8c006ee265f6c9458a9d5edc71f846670 Mon Sep 17 00:00:00 2001 From: Tomasz 'Tom' Kandula Date: Sun, 11 Feb 2024 12:21:32 +0100 Subject: [PATCH 07/18] feat: register http client service --- EmailSender.WebApi/Configuration/Dependencies.cs | 4 +++- EmailSender.WebApi/EmailSender.WebApi.csproj | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/EmailSender.WebApi/Configuration/Dependencies.cs b/EmailSender.WebApi/Configuration/Dependencies.cs index f6f343a..d4a3161 100644 --- a/EmailSender.WebApi/Configuration/Dependencies.cs +++ b/EmailSender.WebApi/Configuration/Dependencies.cs @@ -8,6 +8,8 @@ using EmailSender.Persistence.Database; using EmailSender.Persistence.Database.Initializer; using EmailSender.Services.BehaviourService; +using EmailSender.Services.HttpClientService; +using EmailSender.Services.HttpClientService.Abstractions; using EmailSender.Services.SenderService; using EmailSender.Services.SmtpService; using EmailSender.Services.UserService; @@ -53,8 +55,8 @@ private static void SetupDatabase(IServiceCollection services, IConfiguration co private static void SetupServices(IServiceCollection services) { services.AddHttpContextAccessor(); + services.AddSingleton(_ => new HttpClientServiceFactory()); - services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); diff --git a/EmailSender.WebApi/EmailSender.WebApi.csproj b/EmailSender.WebApi/EmailSender.WebApi.csproj index 1edef5a..806e2fc 100644 --- a/EmailSender.WebApi/EmailSender.WebApi.csproj +++ b/EmailSender.WebApi/EmailSender.WebApi.csproj @@ -28,6 +28,7 @@ + From 797037a791e26eec4b0f95e84fd52cbbf6cf5d92 Mon Sep 17 00:00:00 2001 From: Tomasz 'Tom' Kandula Date: Sun, 11 Feb 2024 12:24:25 +0100 Subject: [PATCH 08/18] feat: remove unused code --- .../Logger/GetLogFileContentQuery.cs | 9 ------- .../Logger/GetLogFileContentQueryHandler.cs | 22 ----------------- .../Logger/GetLogFileContentQueryValidator.cs | 18 -------------- .../Logger/GetLogFilesListQuery.cs | 5 ---- .../Logger/GetLogFilesListQueryHandler.cs | 24 ------------------- .../Logger/GetLogFilesListQueryResult.cs | 6 ----- .../Controllers/LoggerController.cs | 24 ------------------- 7 files changed, 108 deletions(-) delete mode 100644 EmailSender.Backend/EmailSender.Backend.Application/Logger/GetLogFileContentQuery.cs delete mode 100644 EmailSender.Backend/EmailSender.Backend.Application/Logger/GetLogFileContentQueryHandler.cs delete mode 100644 EmailSender.Backend/EmailSender.Backend.Application/Logger/GetLogFileContentQueryValidator.cs delete mode 100644 EmailSender.Backend/EmailSender.Backend.Application/Logger/GetLogFilesListQuery.cs delete mode 100644 EmailSender.Backend/EmailSender.Backend.Application/Logger/GetLogFilesListQueryHandler.cs delete mode 100644 EmailSender.Backend/EmailSender.Backend.Application/Logger/GetLogFilesListQueryResult.cs delete mode 100644 EmailSender.WebApi/Controllers/LoggerController.cs diff --git a/EmailSender.Backend/EmailSender.Backend.Application/Logger/GetLogFileContentQuery.cs b/EmailSender.Backend/EmailSender.Backend.Application/Logger/GetLogFileContentQuery.cs deleted file mode 100644 index 8ebd082..0000000 --- a/EmailSender.Backend/EmailSender.Backend.Application/Logger/GetLogFileContentQuery.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Microsoft.AspNetCore.Mvc; -using MediatR; - -namespace EmailSender.Backend.Application.Logger; - -public class GetLogFileContentQuery : IRequest -{ - public string LogFileName { get; set; } = ""; -} \ No newline at end of file diff --git a/EmailSender.Backend/EmailSender.Backend.Application/Logger/GetLogFileContentQueryHandler.cs b/EmailSender.Backend/EmailSender.Backend.Application/Logger/GetLogFileContentQueryHandler.cs deleted file mode 100644 index f804d58..0000000 --- a/EmailSender.Backend/EmailSender.Backend.Application/Logger/GetLogFileContentQueryHandler.cs +++ /dev/null @@ -1,22 +0,0 @@ -using EmailSender.Backend.Core.Exceptions; -using EmailSender.Backend.Shared.Resources; -using Microsoft.AspNetCore.Mvc; - -namespace EmailSender.Backend.Application.Logger; - -public class GetLogFileContentQueryHandler : RequestHandler -{ - public override async Task Handle(GetLogFileContentQuery request, CancellationToken cancellationToken) - { - var pathToFolder = $"{AppDomain.CurrentDomain.BaseDirectory}logs"; - if (!Directory.Exists(pathToFolder)) - throw new BusinessException(nameof(ErrorCodes.ERROR_UNEXPECTED), ErrorCodes.ERROR_UNEXPECTED); - - var fullFilePath = $"{pathToFolder}{Path.DirectorySeparatorChar}{request.LogFileName}"; - if (!File.Exists(fullFilePath)) - throw new BusinessException(nameof(ErrorCodes.FILE_NOT_FOUND), ErrorCodes.FILE_NOT_FOUND); - - var fileContent = await File.ReadAllBytesAsync(fullFilePath, cancellationToken); - return new FileContentResult(fileContent, "text/plain"); - } -} \ No newline at end of file diff --git a/EmailSender.Backend/EmailSender.Backend.Application/Logger/GetLogFileContentQueryValidator.cs b/EmailSender.Backend/EmailSender.Backend.Application/Logger/GetLogFileContentQueryValidator.cs deleted file mode 100644 index d23dc51..0000000 --- a/EmailSender.Backend/EmailSender.Backend.Application/Logger/GetLogFileContentQueryValidator.cs +++ /dev/null @@ -1,18 +0,0 @@ -using EmailSender.Backend.Shared.Resources; -using FluentValidation; - -namespace EmailSender.Backend.Application.Logger; - -public class GetLogFileContentQueryValidator : AbstractValidator -{ - public GetLogFileContentQueryValidator() - { - RuleFor(query => query.LogFileName) - .NotEmpty() - .WithErrorCode(nameof(ValidationCodes.REQUIRED)) - .WithMessage(ValidationCodes.REQUIRED) - .MaximumLength(255) - .WithErrorCode(nameof(ValidationCodes.VALUE_TOO_LONG)) - .WithMessage(ValidationCodes.VALUE_TOO_LONG); - } -} \ No newline at end of file diff --git a/EmailSender.Backend/EmailSender.Backend.Application/Logger/GetLogFilesListQuery.cs b/EmailSender.Backend/EmailSender.Backend.Application/Logger/GetLogFilesListQuery.cs deleted file mode 100644 index dd6970a..0000000 --- a/EmailSender.Backend/EmailSender.Backend.Application/Logger/GetLogFilesListQuery.cs +++ /dev/null @@ -1,5 +0,0 @@ -using MediatR; - -namespace EmailSender.Backend.Application.Logger; - -public class GetLogFilesListQuery : IRequest { } \ No newline at end of file diff --git a/EmailSender.Backend/EmailSender.Backend.Application/Logger/GetLogFilesListQueryHandler.cs b/EmailSender.Backend/EmailSender.Backend.Application/Logger/GetLogFilesListQueryHandler.cs deleted file mode 100644 index 137b5b9..0000000 --- a/EmailSender.Backend/EmailSender.Backend.Application/Logger/GetLogFilesListQueryHandler.cs +++ /dev/null @@ -1,24 +0,0 @@ -namespace EmailSender.Backend.Application.Logger; - -public class GetLogFilesListQueryHandler : RequestHandler -{ - public override async Task Handle(GetLogFilesListQuery request, CancellationToken cancellationToken) - { - var pathToFolder = $"{AppDomain.CurrentDomain.BaseDirectory}logs"; - if (!Directory.Exists(pathToFolder)) - return new GetLogFilesListQueryResult(); - - var fullPathFileList = Directory.EnumerateFiles(pathToFolder, "*.txt", SearchOption.TopDirectoryOnly); - var logFiles = new List(); - - foreach (var item in fullPathFileList) - { - logFiles.Add(Path.GetFileName(item)); - } - - return await Task.FromResult(new GetLogFilesListQueryResult - { - LogFiles = logFiles - }); - } -} \ No newline at end of file diff --git a/EmailSender.Backend/EmailSender.Backend.Application/Logger/GetLogFilesListQueryResult.cs b/EmailSender.Backend/EmailSender.Backend.Application/Logger/GetLogFilesListQueryResult.cs deleted file mode 100644 index 4974e85..0000000 --- a/EmailSender.Backend/EmailSender.Backend.Application/Logger/GetLogFilesListQueryResult.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace EmailSender.Backend.Application.Logger; - -public class GetLogFilesListQueryResult -{ - public List LogFiles { get; set; } = new(); -} \ No newline at end of file diff --git a/EmailSender.WebApi/Controllers/LoggerController.cs b/EmailSender.WebApi/Controllers/LoggerController.cs deleted file mode 100644 index 76d1700..0000000 --- a/EmailSender.WebApi/Controllers/LoggerController.cs +++ /dev/null @@ -1,24 +0,0 @@ -using EmailSender.Backend.Application.Logger; -using EmailSender.Backend.Shared.Attributes; -using Microsoft.AspNetCore.Mvc; -using MediatR; - -namespace EmailSender.WebApi.Controllers; - -[ApiVersion("1.0")] -public class LoggerController : BaseController -{ - public LoggerController(IMediator mediator) : base(mediator) { } - - [HttpGet] - [RequireAdministrator] - [ProducesResponseType(typeof(GetLogFilesListQueryResult), StatusCodes.Status200OK)] - public async Task GetLogFilesList([FromHeader(Name = HeaderName)] string privateKey) - => await Mediator.Send(new GetLogFilesListQuery()); - - [HttpGet("{fileName}")] - [RequireAdministrator] - [ProducesResponseType(typeof(IActionResult), StatusCodes.Status200OK)] - public async Task GetLogFileContent([FromRoute] string fileName, [FromHeader(Name = HeaderName)] string privateKey) - => await Mediator.Send(new GetLogFileContentQuery { LogFileName = fileName }); -} \ No newline at end of file From 745128b56d36e0d80d26380a1e6d2a798f642794 Mon Sep 17 00:00:00 2001 From: Tomasz 'Tom' Kandula Date: Sun, 11 Feb 2024 13:51:37 +0100 Subject: [PATCH 09/18] feat: add status url --- .github/workflows/dev_build_test.yml | 1 + .github/workflows/master_build_test_publish.yml | 1 + .github/workflows/stage_build_test_publish.yml | 1 + EmailSender.Configuration/appsettings.Development.json | 3 ++- EmailSender.Configuration/appsettings.Production.json | 3 ++- EmailSender.Configuration/appsettings.Staging.json | 3 ++- 6 files changed, 9 insertions(+), 3 deletions(-) diff --git a/.github/workflows/dev_build_test.yml b/.github/workflows/dev_build_test.yml index f244ef8..ac32a05 100644 --- a/.github/workflows/dev_build_test.yml +++ b/.github/workflows/dev_build_test.yml @@ -31,6 +31,7 @@ jobs: AZ_Storage_ContainerName: "${{ secrets.STORAGE_CONTAINER_NAME_TEST }}" AZ_Storage_ConnectionString: "${{ secrets.STORAGE_CONNECTION_STRING }}" Mailcow_API_Key: "${{ secrets.MAILCOW_API_KEY }}" + Mailcow_Status_Url: "${{ secrets.MAILCOW_STATUS_URL }}" - name: Setup dotnet uses: actions/setup-dotnet@v4 diff --git a/.github/workflows/master_build_test_publish.yml b/.github/workflows/master_build_test_publish.yml index 9f1f8cb..dd3fd35 100644 --- a/.github/workflows/master_build_test_publish.yml +++ b/.github/workflows/master_build_test_publish.yml @@ -61,6 +61,7 @@ jobs: AZ_Storage_ContainerName: "${{ secrets.STORAGE_CONTAINER_NAME_PROD }}" AZ_Storage_ConnectionString: "${{ secrets.STORAGE_CONNECTION_STRING }}" Mailcow_API_Key: "${{ secrets.MAILCOW_API_KEY }}" + Mailcow_Status_Url: "${{ secrets.MAILCOW_STATUS_URL }}" - name: Production | Build Docker (prepare) uses: azure/docker-login@v1.0.1 diff --git a/.github/workflows/stage_build_test_publish.yml b/.github/workflows/stage_build_test_publish.yml index 68e1dc1..7480fe6 100644 --- a/.github/workflows/stage_build_test_publish.yml +++ b/.github/workflows/stage_build_test_publish.yml @@ -24,6 +24,7 @@ jobs: AZ_Storage_ContainerName: "${{ secrets.STORAGE_CONTAINER_NAME_STAGE }}" AZ_Storage_ConnectionString: "${{ secrets.STORAGE_CONNECTION_STRING }}" Mailcow_API_Key: "${{ secrets.MAILCOW_API_KEY }}" + Mailcow_Status_Url: "${{ secrets.MAILCOW_STATUS_URL }}" - name: Staging | Build Docker (prepare) uses: azure/docker-login@v1.0.1 diff --git a/EmailSender.Configuration/appsettings.Development.json b/EmailSender.Configuration/appsettings.Development.json index 6e82a8c..800464f 100644 --- a/EmailSender.Configuration/appsettings.Development.json +++ b/EmailSender.Configuration/appsettings.Development.json @@ -3,5 +3,6 @@ "DbConnect": "", "AZ_Storage_ContainerName": "", "AZ_Storage_ConnectionString": "", - "Mailcow_API_Key": "" + "Mailcow_API_Key": "", + "Mailcow_Status_Url": "" } \ No newline at end of file diff --git a/EmailSender.Configuration/appsettings.Production.json b/EmailSender.Configuration/appsettings.Production.json index 6e82a8c..800464f 100644 --- a/EmailSender.Configuration/appsettings.Production.json +++ b/EmailSender.Configuration/appsettings.Production.json @@ -3,5 +3,6 @@ "DbConnect": "", "AZ_Storage_ContainerName": "", "AZ_Storage_ConnectionString": "", - "Mailcow_API_Key": "" + "Mailcow_API_Key": "", + "Mailcow_Status_Url": "" } \ No newline at end of file diff --git a/EmailSender.Configuration/appsettings.Staging.json b/EmailSender.Configuration/appsettings.Staging.json index 6e82a8c..800464f 100644 --- a/EmailSender.Configuration/appsettings.Staging.json +++ b/EmailSender.Configuration/appsettings.Staging.json @@ -3,5 +3,6 @@ "DbConnect": "", "AZ_Storage_ContainerName": "", "AZ_Storage_ConnectionString": "", - "Mailcow_API_Key": "" + "Mailcow_API_Key": "", + "Mailcow_Status_Url": "" } \ No newline at end of file From 6374b7d2ab69a5e086ba05c3b36262f698b019a2 Mon Sep 17 00:00:00 2001 From: Tomasz 'Tom' Kandula Date: Sun, 11 Feb 2024 13:52:08 +0100 Subject: [PATCH 10/18] feat: add models --- .../Mailcow/Models/MailcowStatus.cs | 63 +++++++++++++++++++ .../Mailcow/Models/Status.cs | 10 --- .../Mailcow/Models/StatusItem.cs | 21 +++++++ .../Mailcow/Models/StatusTypes.cs | 9 +++ 4 files changed, 93 insertions(+), 10 deletions(-) create mode 100644 EmailSender.Backend/EmailSender.Backend.Application/Mailcow/Models/MailcowStatus.cs delete mode 100644 EmailSender.Backend/EmailSender.Backend.Application/Mailcow/Models/Status.cs create mode 100644 EmailSender.Backend/EmailSender.Backend.Application/Mailcow/Models/StatusItem.cs create mode 100644 EmailSender.Backend/EmailSender.Backend.Application/Mailcow/Models/StatusTypes.cs diff --git a/EmailSender.Backend/EmailSender.Backend.Application/Mailcow/Models/MailcowStatus.cs b/EmailSender.Backend/EmailSender.Backend.Application/Mailcow/Models/MailcowStatus.cs new file mode 100644 index 0000000..48d1e65 --- /dev/null +++ b/EmailSender.Backend/EmailSender.Backend.Application/Mailcow/Models/MailcowStatus.cs @@ -0,0 +1,63 @@ +using System.Text.Json.Serialization; + +namespace EmailSender.Backend.Application.Mailcow.Models; + +public class MailcowStatus +{ + [JsonPropertyName("ipv6nat-mailcow")] + public StatusItem? Ipv6Nat { get; set; } + + [JsonPropertyName("watchdog-mailcow")] + public StatusItem? Watchdog { get; set; } + + [JsonPropertyName("acme-mailcow")] + public StatusItem? Acme { get; set; } + + [JsonPropertyName("ofelia-mailcow")] + public StatusItem? Ofelia { get; set; } + + [JsonPropertyName("rspamd-mailcow")] + public StatusItem? Rspamd { get; set; } + + [JsonPropertyName("nginx-mailcow")] + public StatusItem? Nginx { get; set; } + + [JsonPropertyName("postfix-mailcow")] + public StatusItem? Postfix { get; set; } + + [JsonPropertyName("dovecot-mailcow")] + public StatusItem? DoveCot { get; set; } + + [JsonPropertyName("php-fpm-mailcow")] + public StatusItem? PhpFpm { get; set; } + + [JsonPropertyName("mysql-mailcow")] + public StatusItem? MySql { get; set; } + + [JsonPropertyName("redis-mailcow")] + public StatusItem? Redis { get; set; } + + [JsonPropertyName("solr-mailcow")] + public StatusItem? Solr { get; set; } + + [JsonPropertyName("clamd-mailcow")] + public StatusItem? Clamd { get; set; } + + [JsonPropertyName("dockerapi-mailcow")] + public StatusItem? DockerApi { get; set; } + + [JsonPropertyName("memcached-mailcow")] + public StatusItem? MemCached { get; set; } + + [JsonPropertyName("sogo-mailcow")] + public StatusItem? SoGo { get; set; } + + [JsonPropertyName("unbound-mailcow")] + public StatusItem? Unbound { get; set; } + + [JsonPropertyName("netfilter-mailcow")] + public StatusItem? NetFilter { get; set; } + + [JsonPropertyName("olefy-mailcow")] + public StatusItem? Olefy { get; set; } +} \ No newline at end of file diff --git a/EmailSender.Backend/EmailSender.Backend.Application/Mailcow/Models/Status.cs b/EmailSender.Backend/EmailSender.Backend.Application/Mailcow/Models/Status.cs deleted file mode 100644 index 5e74d13..0000000 --- a/EmailSender.Backend/EmailSender.Backend.Application/Mailcow/Models/Status.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace EmailSender.Backend.Application.Mailcow.Models; - -public class Status -{ - public string? Container { get; set; } - - public string? State { get; set; } - - public bool? StartedAt { get; set; } -} \ No newline at end of file diff --git a/EmailSender.Backend/EmailSender.Backend.Application/Mailcow/Models/StatusItem.cs b/EmailSender.Backend/EmailSender.Backend.Application/Mailcow/Models/StatusItem.cs new file mode 100644 index 0000000..e78ebca --- /dev/null +++ b/EmailSender.Backend/EmailSender.Backend.Application/Mailcow/Models/StatusItem.cs @@ -0,0 +1,21 @@ +using System.Text.Json.Serialization; + +namespace EmailSender.Backend.Application.Mailcow.Models; + +public class StatusItem +{ + [JsonPropertyName("type")] + public string? Type { get; set; } + + [JsonPropertyName("container")] + public string? Container { get; set; } + + [JsonPropertyName("state")] + public string? State { get; set; } + + [JsonPropertyName("started_at")] + public DateTime? StartedAt { get; set; } + + [JsonPropertyName("image")] + public string? Image { get; set; } +} \ No newline at end of file diff --git a/EmailSender.Backend/EmailSender.Backend.Application/Mailcow/Models/StatusTypes.cs b/EmailSender.Backend/EmailSender.Backend.Application/Mailcow/Models/StatusTypes.cs new file mode 100644 index 0000000..871eaf6 --- /dev/null +++ b/EmailSender.Backend/EmailSender.Backend.Application/Mailcow/Models/StatusTypes.cs @@ -0,0 +1,9 @@ +namespace EmailSender.Backend.Application.Mailcow.Models; + +public enum StatusTypes +{ + Unknown, + Healthy, + Degraded, + Unhealthy +} \ No newline at end of file From 5c76c75191ff188f6d6c051395a2510ad5c78b52 Mon Sep 17 00:00:00 2001 From: Tomasz 'Tom' Kandula Date: Sun, 11 Feb 2024 13:52:26 +0100 Subject: [PATCH 11/18] feat: implement health check for mailcow --- .../EmailSender.Backend.Application.csproj | 1 + .../Mailcow/GetMailcowStatusQueryHandler.cs | 92 ++++++++++++++++++- .../Mailcow/GetMailcowStatusQueryResult.cs | 4 +- 3 files changed, 94 insertions(+), 3 deletions(-) diff --git a/EmailSender.Backend/EmailSender.Backend.Application/EmailSender.Backend.Application.csproj b/EmailSender.Backend/EmailSender.Backend.Application/EmailSender.Backend.Application.csproj index 3bc5140..8891a5c 100644 --- a/EmailSender.Backend/EmailSender.Backend.Application/EmailSender.Backend.Application.csproj +++ b/EmailSender.Backend/EmailSender.Backend.Application/EmailSender.Backend.Application.csproj @@ -9,6 +9,7 @@ + diff --git a/EmailSender.Backend/EmailSender.Backend.Application/Mailcow/GetMailcowStatusQueryHandler.cs b/EmailSender.Backend/EmailSender.Backend.Application/Mailcow/GetMailcowStatusQueryHandler.cs index de8649f..e254d32 100644 --- a/EmailSender.Backend/EmailSender.Backend.Application/Mailcow/GetMailcowStatusQueryHandler.cs +++ b/EmailSender.Backend/EmailSender.Backend.Application/Mailcow/GetMailcowStatusQueryHandler.cs @@ -1,13 +1,103 @@ +using System.Reflection; +using EmailSender.Backend.Application.Mailcow.Models; +using EmailSender.Backend.Core.Utilities.LoggerService; +using EmailSender.Services.HttpClientService.Abstractions; +using EmailSender.Services.HttpClientService.Models; +using Microsoft.Extensions.Configuration; + namespace EmailSender.Backend.Application.Mailcow; public class GetMailcowStatusQueryHandler : RequestHandler { + private readonly IHttpClientServiceFactory _httpClientServiceFactory; + + private readonly ILoggerService _loggerService; + + private readonly IConfiguration _configuration; + + private const BindingFlags Flags = BindingFlags.IgnoreCase | BindingFlags.Instance | BindingFlags.Public | BindingFlags.SetField; + + public GetMailcowStatusQueryHandler(IHttpClientServiceFactory httpClientServiceFactory, ILoggerService loggerService, IConfiguration configuration) + { + _httpClientServiceFactory = httpClientServiceFactory; + _loggerService = loggerService; + _configuration = configuration; + } + public override async Task Handle(GetMailcowStatusQuery request, CancellationToken cancellationToken) { + var headers = new Dictionary + { + ["X-API-Key"] = _configuration.GetValue("Mailcow_API_Key") + }; + + var configuration = new Configuration + { + Url = _configuration.GetValue("Mailcow_Status_Url"), + Method = "GET", + Headers = headers + }; + + var client = _httpClientServiceFactory.Create(false, _loggerService); + var result = await client.Execute(configuration, cancellationToken); + + var healthyCount = 0; + var unhealthyCount = 0; + var data = new List(); + + var resultType = result.GetType(); + var resultProps = resultType.GetProperties(Flags); + + foreach (var prop in resultProps) + { + var item = prop.GetValue(result, null); + if (item is null) + continue; + + var statusItem = item as StatusItem; + data.Add(statusItem!); + + if (statusItem?.State != "running") + { + healthyCount += 1; + } + else + { + unhealthyCount += 1; + } + } + if (healthyCount == data.Count) + { + return new GetMailcowStatusQueryResult + { + Status = StatusTypes.Healthy, + Results = data + }; + } + if (healthyCount != unhealthyCount) + { + return new GetMailcowStatusQueryResult + { + Status = StatusTypes.Degraded, + Results = data + }; + } + if (healthyCount == 0) + { + return new GetMailcowStatusQueryResult + { + Status = StatusTypes.Unhealthy, + Results = data + }; + } - return new GetMailcowStatusQueryResult(); + return new GetMailcowStatusQueryResult + { + Status = StatusTypes.Unknown, + Results = data + }; } } \ No newline at end of file diff --git a/EmailSender.Backend/EmailSender.Backend.Application/Mailcow/GetMailcowStatusQueryResult.cs b/EmailSender.Backend/EmailSender.Backend.Application/Mailcow/GetMailcowStatusQueryResult.cs index 26f150c..d10ef28 100644 --- a/EmailSender.Backend/EmailSender.Backend.Application/Mailcow/GetMailcowStatusQueryResult.cs +++ b/EmailSender.Backend/EmailSender.Backend.Application/Mailcow/GetMailcowStatusQueryResult.cs @@ -4,7 +4,7 @@ namespace EmailSender.Backend.Application.Mailcow; public class GetMailcowStatusQueryResult { - public bool IsRunning { get; set; } + public StatusTypes Status { get; set; } - public IList? Results { get; set; } + public IList? Results { get; set; } } \ No newline at end of file From d751bd2c6f06ff64da04856bcde11388c4fe973c Mon Sep 17 00:00:00 2001 From: Tomasz 'Tom' Kandula Date: Sun, 11 Feb 2024 18:49:38 +0100 Subject: [PATCH 12/18] feat: update models --- .../Mailcow/Models/MailcowStatus.cs | 40 +++++++++---------- .../Mailcow/Models/StatusItem.cs | 12 +++--- 2 files changed, 26 insertions(+), 26 deletions(-) diff --git a/EmailSender.Backend/EmailSender.Backend.Application/Mailcow/Models/MailcowStatus.cs b/EmailSender.Backend/EmailSender.Backend.Application/Mailcow/Models/MailcowStatus.cs index 48d1e65..da1ed5f 100644 --- a/EmailSender.Backend/EmailSender.Backend.Application/Mailcow/Models/MailcowStatus.cs +++ b/EmailSender.Backend/EmailSender.Backend.Application/Mailcow/Models/MailcowStatus.cs @@ -1,63 +1,63 @@ -using System.Text.Json.Serialization; +using Newtonsoft.Json; namespace EmailSender.Backend.Application.Mailcow.Models; public class MailcowStatus { - [JsonPropertyName("ipv6nat-mailcow")] + [JsonProperty("ipv6nat-mailcow")] public StatusItem? Ipv6Nat { get; set; } - [JsonPropertyName("watchdog-mailcow")] + [JsonProperty("watchdog-mailcow")] public StatusItem? Watchdog { get; set; } - [JsonPropertyName("acme-mailcow")] + [JsonProperty("acme-mailcow")] public StatusItem? Acme { get; set; } - [JsonPropertyName("ofelia-mailcow")] + [JsonProperty("ofelia-mailcow")] public StatusItem? Ofelia { get; set; } - [JsonPropertyName("rspamd-mailcow")] + [JsonProperty("rspamd-mailcow")] public StatusItem? Rspamd { get; set; } - [JsonPropertyName("nginx-mailcow")] + [JsonProperty("nginx-mailcow")] public StatusItem? Nginx { get; set; } - [JsonPropertyName("postfix-mailcow")] + [JsonProperty("postfix-mailcow")] public StatusItem? Postfix { get; set; } - [JsonPropertyName("dovecot-mailcow")] + [JsonProperty("dovecot-mailcow")] public StatusItem? DoveCot { get; set; } - [JsonPropertyName("php-fpm-mailcow")] + [JsonProperty("php-fpm-mailcow")] public StatusItem? PhpFpm { get; set; } - [JsonPropertyName("mysql-mailcow")] + [JsonProperty("mysql-mailcow")] public StatusItem? MySql { get; set; } - [JsonPropertyName("redis-mailcow")] + [JsonProperty("redis-mailcow")] public StatusItem? Redis { get; set; } - [JsonPropertyName("solr-mailcow")] + [JsonProperty("solr-mailcow")] public StatusItem? Solr { get; set; } - [JsonPropertyName("clamd-mailcow")] + [JsonProperty("clamd-mailcow")] public StatusItem? Clamd { get; set; } - [JsonPropertyName("dockerapi-mailcow")] + [JsonProperty("dockerapi-mailcow")] public StatusItem? DockerApi { get; set; } - [JsonPropertyName("memcached-mailcow")] + [JsonProperty("memcached-mailcow")] public StatusItem? MemCached { get; set; } - [JsonPropertyName("sogo-mailcow")] + [JsonProperty("sogo-mailcow")] public StatusItem? SoGo { get; set; } - [JsonPropertyName("unbound-mailcow")] + [JsonProperty("unbound-mailcow")] public StatusItem? Unbound { get; set; } - [JsonPropertyName("netfilter-mailcow")] + [JsonProperty("netfilter-mailcow")] public StatusItem? NetFilter { get; set; } - [JsonPropertyName("olefy-mailcow")] + [JsonProperty("olefy-mailcow")] public StatusItem? Olefy { get; set; } } \ No newline at end of file diff --git a/EmailSender.Backend/EmailSender.Backend.Application/Mailcow/Models/StatusItem.cs b/EmailSender.Backend/EmailSender.Backend.Application/Mailcow/Models/StatusItem.cs index e78ebca..bb9b79d 100644 --- a/EmailSender.Backend/EmailSender.Backend.Application/Mailcow/Models/StatusItem.cs +++ b/EmailSender.Backend/EmailSender.Backend.Application/Mailcow/Models/StatusItem.cs @@ -1,21 +1,21 @@ -using System.Text.Json.Serialization; +using Newtonsoft.Json; namespace EmailSender.Backend.Application.Mailcow.Models; public class StatusItem { - [JsonPropertyName("type")] + [JsonProperty("type")] public string? Type { get; set; } - [JsonPropertyName("container")] + [JsonProperty("container")] public string? Container { get; set; } - [JsonPropertyName("state")] + [JsonProperty("state")] public string? State { get; set; } - [JsonPropertyName("started_at")] + [JsonProperty("started_at")] public DateTime? StartedAt { get; set; } - [JsonPropertyName("image")] + [JsonProperty("image")] public string? Image { get; set; } } \ No newline at end of file From 618ad68989d1f162c1f5b402def2d802c697c539 Mon Sep 17 00:00:00 2001 From: Tomasz 'Tom' Kandula Date: Sun, 11 Feb 2024 18:50:02 +0100 Subject: [PATCH 13/18] fix: check if equal --- .../Mailcow/GetMailcowStatusQueryHandler.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/EmailSender.Backend/EmailSender.Backend.Application/Mailcow/GetMailcowStatusQueryHandler.cs b/EmailSender.Backend/EmailSender.Backend.Application/Mailcow/GetMailcowStatusQueryHandler.cs index e254d32..e85851e 100644 --- a/EmailSender.Backend/EmailSender.Backend.Application/Mailcow/GetMailcowStatusQueryHandler.cs +++ b/EmailSender.Backend/EmailSender.Backend.Application/Mailcow/GetMailcowStatusQueryHandler.cs @@ -57,7 +57,7 @@ public override async Task Handle(GetMailcowStatusQ var statusItem = item as StatusItem; data.Add(statusItem!); - if (statusItem?.State != "running") + if (statusItem?.State == "running") { healthyCount += 1; } From a97cecfa62751e30a1040ff76d573e881048fe94 Mon Sep 17 00:00:00 2001 From: Tomasz 'Tom' Kandula Date: Sun, 11 Feb 2024 18:50:18 +0100 Subject: [PATCH 14/18] feat: make endpoint publicly available --- EmailSender.WebApi/Controllers/MailcowController.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/EmailSender.WebApi/Controllers/MailcowController.cs b/EmailSender.WebApi/Controllers/MailcowController.cs index 3ff293b..50c0a8d 100644 --- a/EmailSender.WebApi/Controllers/MailcowController.cs +++ b/EmailSender.WebApi/Controllers/MailcowController.cs @@ -1,5 +1,6 @@ using EmailSender.Backend.Application.Mailcow; using MediatR; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; namespace EmailSender.WebApi.Controllers; @@ -10,6 +11,7 @@ public class MailcowController : BaseController public MailcowController(IMediator mediator) : base(mediator) { } [HttpGet] + [AllowAnonymous] [ProducesResponseType(typeof(GetMailcowStatusQueryResult), StatusCodes.Status200OK)] public async Task GetStatus() => await Mediator.Send(new GetMailcowStatusQuery()); From f7beef4e2b480ef6a5ed8cac98a59e0dc65ac444 Mon Sep 17 00:00:00 2001 From: Tomasz 'Tom' Kandula Date: Sun, 11 Feb 2024 18:50:33 +0100 Subject: [PATCH 15/18] fix: add missing service --- dockerfile | 1 + 1 file changed, 1 insertion(+) diff --git a/dockerfile b/dockerfile index 29c3a2b..b3f030a 100644 --- a/dockerfile +++ b/dockerfile @@ -30,6 +30,7 @@ COPY --from=PROJECTS "/app/EmailSender.Persistence/EmailSender.Persistence.Datab # SERVICES COPY --from=PROJECTS "/app/EmailSender.Services/EmailSender.Services.BehaviourService/bin/Release/net6.0" . +COPY --from=PROJECTS "/app/EmailSender.Services/EmailSender.Services.HttpClientService/bin/Release/net6.0" . COPY --from=PROJECTS "/app/EmailSender.Services/EmailSender.Services.SenderService/bin/Release/net6.0" . COPY --from=PROJECTS "/app/EmailSender.Services/EmailSender.Services.SmtpService/bin/Release/net6.0" . COPY --from=PROJECTS "/app/EmailSender.Services/EmailSender.Services.UserService/bin/Release/net6.0" . From 366ad1264ee677c4fda91a54f0239938a7cbc5cd Mon Sep 17 00:00:00 2001 From: Tomasz 'Tom' Kandula Date: Sun, 11 Feb 2024 19:21:46 +0100 Subject: [PATCH 16/18] feat: update docker file --- dockerfile | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/dockerfile b/dockerfile index b3f030a..4325004 100644 --- a/dockerfile +++ b/dockerfile @@ -39,6 +39,10 @@ COPY --from=PROJECTS "/app/EmailSender.Services/EmailSender.Services.UserService COPY --from=PROJECTS "/app/EmailSender.WebApi/bin/Release/net6.0" . COPY --from=PROJECTS "/app/EmailSender.WebApi.Dto/bin/Release/net6.0" . +# CONFIGURATION +ARG ENV_VALUE +ENV ASPNETCORE_ENVIRONMENT=${ENV_VALUE} ENV ASPNETCORE_URLS=http://+:80 + EXPOSE 80 ENTRYPOINT ["dotnet", "EmailSender.WebApi.dll"] From 36ea20cc8ceb67498fe77a3e1c45d948c5062244 Mon Sep 17 00:00:00 2001 From: Tomasz 'Tom' Kandula Date: Sun, 11 Feb 2024 19:21:55 +0100 Subject: [PATCH 17/18] feat: add env to docker build --- .github/workflows/master_build_test_publish.yml | 1 + .github/workflows/stage_build_test_publish.yml | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/master_build_test_publish.yml b/.github/workflows/master_build_test_publish.yml index dd3fd35..a4c44a1 100644 --- a/.github/workflows/master_build_test_publish.yml +++ b/.github/workflows/master_build_test_publish.yml @@ -73,6 +73,7 @@ jobs: - name: Production | Build Docker (execute with tests) run: | docker build . \ + --build-arg "ENV_VALUE=Production" \ -t ${{ secrets.DOCKER_REGISTRY_SERVER_URL }}/${{ secrets.DOCKER_REGISTRY_SERVER_USERNAME }}:backend-production-${{ github.sha }} - name: Production | Push Docker image (prepare) diff --git a/.github/workflows/stage_build_test_publish.yml b/.github/workflows/stage_build_test_publish.yml index 7480fe6..bd32c2b 100644 --- a/.github/workflows/stage_build_test_publish.yml +++ b/.github/workflows/stage_build_test_publish.yml @@ -34,7 +34,10 @@ jobs: password: ${{ secrets.DOCKER_REGISTRY_SERVER_PASSWORD }} - name: Staging | Build Docker (execute with tests) - run: docker build . -t ${{ secrets.DOCKER_REGISTRY_SERVER_URL }}/${{ secrets.DOCKER_REGISTRY_SERVER_USERNAME }}:backend-staging-${{ github.sha }} + run: | + docker build . \ + --build-arg "ENV_VALUE=Staging" \ + -t ${{ secrets.DOCKER_REGISTRY_SERVER_URL }}/${{ secrets.DOCKER_REGISTRY_SERVER_USERNAME }}:backend-staging-${{ github.sha }} - name: Staging | Push Docker image (prepare) uses: azure/docker-login@v1.0.1 From 1c1bc8c0cfde0a931b832a461b023a0d186e5b83 Mon Sep 17 00:00:00 2001 From: Tomasz 'Tom' Kandula Date: Sun, 11 Feb 2024 19:22:06 +0100 Subject: [PATCH 18/18] feat: update test file --- __testrun.sh | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/__testrun.sh b/__testrun.sh index 594d325..69f9038 100644 --- a/__testrun.sh +++ b/__testrun.sh @@ -1,3 +1,5 @@ -APP_NAME="emailsender-webapi" -docker build . -t "$APP_NAME" -docker run --rm -it -p 5008:80 "$APP_NAME" \ No newline at end of file +ENV_VALUE="Development" +APP_NAME="emailsender-test" + +docker build . --build-arg "ENV_VALUE=$ENV_VALUE" -t "$APP_NAME" +docker run --rm -it -p 7008:80 "$APP_NAME"