From cff588be6bc44675a4471d4bdf75bcde603f496f Mon Sep 17 00:00:00 2001 From: Thomas Staub <thomas.staub@gibb.ch> Date: Tue, 12 Sep 2023 10:01:49 +0200 Subject: [PATCH] Update Nuget 1.0.16 --- .../Play.Catalog.Service.csproj | 2 +- Play.Common/README.md | 12 ++- .../Play.Common/Configuration/Extensions.cs | 29 ++++++ .../Play.Common/HealthChecks/Extensions.cs | 53 +++++++++++ .../HealthChecks/MongoDbHealthCheck.cs | 33 +++++++ .../src/Play.Common/Identity/Extensions.cs | 8 +- .../src/Play.Common/MassTransit/Extensions.cs | 79 ++++++++++++++++ .../src/Play.Common/Play.Common.csproj | 5 +- .../Play.Common/Settings/MongoDbSettings.cs | 10 ++- .../Settings/ServiceBusSettings.cs | 7 ++ .../Play.Common/Settings/ServiceSettings.cs | 2 + Play.Common1/.gitignore | 2 + Play.Common1/.vscode/tasks.json | 46 ++++++++++ Play.Common1/README.md | 16 ++++ .../Play.Common/DesignTimeBuild/.dtbcache.v2 | Bin ...55a3eb04-01a5-4be6-9993-d1a5d9968f14.vsidx | Bin .../Play.Common/FileContentIndex/read.lock | 0 .../.vs/Play.Common/v17/.futdcache.v2 | Bin .../src/Play.Common/.vs/Play.Common/v17/.suo | Bin .../Play.Common/HealthChecks/Extensions.cs | 53 +++++++++++ .../HealthChecks/MongoDbHealthCheck.cs | 33 +++++++ Play.Common1/src/Play.Common/IEntity.cs | 9 ++ Play.Common1/src/Play.Common/IRepository.cs | 18 ++++ .../Identity/ConfigureJwtBearerOptions.cs | 62 +++++++++++++ .../src/Play.Common/Identity/Extensions.cs | 16 ++++ .../src/Play.Common/MassTransit/Extensions.cs | 52 +++++++++++ .../src/Play.Common/MongoDB/Extensions.cs | 42 +++++++++ .../Play.Common/MongoDB/MongoRepository.cs | 68 ++++++++++++++ .../src/Play.Common/Play.Common.csproj | 20 +++++ .../src/Play.Common/Play.Common.sln | 0 .../Play.Common/Settings/MongoDbSettings.cs | 11 +++ .../Play.Common/Settings/RabbitMQSettings.cs | 7 ++ .../Settings/ServiceBusSettings.cs | 7 ++ .../Play.Common/Settings/ServiceSettings.cs | 8 ++ .../src/Play.Common/nuget.config | 0 Play.Identity/kubernetes/identity.yaml | 84 ++++++++++++++++++ Play.Identity/kubernetes/singning-cer.yaml | 11 +++ .../Play.Identity.Service.csproj | 2 +- .../src/Play.Identity.Service/Program.cs | 5 ++ Play.Infra/cert-manager/acme-challenge.yaml | 21 +++++ Play.Infra/cert-manager/cluster-issuer.yaml | 14 +++ Play.Infra/emissary-ingress/host.yaml | 10 +++ Play.Infra/emissary-ingress/listener.yaml | 24 +++++ Play.Infra/emissary-ingress/mappings.yaml | 50 +++++++++++ .../emissary-ingress/tls-certificate.yaml | 11 +++ .../Play.Inventory.Service.csproj | 2 +- .../Play.Trading.Service.csproj | 2 +- README.md | 15 +++- packages/Play.Common.1.0.15.nupkg | Bin 0 -> 14224 bytes packages/Play.Common.1.0.16.nupkg | Bin 0 -> 14224 bytes 50 files changed, 949 insertions(+), 12 deletions(-) create mode 100644 Play.Common/src/Play.Common/Configuration/Extensions.cs create mode 100644 Play.Common/src/Play.Common/HealthChecks/Extensions.cs create mode 100644 Play.Common/src/Play.Common/HealthChecks/MongoDbHealthCheck.cs create mode 100644 Play.Common/src/Play.Common/Settings/ServiceBusSettings.cs create mode 100644 Play.Common1/.gitignore create mode 100644 Play.Common1/.vscode/tasks.json create mode 100644 Play.Common1/README.md rename {Play.Common => Play.Common1}/src/Play.Common/.vs/Play.Common/DesignTimeBuild/.dtbcache.v2 (100%) rename {Play.Common => Play.Common1}/src/Play.Common/.vs/Play.Common/FileContentIndex/55a3eb04-01a5-4be6-9993-d1a5d9968f14.vsidx (100%) rename {Play.Common => Play.Common1}/src/Play.Common/.vs/Play.Common/FileContentIndex/read.lock (100%) rename {Play.Common => Play.Common1}/src/Play.Common/.vs/Play.Common/v17/.futdcache.v2 (100%) rename {Play.Common => Play.Common1}/src/Play.Common/.vs/Play.Common/v17/.suo (100%) create mode 100644 Play.Common1/src/Play.Common/HealthChecks/Extensions.cs create mode 100644 Play.Common1/src/Play.Common/HealthChecks/MongoDbHealthCheck.cs create mode 100644 Play.Common1/src/Play.Common/IEntity.cs create mode 100644 Play.Common1/src/Play.Common/IRepository.cs create mode 100644 Play.Common1/src/Play.Common/Identity/ConfigureJwtBearerOptions.cs create mode 100644 Play.Common1/src/Play.Common/Identity/Extensions.cs create mode 100644 Play.Common1/src/Play.Common/MassTransit/Extensions.cs create mode 100644 Play.Common1/src/Play.Common/MongoDB/Extensions.cs create mode 100644 Play.Common1/src/Play.Common/MongoDB/MongoRepository.cs create mode 100644 Play.Common1/src/Play.Common/Play.Common.csproj rename {Play.Common => Play.Common1}/src/Play.Common/Play.Common.sln (100%) create mode 100644 Play.Common1/src/Play.Common/Settings/MongoDbSettings.cs create mode 100644 Play.Common1/src/Play.Common/Settings/RabbitMQSettings.cs create mode 100644 Play.Common1/src/Play.Common/Settings/ServiceBusSettings.cs create mode 100644 Play.Common1/src/Play.Common/Settings/ServiceSettings.cs rename {Play.Common => Play.Common1}/src/Play.Common/nuget.config (100%) create mode 100644 Play.Identity/kubernetes/identity.yaml create mode 100644 Play.Identity/kubernetes/singning-cer.yaml create mode 100644 Play.Infra/cert-manager/acme-challenge.yaml create mode 100644 Play.Infra/cert-manager/cluster-issuer.yaml create mode 100644 Play.Infra/emissary-ingress/host.yaml create mode 100644 Play.Infra/emissary-ingress/listener.yaml create mode 100644 Play.Infra/emissary-ingress/mappings.yaml create mode 100644 Play.Infra/emissary-ingress/tls-certificate.yaml create mode 100644 packages/Play.Common.1.0.15.nupkg create mode 100644 packages/Play.Common.1.0.16.nupkg diff --git a/Play.Catalog/src/Play.Catalog.Service/Play.Catalog.Service.csproj b/Play.Catalog/src/Play.Catalog.Service/Play.Catalog.Service.csproj index 484f3df..0e5659a 100644 --- a/Play.Catalog/src/Play.Catalog.Service/Play.Catalog.Service.csproj +++ b/Play.Catalog/src/Play.Catalog.Service/Play.Catalog.Service.csproj @@ -5,7 +5,7 @@ </PropertyGroup> <ItemGroup> - <PackageReference Include="Play.Common" Version="1.0.13" /> + <PackageReference Include="Play.Common" Version="1.0.16" /> <PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0" /> </ItemGroup> diff --git a/Play.Common/README.md b/Play.Common/README.md index 5da64cc..536bd68 100644 --- a/Play.Common/README.md +++ b/Play.Common/README.md @@ -1,9 +1,15 @@ -dotnet pack src\Play.Common\ --configuration Release -p:PackageVersion=1.0.13 -o ..\packages +From Root directory -Wechseln ins Verzeichniss play.Common +dotnet pack .\Play.Common\src\Play.Common\ --configuration Release -p:PackageVersion=1.0.14 -o .\packages -dotnet nuget push Play.Common.1.0.13.nupkg --source https://git.gibb.ch/api/v4/projects/5940/packages/nuget/index.json --api-key glpat-xr4z39bn9pBiWQcygXJn +Wechseln ins Verzeichniss packages + +dotnet nuget push .\packages\Play.Common.1.0.15.nupkg --source https://git.gibb.ch/api/v4/projects/5940/packages/nuget/index.json --api-key glpat-8GxsWezmB9YrpVnGygyg + + +dotnet nuget push .\packages\Play.Catalog.Contracts.1.0.1.nupkg --source https://git.gibb.ch/api/v4/projects/5940/packages/nuget/index.json --api-key glpat-8GxsWezmB9YrpVnGygyg + diff --git a/Play.Common/src/Play.Common/Configuration/Extensions.cs b/Play.Common/src/Play.Common/Configuration/Extensions.cs new file mode 100644 index 0000000..94994f9 --- /dev/null +++ b/Play.Common/src/Play.Common/Configuration/Extensions.cs @@ -0,0 +1,29 @@ +using System; +using Azure.Identity; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; +using Play.Common.Settings; + +namespace Play.Common.Configuration +{ + public static class Extensions + { + public static IHostBuilder ConfigureAzureKeyVault(this IHostBuilder builder) + { + return builder.ConfigureAppConfiguration((context, configurationBuilder) => + { + if (context.HostingEnvironment.IsProduction()) + { + var configuration = configurationBuilder.Build(); + var serviceSettings = configuration.GetSection(nameof(ServiceSettings)) + .Get<ServiceSettings>(); + + configurationBuilder.AddAzureKeyVault( + new Uri($"https://{serviceSettings.KeyVaultName}.vault.azure.net/"), + new DefaultAzureCredential() + ); + } + }); + } + } +} \ No newline at end of file diff --git a/Play.Common/src/Play.Common/HealthChecks/Extensions.cs b/Play.Common/src/Play.Common/HealthChecks/Extensions.cs new file mode 100644 index 0000000..70be04c --- /dev/null +++ b/Play.Common/src/Play.Common/HealthChecks/Extensions.cs @@ -0,0 +1,53 @@ +using System; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Diagnostics.HealthChecks; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Diagnostics.HealthChecks; +using MongoDB.Driver; +using Play.Common.Settings; + +namespace Play.Common.HealthChecks +{ + public static class Extensions + { + private const string MongoCheckName = "mongodb"; + private const string ReadyTagName = "ready"; + private const string LiveTagName = "live"; + private const string HealthEndpoint = "health"; + private const int DefaultSeconds = 3; + + public static IHealthChecksBuilder AddMongoDb( + this IHealthChecksBuilder builder, + TimeSpan? timeout = default) + { + return builder.Add(new HealthCheckRegistration( + MongoCheckName, + serviceProvider => + { + var configuration = serviceProvider.GetService<IConfiguration>(); + var mongoDbSettings = configuration.GetSection(nameof(MongoDbSettings)) + .Get<MongoDbSettings>(); + var mongoClient = new MongoClient(mongoDbSettings.ConnectionString); + return new MongoDbHealthCheck(mongoClient); + }, + HealthStatus.Unhealthy, + new[] { ReadyTagName }, + TimeSpan.FromSeconds(DefaultSeconds) + )); + } + + public static void MapPlayEconomyHealthChecks(this IEndpointRouteBuilder endpoints) + { + endpoints.MapHealthChecks($"/{HealthEndpoint}/{ReadyTagName}", new HealthCheckOptions() + { + Predicate = (check) => check.Tags.Contains(ReadyTagName) + }); + endpoints.MapHealthChecks($"/{HealthEndpoint}/{LiveTagName}", new HealthCheckOptions() + { + Predicate = (check) => false + }); + } + } +} \ No newline at end of file diff --git a/Play.Common/src/Play.Common/HealthChecks/MongoDbHealthCheck.cs b/Play.Common/src/Play.Common/HealthChecks/MongoDbHealthCheck.cs new file mode 100644 index 0000000..481f5af --- /dev/null +++ b/Play.Common/src/Play.Common/HealthChecks/MongoDbHealthCheck.cs @@ -0,0 +1,33 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Diagnostics.HealthChecks; +using MongoDB.Driver; + +namespace Play.Common.HealthChecks +{ + public class MongoDbHealthCheck : IHealthCheck + { + private readonly MongoClient client; + + public MongoDbHealthCheck(MongoClient client) + { + this.client = client; + } + + public async Task<HealthCheckResult> CheckHealthAsync( + HealthCheckContext context, + CancellationToken cancellationToken = default) + { + try + { + await client.ListDatabaseNamesAsync(cancellationToken); + return HealthCheckResult.Healthy(); + } + catch (Exception ex) + { + return HealthCheckResult.Unhealthy(exception: ex); + } + } + } +} \ No newline at end of file diff --git a/Play.Common/src/Play.Common/Identity/Extensions.cs b/Play.Common/src/Play.Common/Identity/Extensions.cs index 86e9936..e43c56c 100644 --- a/Play.Common/src/Play.Common/Identity/Extensions.cs +++ b/Play.Common/src/Play.Common/Identity/Extensions.cs @@ -10,7 +10,11 @@ namespace Play.Common.Identity { return services.ConfigureOptions<ConfigureJwtBearerOptions>() .AddAuthentication(JwtBearerDefaults.AuthenticationScheme) - .AddJwtBearer(); + .AddJwtBearer(options => + { + + options.RequireHttpsMetadata = false; + }); } } -} \ No newline at end of file +} diff --git a/Play.Common/src/Play.Common/MassTransit/Extensions.cs b/Play.Common/src/Play.Common/MassTransit/Extensions.cs index 33629b9..53b3b94 100644 --- a/Play.Common/src/Play.Common/MassTransit/Extensions.cs +++ b/Play.Common/src/Play.Common/MassTransit/Extensions.cs @@ -13,6 +13,30 @@ namespace Play.Common.MassTransit { public static class Extensions { + private const string RabbitMq = "RABBITMQ"; + private const string ServiceBus = "SERVICEBUS"; + + public static IServiceCollection AddMassTransitWithMessageBroker( + this IServiceCollection services, + IConfiguration config, + Action<IRetryConfigurator> configureRetries = null) + { + var serviceSettings = config.GetSection(nameof(ServiceSettings)).Get<ServiceSettings>(); + + switch (serviceSettings.MessageBroker?.ToUpper()) + { + case ServiceBus: + services.AddMassTransitWithServiceBus(configureRetries); + break; + case RabbitMq: + default: + services.AddMassTransitWithRabbitMq(configureRetries); + break; + } + + return services; + } + public static IServiceCollection AddMassTransitWithRabbitMq( this IServiceCollection services, Action<IRetryConfigurator> configureRetries = null) @@ -28,6 +52,40 @@ namespace Play.Common.MassTransit return services; } + public static IServiceCollection AddMassTransitWithServiceBus( + this IServiceCollection services, + Action<IRetryConfigurator> configureRetries = null) + { + services.AddMassTransit(configure => + { + configure.AddConsumers(Assembly.GetEntryAssembly()); + configure.UsingPlayEconomyAzureServiceBus(configureRetries); + }); + + services.AddMassTransitHostedService(); + + return services; + } + + public static void UsingPlayEconomyMessageBroker( + this IServiceCollectionBusConfigurator configure, + IConfiguration config, + Action<IRetryConfigurator> configureRetries = null) + { + var serviceSettings = config.GetSection(nameof(ServiceSettings)).Get<ServiceSettings>(); + + switch (serviceSettings.MessageBroker?.ToUpper()) + { + case ServiceBus: + configure.UsingPlayEconomyAzureServiceBus(configureRetries); + break; + case RabbitMq: + default: + configure.UsingPlayEconomyRabbitMq(configureRetries); + break; + } + } + public static void UsingPlayEconomyRabbitMq( this IServiceCollectionBusConfigurator configure, Action<IRetryConfigurator> configureRetries = null) @@ -48,5 +106,26 @@ namespace Play.Common.MassTransit configurator.UseMessageRetry(configureRetries); }); } + + public static void UsingPlayEconomyAzureServiceBus( + this IServiceCollectionBusConfigurator configure, + Action<IRetryConfigurator> configureRetries = null) + { + configure.UsingAzureServiceBus((context, configurator) => + { + var configuration = context.GetService<IConfiguration>(); + var serviceSettings = configuration.GetSection(nameof(ServiceSettings)).Get<ServiceSettings>(); + var serviceBusSettings = configuration.GetSection(nameof(ServiceBusSettings)).Get<ServiceBusSettings>(); + configurator.Host(serviceBusSettings.ConnectionString); + configurator.ConfigureEndpoints(context, new KebabCaseEndpointNameFormatter(serviceSettings.ServiceName, false)); + + if (configureRetries == null) + { + configureRetries = (retryConfigurator) => retryConfigurator.Interval(3, TimeSpan.FromSeconds(5)); + } + + configurator.UseMessageRetry(configureRetries); + }); + } } } \ No newline at end of file diff --git a/Play.Common/src/Play.Common/Play.Common.csproj b/Play.Common/src/Play.Common/Play.Common.csproj index 2e9f83a..707d300 100644 --- a/Play.Common/src/Play.Common/Play.Common.csproj +++ b/Play.Common/src/Play.Common/Play.Common.csproj @@ -3,13 +3,16 @@ <PropertyGroup> <TargetFramework>net7.0</TargetFramework> <PackageDescription>Common libraries used by Play Economy services.</PackageDescription> - <Authors>Thomas Staub</Authors> + <Authors>Julio Casal</Authors> <Company>.NET Microservices</Company> </PropertyGroup> <ItemGroup> + <PackageReference Include="Azure.Extensions.AspNetCore.Configuration.Secrets" Version="1.2.2" /> + <PackageReference Include="Azure.Identity" Version="1.8.0" /> <PackageReference Include="MassTransit.AspNetCore" Version="7.3.1" /> <PackageReference Include="MassTransit.RabbitMQ" Version="7.3.1" /> + <PackageReference Include="MassTransit.Azure.ServiceBus.Core" Version="7.3.1" /> <PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="7.0.0" /> <PackageReference Include="Microsoft.Extensions.Configuration" Version="7.0.0" /> <PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="7.0.0" /> diff --git a/Play.Common/src/Play.Common/Settings/MongoDbSettings.cs b/Play.Common/src/Play.Common/Settings/MongoDbSettings.cs index 8a0be7d..0d1bcf3 100644 --- a/Play.Common/src/Play.Common/Settings/MongoDbSettings.cs +++ b/Play.Common/src/Play.Common/Settings/MongoDbSettings.cs @@ -2,10 +2,18 @@ namespace Play.Common.Settings { public class MongoDbSettings { + private string connectionString; + public string Host { get; init; } public int Port { get; init; } - public string ConnectionString => $"mongodb://{Host}:{Port}"; + public string ConnectionString + { + get { return string.IsNullOrWhiteSpace(connectionString) + ? $"mongodb://{Host}:{Port}" : connectionString; } + init { connectionString = value; } + } + } } \ No newline at end of file diff --git a/Play.Common/src/Play.Common/Settings/ServiceBusSettings.cs b/Play.Common/src/Play.Common/Settings/ServiceBusSettings.cs new file mode 100644 index 0000000..065122a --- /dev/null +++ b/Play.Common/src/Play.Common/Settings/ServiceBusSettings.cs @@ -0,0 +1,7 @@ +namespace Play.Common.Settings +{ + public class ServiceBusSettings + { + public string ConnectionString { get; init; } + } +} \ No newline at end of file diff --git a/Play.Common/src/Play.Common/Settings/ServiceSettings.cs b/Play.Common/src/Play.Common/Settings/ServiceSettings.cs index 046b661..1f2f474 100644 --- a/Play.Common/src/Play.Common/Settings/ServiceSettings.cs +++ b/Play.Common/src/Play.Common/Settings/ServiceSettings.cs @@ -4,5 +4,7 @@ namespace Play.Common.Settings { public string ServiceName { get; init; } public string Authority { get; init; } + public string MessageBroker { get; init; } + public string KeyVaultName { get; init; } } } \ No newline at end of file diff --git a/Play.Common1/.gitignore b/Play.Common1/.gitignore new file mode 100644 index 0000000..d45db4d --- /dev/null +++ b/Play.Common1/.gitignore @@ -0,0 +1,2 @@ +[Bb]in/ +[Oo]bj/ \ No newline at end of file diff --git a/Play.Common1/.vscode/tasks.json b/Play.Common1/.vscode/tasks.json new file mode 100644 index 0000000..35f4315 --- /dev/null +++ b/Play.Common1/.vscode/tasks.json @@ -0,0 +1,46 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "build", + "command": "dotnet", + "type": "process", + "args": [ + "build", + "${workspaceFolder}/src/Play.Common/Play.Common.csproj", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "problemMatcher": "$msCompile", + "group": { + "kind": "build", + "isDefault": true + } + }, + { + "label": "publish", + "command": "dotnet", + "type": "process", + "args": [ + "publish", + "${workspaceFolder}/src/Play.Common/Play.Common.csproj", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "problemMatcher": "$msCompile" + }, + { + "label": "watch", + "command": "dotnet", + "type": "process", + "args": [ + "watch", + "run", + "${workspaceFolder}/src/Play.Common/Play.Common.csproj", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "problemMatcher": "$msCompile" + } + ] +} \ No newline at end of file diff --git a/Play.Common1/README.md b/Play.Common1/README.md new file mode 100644 index 0000000..a4c6b88 --- /dev/null +++ b/Play.Common1/README.md @@ -0,0 +1,16 @@ + +From Root directory + +dotnet pack .\Play.Common\src\Play.Common\ --configuration Release -p:PackageVersion=1.0.14 -o .\packages + + + + + +Wechseln ins Verzeichniss packages + +dotnet nuget push .\packages\Play.Common.1.0.15.nupkg --source https://git.gibb.ch/api/v4/projects/5940/packages/nuget/index.json --api-key glpat-8GxsWezmB9YrpVnGygyg + + + + diff --git a/Play.Common/src/Play.Common/.vs/Play.Common/DesignTimeBuild/.dtbcache.v2 b/Play.Common1/src/Play.Common/.vs/Play.Common/DesignTimeBuild/.dtbcache.v2 similarity index 100% rename from Play.Common/src/Play.Common/.vs/Play.Common/DesignTimeBuild/.dtbcache.v2 rename to Play.Common1/src/Play.Common/.vs/Play.Common/DesignTimeBuild/.dtbcache.v2 diff --git a/Play.Common/src/Play.Common/.vs/Play.Common/FileContentIndex/55a3eb04-01a5-4be6-9993-d1a5d9968f14.vsidx b/Play.Common1/src/Play.Common/.vs/Play.Common/FileContentIndex/55a3eb04-01a5-4be6-9993-d1a5d9968f14.vsidx similarity index 100% rename from Play.Common/src/Play.Common/.vs/Play.Common/FileContentIndex/55a3eb04-01a5-4be6-9993-d1a5d9968f14.vsidx rename to Play.Common1/src/Play.Common/.vs/Play.Common/FileContentIndex/55a3eb04-01a5-4be6-9993-d1a5d9968f14.vsidx diff --git a/Play.Common/src/Play.Common/.vs/Play.Common/FileContentIndex/read.lock b/Play.Common1/src/Play.Common/.vs/Play.Common/FileContentIndex/read.lock similarity index 100% rename from Play.Common/src/Play.Common/.vs/Play.Common/FileContentIndex/read.lock rename to Play.Common1/src/Play.Common/.vs/Play.Common/FileContentIndex/read.lock diff --git a/Play.Common/src/Play.Common/.vs/Play.Common/v17/.futdcache.v2 b/Play.Common1/src/Play.Common/.vs/Play.Common/v17/.futdcache.v2 similarity index 100% rename from Play.Common/src/Play.Common/.vs/Play.Common/v17/.futdcache.v2 rename to Play.Common1/src/Play.Common/.vs/Play.Common/v17/.futdcache.v2 diff --git a/Play.Common/src/Play.Common/.vs/Play.Common/v17/.suo b/Play.Common1/src/Play.Common/.vs/Play.Common/v17/.suo similarity index 100% rename from Play.Common/src/Play.Common/.vs/Play.Common/v17/.suo rename to Play.Common1/src/Play.Common/.vs/Play.Common/v17/.suo diff --git a/Play.Common1/src/Play.Common/HealthChecks/Extensions.cs b/Play.Common1/src/Play.Common/HealthChecks/Extensions.cs new file mode 100644 index 0000000..70be04c --- /dev/null +++ b/Play.Common1/src/Play.Common/HealthChecks/Extensions.cs @@ -0,0 +1,53 @@ +using System; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Diagnostics.HealthChecks; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Diagnostics.HealthChecks; +using MongoDB.Driver; +using Play.Common.Settings; + +namespace Play.Common.HealthChecks +{ + public static class Extensions + { + private const string MongoCheckName = "mongodb"; + private const string ReadyTagName = "ready"; + private const string LiveTagName = "live"; + private const string HealthEndpoint = "health"; + private const int DefaultSeconds = 3; + + public static IHealthChecksBuilder AddMongoDb( + this IHealthChecksBuilder builder, + TimeSpan? timeout = default) + { + return builder.Add(new HealthCheckRegistration( + MongoCheckName, + serviceProvider => + { + var configuration = serviceProvider.GetService<IConfiguration>(); + var mongoDbSettings = configuration.GetSection(nameof(MongoDbSettings)) + .Get<MongoDbSettings>(); + var mongoClient = new MongoClient(mongoDbSettings.ConnectionString); + return new MongoDbHealthCheck(mongoClient); + }, + HealthStatus.Unhealthy, + new[] { ReadyTagName }, + TimeSpan.FromSeconds(DefaultSeconds) + )); + } + + public static void MapPlayEconomyHealthChecks(this IEndpointRouteBuilder endpoints) + { + endpoints.MapHealthChecks($"/{HealthEndpoint}/{ReadyTagName}", new HealthCheckOptions() + { + Predicate = (check) => check.Tags.Contains(ReadyTagName) + }); + endpoints.MapHealthChecks($"/{HealthEndpoint}/{LiveTagName}", new HealthCheckOptions() + { + Predicate = (check) => false + }); + } + } +} \ No newline at end of file diff --git a/Play.Common1/src/Play.Common/HealthChecks/MongoDbHealthCheck.cs b/Play.Common1/src/Play.Common/HealthChecks/MongoDbHealthCheck.cs new file mode 100644 index 0000000..481f5af --- /dev/null +++ b/Play.Common1/src/Play.Common/HealthChecks/MongoDbHealthCheck.cs @@ -0,0 +1,33 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Diagnostics.HealthChecks; +using MongoDB.Driver; + +namespace Play.Common.HealthChecks +{ + public class MongoDbHealthCheck : IHealthCheck + { + private readonly MongoClient client; + + public MongoDbHealthCheck(MongoClient client) + { + this.client = client; + } + + public async Task<HealthCheckResult> CheckHealthAsync( + HealthCheckContext context, + CancellationToken cancellationToken = default) + { + try + { + await client.ListDatabaseNamesAsync(cancellationToken); + return HealthCheckResult.Healthy(); + } + catch (Exception ex) + { + return HealthCheckResult.Unhealthy(exception: ex); + } + } + } +} \ No newline at end of file diff --git a/Play.Common1/src/Play.Common/IEntity.cs b/Play.Common1/src/Play.Common/IEntity.cs new file mode 100644 index 0000000..b2d0616 --- /dev/null +++ b/Play.Common1/src/Play.Common/IEntity.cs @@ -0,0 +1,9 @@ +using System; + +namespace Play.Common +{ + public interface IEntity + { + Guid Id { get; set; } + } +} \ No newline at end of file diff --git a/Play.Common1/src/Play.Common/IRepository.cs b/Play.Common1/src/Play.Common/IRepository.cs new file mode 100644 index 0000000..8b33b2b --- /dev/null +++ b/Play.Common1/src/Play.Common/IRepository.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Linq.Expressions; +using System.Threading.Tasks; + +namespace Play.Common +{ + public interface IRepository<T> where T : IEntity + { + Task CreateAsync(T entity); + Task<IReadOnlyCollection<T>> GetAllAsync(); + Task<IReadOnlyCollection<T>> GetAllAsync(Expression<Func<T, bool>> filter); + Task<T> GetAsync(Guid id); + Task<T> GetAsync(Expression<Func<T, bool>> filter); + Task RemoveAsync(Guid id); + Task UpdateAsync(T entity); + } +} \ No newline at end of file diff --git a/Play.Common1/src/Play.Common/Identity/ConfigureJwtBearerOptions.cs b/Play.Common1/src/Play.Common/Identity/ConfigureJwtBearerOptions.cs new file mode 100644 index 0000000..faddf8a --- /dev/null +++ b/Play.Common1/src/Play.Common/Identity/ConfigureJwtBearerOptions.cs @@ -0,0 +1,62 @@ +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Options; +using Microsoft.IdentityModel.Tokens; +using Play.Common.Settings; + +namespace Play.Common.Identity +{ + public class ConfigureJwtBearerOptions : IConfigureNamedOptions<JwtBearerOptions> + { + private const string AccessTokenParameter = "access_token"; + private const string MessageHubPath = "/messageHub"; + + private readonly IConfiguration configuration; + + public ConfigureJwtBearerOptions(IConfiguration configuration) + { + this.configuration = configuration; + } + + public void Configure(string name, JwtBearerOptions options) + { + if (name == JwtBearerDefaults.AuthenticationScheme) + { + var serviceSettings = configuration.GetSection(nameof(ServiceSettings)) + .Get<ServiceSettings>(); + + options.Authority = serviceSettings.Authority; + options.Audience = serviceSettings.ServiceName; + options.MapInboundClaims = false; + options.TokenValidationParameters = new TokenValidationParameters + { + NameClaimType = "name", + RoleClaimType = "role" + }; + + options.Events = new JwtBearerEvents + { + OnMessageReceived = context => + { + var accessToken = context.Request.Query[AccessTokenParameter]; + var path = context.HttpContext.Request.Path; + + if (!string.IsNullOrEmpty(accessToken) && + path.StartsWithSegments(MessageHubPath)) + { + context.Token = accessToken; + } + + return Task.CompletedTask; + } + }; + } + } + + public void Configure(JwtBearerOptions options) + { + Configure(Options.DefaultName, options); + } + } +} \ No newline at end of file diff --git a/Play.Common1/src/Play.Common/Identity/Extensions.cs b/Play.Common1/src/Play.Common/Identity/Extensions.cs new file mode 100644 index 0000000..86e9936 --- /dev/null +++ b/Play.Common1/src/Play.Common/Identity/Extensions.cs @@ -0,0 +1,16 @@ +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.Extensions.DependencyInjection; + +namespace Play.Common.Identity +{ + public static class Extensions + { + public static AuthenticationBuilder AddJwtBearerAuthentication(this IServiceCollection services) + { + return services.ConfigureOptions<ConfigureJwtBearerOptions>() + .AddAuthentication(JwtBearerDefaults.AuthenticationScheme) + .AddJwtBearer(); + } + } +} \ No newline at end of file diff --git a/Play.Common1/src/Play.Common/MassTransit/Extensions.cs b/Play.Common1/src/Play.Common/MassTransit/Extensions.cs new file mode 100644 index 0000000..33629b9 --- /dev/null +++ b/Play.Common1/src/Play.Common/MassTransit/Extensions.cs @@ -0,0 +1,52 @@ +using System; +using System.Reflection; +using GreenPipes; +using GreenPipes.Configurators; +using MassTransit; +using MassTransit.Definition; +using MassTransit.ExtensionsDependencyInjectionIntegration; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Play.Common.Settings; + +namespace Play.Common.MassTransit +{ + public static class Extensions + { + public static IServiceCollection AddMassTransitWithRabbitMq( + this IServiceCollection services, + Action<IRetryConfigurator> configureRetries = null) + { + services.AddMassTransit(configure => + { + configure.AddConsumers(Assembly.GetEntryAssembly()); + configure.UsingPlayEconomyRabbitMq(configureRetries); + }); + + services.AddMassTransitHostedService(); + + return services; + } + + public static void UsingPlayEconomyRabbitMq( + this IServiceCollectionBusConfigurator configure, + Action<IRetryConfigurator> configureRetries = null) + { + configure.UsingRabbitMq((context, configurator) => + { + var configuration = context.GetService<IConfiguration>(); + var serviceSettings = configuration.GetSection(nameof(ServiceSettings)).Get<ServiceSettings>(); + var rabbitMQSettings = configuration.GetSection(nameof(RabbitMQSettings)).Get<RabbitMQSettings>(); + configurator.Host(rabbitMQSettings.Host); + configurator.ConfigureEndpoints(context, new KebabCaseEndpointNameFormatter(serviceSettings.ServiceName, false)); + + if (configureRetries == null) + { + configureRetries = (retryConfigurator) => retryConfigurator.Interval(3, TimeSpan.FromSeconds(5)); + } + + configurator.UseMessageRetry(configureRetries); + }); + } + } +} \ No newline at end of file diff --git a/Play.Common1/src/Play.Common/MongoDB/Extensions.cs b/Play.Common1/src/Play.Common/MongoDB/Extensions.cs new file mode 100644 index 0000000..aa52bf5 --- /dev/null +++ b/Play.Common1/src/Play.Common/MongoDB/Extensions.cs @@ -0,0 +1,42 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using MongoDB.Bson; +using MongoDB.Bson.Serialization; +using MongoDB.Bson.Serialization.Serializers; +using MongoDB.Driver; +using Play.Common.Settings; + +namespace Play.Common.MongoDB +{ + public static class Extensions + { + public static IServiceCollection AddMongo(this IServiceCollection services) + { + BsonSerializer.RegisterSerializer(new GuidSerializer(BsonType.String)); + BsonSerializer.RegisterSerializer(new DateTimeOffsetSerializer(BsonType.String)); + + services.AddSingleton(serviceProvider => + { + var configuration = serviceProvider.GetService<IConfiguration>(); + var serviceSettings = configuration.GetSection(nameof(ServiceSettings)).Get<ServiceSettings>(); + var mongoDbSettings = configuration.GetSection(nameof(MongoDbSettings)).Get<MongoDbSettings>(); + var mongoClient = new MongoClient(mongoDbSettings.ConnectionString); + return mongoClient.GetDatabase(serviceSettings.ServiceName); + }); + + return services; + } + + public static IServiceCollection AddMongoRepository<T>(this IServiceCollection services, string collectionName) + where T : IEntity + { + services.AddSingleton<IRepository<T>>(serviceProvider => + { + var database = serviceProvider.GetService<IMongoDatabase>(); + return new MongoRepository<T>(database, collectionName); + }); + + return services; + } + } +} \ No newline at end of file diff --git a/Play.Common1/src/Play.Common/MongoDB/MongoRepository.cs b/Play.Common1/src/Play.Common/MongoDB/MongoRepository.cs new file mode 100644 index 0000000..6e91c2c --- /dev/null +++ b/Play.Common1/src/Play.Common/MongoDB/MongoRepository.cs @@ -0,0 +1,68 @@ +using System; +using System.Collections.Generic; +using System.Linq.Expressions; +using System.Threading.Tasks; +using MongoDB.Driver; + +namespace Play.Common.MongoDB +{ + + public class MongoRepository<T> : IRepository<T> where T : IEntity + { + private readonly IMongoCollection<T> dbCollection; + private readonly FilterDefinitionBuilder<T> filterBuilder = Builders<T>.Filter; + + public MongoRepository(IMongoDatabase database, string collectionName) + { + dbCollection = database.GetCollection<T>(collectionName); + } + + public async Task<IReadOnlyCollection<T>> GetAllAsync() + { + return await dbCollection.Find(filterBuilder.Empty).ToListAsync(); + } + + public async Task<IReadOnlyCollection<T>> GetAllAsync(Expression<Func<T, bool>> filter) + { + return await dbCollection.Find(filter).ToListAsync(); + } + + public async Task<T> GetAsync(Guid id) + { + FilterDefinition<T> filter = filterBuilder.Eq(entity => entity.Id, id); + return await dbCollection.Find(filter).FirstOrDefaultAsync(); + } + + public async Task<T> GetAsync(Expression<Func<T, bool>> filter) + { + return await dbCollection.Find(filter).FirstOrDefaultAsync(); + } + + public async Task CreateAsync(T entity) + { + if (entity == null) + { + throw new ArgumentNullException(nameof(entity)); + } + + await dbCollection.InsertOneAsync(entity); + } + + public async Task UpdateAsync(T entity) + { + if (entity == null) + { + throw new ArgumentNullException(nameof(entity)); + } + + FilterDefinition<T> filter = filterBuilder.Eq(existingEntity => existingEntity.Id, entity.Id); + await dbCollection.ReplaceOneAsync(filter, entity); + } + + public async Task RemoveAsync(Guid id) + { + FilterDefinition<T> filter = filterBuilder.Eq(entity => entity.Id, id); + await dbCollection.DeleteOneAsync(filter); + } + } +} \ No newline at end of file diff --git a/Play.Common1/src/Play.Common/Play.Common.csproj b/Play.Common1/src/Play.Common/Play.Common.csproj new file mode 100644 index 0000000..2e9f83a --- /dev/null +++ b/Play.Common1/src/Play.Common/Play.Common.csproj @@ -0,0 +1,20 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <TargetFramework>net7.0</TargetFramework> + <PackageDescription>Common libraries used by Play Economy services.</PackageDescription> + <Authors>Thomas Staub</Authors> + <Company>.NET Microservices</Company> + </PropertyGroup> + + <ItemGroup> + <PackageReference Include="MassTransit.AspNetCore" Version="7.3.1" /> + <PackageReference Include="MassTransit.RabbitMQ" Version="7.3.1" /> + <PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="7.0.0" /> + <PackageReference Include="Microsoft.Extensions.Configuration" Version="7.0.0" /> + <PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="7.0.0" /> + <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0" /> + <PackageReference Include="MongoDB.Driver" Version="2.18.0" /> + </ItemGroup> + +</Project> diff --git a/Play.Common/src/Play.Common/Play.Common.sln b/Play.Common1/src/Play.Common/Play.Common.sln similarity index 100% rename from Play.Common/src/Play.Common/Play.Common.sln rename to Play.Common1/src/Play.Common/Play.Common.sln diff --git a/Play.Common1/src/Play.Common/Settings/MongoDbSettings.cs b/Play.Common1/src/Play.Common/Settings/MongoDbSettings.cs new file mode 100644 index 0000000..8a0be7d --- /dev/null +++ b/Play.Common1/src/Play.Common/Settings/MongoDbSettings.cs @@ -0,0 +1,11 @@ +namespace Play.Common.Settings +{ + public class MongoDbSettings + { + public string Host { get; init; } + + public int Port { get; init; } + + public string ConnectionString => $"mongodb://{Host}:{Port}"; + } +} \ No newline at end of file diff --git a/Play.Common1/src/Play.Common/Settings/RabbitMQSettings.cs b/Play.Common1/src/Play.Common/Settings/RabbitMQSettings.cs new file mode 100644 index 0000000..467fc24 --- /dev/null +++ b/Play.Common1/src/Play.Common/Settings/RabbitMQSettings.cs @@ -0,0 +1,7 @@ +namespace Play.Common.Settings +{ + public class RabbitMQSettings + { + public string Host { get; init; } + } +} \ No newline at end of file diff --git a/Play.Common1/src/Play.Common/Settings/ServiceBusSettings.cs b/Play.Common1/src/Play.Common/Settings/ServiceBusSettings.cs new file mode 100644 index 0000000..065122a --- /dev/null +++ b/Play.Common1/src/Play.Common/Settings/ServiceBusSettings.cs @@ -0,0 +1,7 @@ +namespace Play.Common.Settings +{ + public class ServiceBusSettings + { + public string ConnectionString { get; init; } + } +} \ No newline at end of file diff --git a/Play.Common1/src/Play.Common/Settings/ServiceSettings.cs b/Play.Common1/src/Play.Common/Settings/ServiceSettings.cs new file mode 100644 index 0000000..046b661 --- /dev/null +++ b/Play.Common1/src/Play.Common/Settings/ServiceSettings.cs @@ -0,0 +1,8 @@ +namespace Play.Common.Settings +{ + public class ServiceSettings + { + public string ServiceName { get; init; } + public string Authority { get; init; } + } +} \ No newline at end of file diff --git a/Play.Common/src/Play.Common/nuget.config b/Play.Common1/src/Play.Common/nuget.config similarity index 100% rename from Play.Common/src/Play.Common/nuget.config rename to Play.Common1/src/Play.Common/nuget.config diff --git a/Play.Identity/kubernetes/identity.yaml b/Play.Identity/kubernetes/identity.yaml new file mode 100644 index 0000000..f0e505b --- /dev/null +++ b/Play.Identity/kubernetes/identity.yaml @@ -0,0 +1,84 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: identity-deployment +spec: + selector: + matchLabels: + app: identity + template: + metadata: + labels: + app: identity + azure.workload.identity/use: "true" + spec: + serviceAccountName: identity-serviceaccount + containers: + - name: identity + image: playeconomy.azurecr.io/play.identity:1.0.10 + env: + - name: ServiceSettings__MessageBroker + value: SERVICEBUS + - name: ServiceSettings__KeyVaultName + value: playeconomy + - name: IdentitySettings__PathBase + value: /identity-svc + - name: IdentitySettings__CertificateCerFilePath + value: "/certificates/certificate.crt" + - name: IdentitySettings__CertificateKeyFilePath + value: "/certificates/certificate.key" + - name: IdentityServerSettings__Clients__0__RedirectUris__0 + value: https://playeconomy.eastus.cloudapp.azure.com/authentication/login-callback + - name: IdentityServerSettings__Clients__0__PostLogoutRedirectUris__0 + value: https://playeconomy.eastus.cloudapp.azure.com/authentication/logout-callback + resources: + limits: + memory: "128Mi" + cpu: "150m" + ports: + - containerPort: 5002 + livenessProbe: + httpGet: + path: /health/live + port: 5002 + initialDelaySeconds: 10 + readinessProbe: + httpGet: + path: /health/ready + port: 5002 + initialDelaySeconds: 10 + volumeMounts: + - name: certificate-volume + mountPath: /certificates + volumes: + - name: certificate-volume + secret: + secretName: signing-cert + items: + - key: tls.key + path: certificate.key + - key: tls.crt + path: certificate.crt + +--- +apiVersion: v1 +kind: Service +metadata: + name: identity-service +spec: + type: ClusterIP + selector: + app: identity + ports: + - port: 80 + targetPort: 5002 + +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: identity-serviceaccount + annotations: + azure.workload.identity/client-id: b68260b6-466a-4483-b432-6df6f7073df1 + labels: + azure.workload.identity/use: "true" \ No newline at end of file diff --git a/Play.Identity/kubernetes/singning-cer.yaml b/Play.Identity/kubernetes/singning-cer.yaml new file mode 100644 index 0000000..99ac088 --- /dev/null +++ b/Play.Identity/kubernetes/singning-cer.yaml @@ -0,0 +1,11 @@ +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + name: signing-cert +spec: + secretName: signing-cert + issuerRef: + name: letsencrypt-prod + kind: ClusterIssuer + dnsNames: + - playeconomy.eastus.cloudapp.azure.com \ No newline at end of file diff --git a/Play.Identity/src/Play.Identity.Service/Play.Identity.Service.csproj b/Play.Identity/src/Play.Identity.Service/Play.Identity.Service.csproj index 187d9c4..bd8aaa3 100644 --- a/Play.Identity/src/Play.Identity.Service/Play.Identity.Service.csproj +++ b/Play.Identity/src/Play.Identity.Service/Play.Identity.Service.csproj @@ -11,7 +11,7 @@ <PackageReference Include="Duende.IdentityServer.AspNetIdentity" Version="6.1.7" /> <PackageReference Include="Microsoft.AspNetCore.Identity.UI" Version="7.0.0" /> <PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="7.0.0" /> - <PackageReference Include="play.common" Version="1.0.7" /> + <PackageReference Include="play.common" Version="1.0.16" /> <PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0" /> </ItemGroup> diff --git a/Play.Identity/src/Play.Identity.Service/Program.cs b/Play.Identity/src/Play.Identity.Service/Program.cs index 6360a0d..e1ebc23 100644 --- a/Play.Identity/src/Play.Identity.Service/Program.cs +++ b/Play.Identity/src/Play.Identity.Service/Program.cs @@ -12,6 +12,7 @@ using Microsoft.Extensions.DependencyInjection; using MongoDB.Bson; using MongoDB.Bson.Serialization; using MongoDB.Bson.Serialization.Serializers; +using Play.Common.HealthChecks; using Play.Common.MassTransit; using Play.Common.Settings; using Play.Identity.Service.Entities; @@ -120,6 +121,9 @@ namespace Play.Identity.Service } }); }); + + services.AddHealthChecks() + .AddMongoDb(); } @@ -187,6 +191,7 @@ namespace Play.Identity.Service { endpoints.MapControllers(); endpoints.MapRazorPages(); + endpoints.MapPlayEconomyHealthChecks(); }); } } diff --git a/Play.Infra/cert-manager/acme-challenge.yaml b/Play.Infra/cert-manager/acme-challenge.yaml new file mode 100644 index 0000000..b624a3c --- /dev/null +++ b/Play.Infra/cert-manager/acme-challenge.yaml @@ -0,0 +1,21 @@ +apiVersion: v1 +kind: Service +metadata: + name: acme-challenge-service +spec: + ports: + - port: 80 + targetPort: 8089 + selector: + acme.cert-manager.io/http01-solver: "true" + +--- +apiVersion: getambassador.io/v3alpha1 +kind: Mapping +metadata: + name: acme-challenge-mapping +spec: + hostname: playeconomy.eastus.cloudapp.azure.com + prefix: /.well-known/acme-challenge/ + rewrite: "" + service: acme-challenge-service \ No newline at end of file diff --git a/Play.Infra/cert-manager/cluster-issuer.yaml b/Play.Infra/cert-manager/cluster-issuer.yaml new file mode 100644 index 0000000..fb3843e --- /dev/null +++ b/Play.Infra/cert-manager/cluster-issuer.yaml @@ -0,0 +1,14 @@ +apiVersion: cert-manager.io/v1 +kind: ClusterIssuer +metadata: + name: letsencrypt-prod +spec: + acme: + server: https://acme-v02.api.letsencrypt.org/directory + email: julioc@dotnetmicroservices.com + privateKeySecretRef: + name: letsencrypt-prod + solvers: + - http01: + ingress: + class: nginx \ No newline at end of file diff --git a/Play.Infra/emissary-ingress/host.yaml b/Play.Infra/emissary-ingress/host.yaml new file mode 100644 index 0000000..9c5e477 --- /dev/null +++ b/Play.Infra/emissary-ingress/host.yaml @@ -0,0 +1,10 @@ +apiVersion: getambassador.io/v3alpha1 +kind: Host +metadata: + name: playeconomy-host +spec: + hostname: playeconomy.eastus.cloudapp.azure.com + acmeProvider: + email: julioc@dotnetmicroservices.com + tlsSecret: + name: playeconomy-tls \ No newline at end of file diff --git a/Play.Infra/emissary-ingress/listener.yaml b/Play.Infra/emissary-ingress/listener.yaml new file mode 100644 index 0000000..30a2c6d --- /dev/null +++ b/Play.Infra/emissary-ingress/listener.yaml @@ -0,0 +1,24 @@ +apiVersion: getambassador.io/v3alpha1 +kind: Listener +metadata: + name: http-listener +spec: + port: 8080 + protocol: HTTP + securityModel: XFP # X-Forwarded-Proto + hostBinding: + namespace: + from: SELF + +--- +apiVersion: getambassador.io/v3alpha1 +kind: Listener +metadata: + name: https-listener +spec: + port: 8443 + protocol: HTTPS + securityModel: XFP + hostBinding: + namespace: + from: SELF \ No newline at end of file diff --git a/Play.Infra/emissary-ingress/mappings.yaml b/Play.Infra/emissary-ingress/mappings.yaml new file mode 100644 index 0000000..3c9411c --- /dev/null +++ b/Play.Infra/emissary-ingress/mappings.yaml @@ -0,0 +1,50 @@ +apiVersion: getambassador.io/v3alpha1 +kind: Mapping +metadata: + name: identity-mapping +spec: + hostname: playeconomy.eastus.cloudapp.azure.com + prefix: /identity-svc/ + service: identity-service.identity + +--- +apiVersion: getambassador.io/v3alpha1 +kind: Mapping +metadata: + name: catalog-mapping +spec: + hostname: playeconomy.eastus.cloudapp.azure.com + prefix: /catalog-svc/ + service: catalog-service.catalog + +--- +apiVersion: getambassador.io/v3alpha1 +kind: Mapping +metadata: + name: inventory-mapping +spec: + hostname: playeconomy.eastus.cloudapp.azure.com + prefix: /inventory-svc/ + service: inventory-service.inventory + +--- +apiVersion: getambassador.io/v3alpha1 +kind: Mapping +metadata: + name: trading-mapping +spec: + hostname: playeconomy.eastus.cloudapp.azure.com + prefix: /trading-svc/ + service: trading-service.trading + allow_upgrade: + - websocket + +--- +apiVersion: getambassador.io/v3alpha1 +kind: Mapping +metadata: + name: frontend-mapping +spec: + hostname: playeconomy.eastus.cloudapp.azure.com + prefix: / + service: frontend-client.frontend diff --git a/Play.Infra/emissary-ingress/tls-certificate.yaml b/Play.Infra/emissary-ingress/tls-certificate.yaml new file mode 100644 index 0000000..d45492d --- /dev/null +++ b/Play.Infra/emissary-ingress/tls-certificate.yaml @@ -0,0 +1,11 @@ +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + name: playeconomy-tls-cert +spec: + secretName: playeconomy-tls + issuerRef: + name: letsencrypt-prod + kind: ClusterIssuer + dnsNames: + - "playeconomy.eastus.cloudapp.azure.com" \ No newline at end of file diff --git a/Play.Inventory/src/Play.Inventory.Service/Play.Inventory.Service.csproj b/Play.Inventory/src/Play.Inventory.Service/Play.Inventory.Service.csproj index 45f4b3d..028c72d 100644 --- a/Play.Inventory/src/Play.Inventory.Service/Play.Inventory.Service.csproj +++ b/Play.Inventory/src/Play.Inventory.Service/Play.Inventory.Service.csproj @@ -7,7 +7,7 @@ <ItemGroup> <PackageReference Include="Microsoft.Extensions.Http.Polly" Version="7.0.0" /> <PackageReference Include="Play.Catalog.Contracts" Version="1.0.1" /> - <PackageReference Include="Play.Common" Version="1.0.13" /> + <PackageReference Include="Play.Common" Version="1.0.16" /> <PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0" /> </ItemGroup> diff --git a/Play.Trading/src/Play.Trading.Service/Play.Trading.Service.csproj b/Play.Trading/src/Play.Trading.Service/Play.Trading.Service.csproj index 82df04a..cd621ae 100644 --- a/Play.Trading/src/Play.Trading.Service/Play.Trading.Service.csproj +++ b/Play.Trading/src/Play.Trading.Service/Play.Trading.Service.csproj @@ -7,7 +7,7 @@ <ItemGroup> <PackageReference Include="MassTransit.MongoDB" Version="7.3.1" /> <PackageReference Include="Play.Catalog.Contracts" Version="1.0.1" /> - <PackageReference Include="Play.Common" Version="1.0.13" /> + <PackageReference Include="Play.Common" Version="1.0.16" /> <PackageReference Include="Play.Identity.Contracts" Version="1.0.1" /> <PackageReference Include="Play.Inventory.Contracts" Version="1.0.1" /> <PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0" /> diff --git a/README.md b/README.md index 557e5e5..4c88669 100644 --- a/README.md +++ b/README.md @@ -4,4 +4,17 @@ Login: admin@play.com PW: Pass@word1 -https://learn.dotnetacademy.io/enrollments \ No newline at end of file +https://learn.dotnetacademy.io/enrollments + + +| Microservice | Port http |Port https| +|--|--|--| +|Catalog | 5000 |5001| +|Inventory |5004 |5005| +|Trading | 5006 |5007| +|Frontend | 3000 || +|Frontendv2| 5008 |5009| +|Identity| 5002 |5003| +|Mongo | 27017 || +|rabbistmq| 5672 / 15672 || + diff --git a/packages/Play.Common.1.0.15.nupkg b/packages/Play.Common.1.0.15.nupkg new file mode 100644 index 0000000000000000000000000000000000000000..105adc0b1e54bb8ad029fe538688920edaf56aec GIT binary patch literal 14224 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 WvK%zbKU+ZmeS`jH>Y4MOum1&3VNF>8 literal 0 HcmV?d00001 diff --git a/packages/Play.Common.1.0.16.nupkg b/packages/Play.Common.1.0.16.nupkg new file mode 100644 index 0000000000000000000000000000000000000000..8ca039a7ccf46f90deab390734a3be99ab782cdc GIT binary patch literal 14224 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 b_WwgtD9b^^{AUX2e<kQ&q+U4w^YniK!kb%l literal 0 HcmV?d00001 -- GitLab