135 lines
7.7 KiB
C#
135 lines
7.7 KiB
C#
using Serilog;
|
|
using Tiku.Application;
|
|
using Tiku.Application.PlatformBilling;
|
|
using Tiku.Application.Security;
|
|
using Tiku.Application.Tenancy;
|
|
using Tiku.Infrastructure;
|
|
using Tiku.Infrastructure.Assets;
|
|
using Tiku.Infrastructure.Storage;
|
|
using Tiku.Infrastructure.Observability;
|
|
using Tiku.Infrastructure.Security;
|
|
using OpenTelemetry.Metrics;
|
|
|
|
namespace Tiku.Worker;
|
|
|
|
internal static class WorkerDependencyInjection
|
|
{
|
|
internal static HostApplicationBuilder AddWorkerServices(this HostApplicationBuilder builder)
|
|
{
|
|
builder.Services.AddSerilog((services, configuration) => configuration
|
|
.ReadFrom.Configuration(builder.Configuration)
|
|
.ReadFrom.Services(services)
|
|
.Enrich.FromLogContext(),
|
|
preserveStaticLogger: true);
|
|
|
|
var connectionString = builder.Configuration.GetConnectionString("Database") ??
|
|
builder.Configuration["DATABASE_URL"] ??
|
|
(builder.Environment.IsDevelopment()
|
|
? $"Host=localhost;Database=tiku;Username={Environment.UserName}"
|
|
: throw new InvalidOperationException(
|
|
"Database connection is required outside Development. Configure ConnectionStrings:Database or DATABASE_URL."));
|
|
builder.Services.AddApplication();
|
|
builder.Services.AddInfrastructure(connectionString);
|
|
var redisConnectionString = builder.Configuration.GetConnectionString("Redis") ?? builder.Configuration["REDIS_URL"];
|
|
builder.Services.AddOptions<AuthorizationCacheOptions>()
|
|
.Bind(builder.Configuration.GetSection(AuthorizationCacheOptions.SectionName));
|
|
if (!string.IsNullOrWhiteSpace(redisConnectionString))
|
|
{
|
|
builder.Services.AddRedisSecurity(redisConnectionString, builder.Environment.EnvironmentName);
|
|
}
|
|
else if (builder.Environment.IsProduction())
|
|
{
|
|
throw new InvalidOperationException(
|
|
"Redis is required in Production for authorization cache invalidation retries.");
|
|
}
|
|
var otlpEndpoint = builder.Configuration["OpenTelemetry:OtlpEndpoint"];
|
|
var telemetry = builder.Services.AddOpenTelemetry().WithMetrics(metrics =>
|
|
metrics.AddMeter(WorkerTelemetry.MeterName, AuthorizationCacheTelemetry.MeterName));
|
|
if (Uri.TryCreate(otlpEndpoint, UriKind.Absolute, out var endpoint))
|
|
{
|
|
telemetry.WithMetrics(metrics => metrics.AddOtlpExporter(options => options.Endpoint = endpoint));
|
|
}
|
|
builder.Services.Configure<ObjectStorageOptions>(builder.Configuration.GetSection(ObjectStorageOptions.SectionName));
|
|
builder.Services.Configure<AliyunOssOptions>(builder.Configuration.GetSection(AliyunOssOptions.SectionName));
|
|
builder.Services.PostConfigure<ObjectStorageOptions>(options =>
|
|
{
|
|
options.DefaultProvider = builder.Configuration["STORAGE_DEFAULT_PROVIDER"] ?? options.DefaultProvider;
|
|
options.DefaultBucket = builder.Configuration["STORAGE_DEFAULT_BUCKET"] ?? options.DefaultBucket;
|
|
options.PublicBaseUrl = builder.Configuration["STORAGE_PUBLIC_BASE_URL"] ?? options.PublicBaseUrl;
|
|
options.AllowedMimePrefixes = SplitLegacyList(
|
|
builder.Configuration["STORAGE_ALLOWED_MIME_PREFIXES"],
|
|
options.AllowedMimePrefixes);
|
|
options.AllowedMimeTypes = SplitLegacyList(
|
|
builder.Configuration["STORAGE_ALLOWED_MIME_TYPES"],
|
|
options.AllowedMimeTypes);
|
|
options.RequireTenantPrefix = bool.TryParse(
|
|
builder.Configuration["STORAGE_REQUIRE_TENANT_PREFIX"],
|
|
out var requireTenantPrefix)
|
|
? requireTenantPrefix
|
|
: options.RequireTenantPrefix;
|
|
options.MaxUploadBytes = long.TryParse(
|
|
builder.Configuration["STORAGE_MAX_UPLOAD_BYTES"],
|
|
out var maxUploadBytes)
|
|
? maxUploadBytes
|
|
: options.MaxUploadBytes;
|
|
});
|
|
builder.Services.PostConfigure<AliyunOssOptions>(options =>
|
|
{
|
|
options.Region = builder.Configuration["ALIYUN_OSS_REGION"] ?? options.Region;
|
|
options.Endpoint = builder.Configuration["ALIYUN_OSS_ENDPOINT"] ?? options.Endpoint;
|
|
options.AccessKeyId = builder.Configuration["ALIYUN_OSS_ACCESS_KEY_ID"] ?? options.AccessKeyId;
|
|
options.AccessKeySecret = builder.Configuration["ALIYUN_OSS_ACCESS_KEY_SECRET"] ?? options.AccessKeySecret;
|
|
options.SecurityToken = builder.Configuration["ALIYUN_OSS_STS_TOKEN"] ?? options.SecurityToken;
|
|
options.UseInternalEndpoint = bool.TryParse(
|
|
builder.Configuration["ALIYUN_OSS_INTERNAL"],
|
|
out var useInternalEndpoint)
|
|
? useInternalEndpoint
|
|
: options.UseInternalEndpoint;
|
|
});
|
|
builder.Services.AddOptions<ObjectStorageOptions>()
|
|
.Validate(
|
|
options => !builder.Environment.IsProduction() ||
|
|
options.DefaultProvider == Tiku.Application.Storage.ObjectStorageProviders.AliyunOss,
|
|
"Production managed storage must use the configured Aliyun OSS provider.")
|
|
.ValidateOnStart();
|
|
builder.Services.AddOptions<AliyunOssOptions>()
|
|
.Validate<Microsoft.Extensions.Options.IOptions<ObjectStorageOptions>>(
|
|
(aliyun, storage) =>
|
|
!builder.Environment.IsProduction() ||
|
|
storage.Value.DefaultProvider != Tiku.Application.Storage.ObjectStorageProviders.AliyunOss ||
|
|
aliyun.IsConfigured,
|
|
"Aliyun OSS credentials and region or endpoint are required when it is the default provider.")
|
|
.ValidateOnStart();
|
|
builder.Services.AddOptions<ClamAvOptions>()
|
|
.Bind(builder.Configuration.GetSection(ClamAvOptions.SectionName))
|
|
.Validate(ClamAvOptions.BeValid, "ClamAV settings are invalid.")
|
|
.Validate<Microsoft.Extensions.Options.IOptions<ObjectStorageOptions>>(
|
|
(clamAv, storage) => clamAv.StreamMaxLength >= storage.Value.MaxUploadBytes,
|
|
"ClamAV StreamMaxLength must cover the storage max upload size.")
|
|
.ValidateOnStart();
|
|
builder.Services.AddOptions<WorkerOptions>()
|
|
.Bind(builder.Configuration.GetSection(WorkerOptions.SectionName))
|
|
.Validate(WorkerOptions.BeValid, "Worker settings are invalid.")
|
|
.ValidateOnStart();
|
|
builder.Services.AddOptions<DomainLifecycleOptions>()
|
|
.Bind(builder.Configuration.GetSection("TenantDomains"));
|
|
builder.Services.AddOptions<SaasSubscriptionLifecycleOptions>()
|
|
.Bind(builder.Configuration.GetSection("SaasSubscriptions"));
|
|
builder.Services.AddOptions<FeatureUsageReconciliationOptions>()
|
|
.Bind(builder.Configuration.GetSection("FeatureUsageReconciliation"));
|
|
builder.Services.AddSingleton<IPeriodicProcessorLock, PostgresPeriodicProcessorLock>();
|
|
builder.Services.AddSingleton<WorkerStateReporter>();
|
|
builder.Services.AddHostedService<TenantDomainWorker>();
|
|
builder.Services.AddHostedService<SaasSubscriptionWorker>();
|
|
builder.Services.AddHostedService<FeatureUsageWorker>();
|
|
builder.Services.AddHostedService<BackgroundJobsWorker>();
|
|
builder.Services.AddHostedService<AuthorizationCacheInvalidationWorker>();
|
|
return builder;
|
|
}
|
|
|
|
private static string[] SplitLegacyList(string? value, string[] fallback) =>
|
|
string.IsNullOrWhiteSpace(value)
|
|
? fallback
|
|
: value.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries);
|
|
}
|