tools/code/publisher/Product.cs (236 lines of code) (raw):
using Azure.Core.Pipeline;
using common;
using LanguageExt;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace publisher;
public delegate ValueTask PutProducts(CancellationToken cancellationToken);
public delegate Option<ProductName> TryParseProductName(FileInfo file);
public delegate bool IsProductNameInSourceControl(ProductName name);
public delegate ValueTask PutProduct(ProductName name, CancellationToken cancellationToken);
public delegate ValueTask<Option<ProductDto>> FindProductDto(ProductName name, CancellationToken cancellationToken);
public delegate ValueTask PutProductInApim(ProductName name, ProductDto dto, CancellationToken cancellationToken);
public delegate ValueTask DeleteProducts(CancellationToken cancellationToken);
public delegate ValueTask DeleteProduct(ProductName name, CancellationToken cancellationToken);
public delegate ValueTask DeleteProductFromApim(ProductName name, CancellationToken cancellationToken);
internal static class ProductModule
{
public static void ConfigurePutProducts(IHostApplicationBuilder builder)
{
CommonModule.ConfigureGetPublisherFiles(builder);
ConfigureTryParseProductName(builder);
ConfigureIsProductNameInSourceControl(builder);
ConfigurePutProduct(builder);
builder.Services.TryAddSingleton(GetPutProducts);
}
private static PutProducts GetPutProducts(IServiceProvider provider)
{
var getPublisherFiles = provider.GetRequiredService<GetPublisherFiles>();
var tryParseName = provider.GetRequiredService<TryParseProductName>();
var isNameInSourceControl = provider.GetRequiredService<IsProductNameInSourceControl>();
var put = provider.GetRequiredService<PutProduct>();
var activitySource = provider.GetRequiredService<ActivitySource>();
var logger = provider.GetRequiredService<ILogger>();
return async cancellationToken =>
{
using var _ = activitySource.StartActivity(nameof(PutProducts));
logger.LogInformation("Putting products...");
await getPublisherFiles()
.Choose(tryParseName.Invoke)
.Where(isNameInSourceControl.Invoke)
.Distinct()
.IterParallel(put.Invoke, cancellationToken);
};
}
private static void ConfigureTryParseProductName(IHostApplicationBuilder builder)
{
AzureModule.ConfigureManagementServiceDirectory(builder);
builder.Services.TryAddSingleton(GetTryParseProductName);
}
private static TryParseProductName GetTryParseProductName(IServiceProvider provider)
{
var serviceDirectory = provider.GetRequiredService<ManagementServiceDirectory>();
return file => from informationFile in ProductInformationFile.TryParse(file, serviceDirectory)
select informationFile.Parent.Name;
}
private static void ConfigureIsProductNameInSourceControl(IHostApplicationBuilder builder)
{
CommonModule.ConfigureGetArtifactFiles(builder);
AzureModule.ConfigureManagementServiceDirectory(builder);
builder.Services.TryAddSingleton(GetIsProductNameInSourceControl);
}
private static IsProductNameInSourceControl GetIsProductNameInSourceControl(IServiceProvider provider)
{
var getArtifactFiles = provider.GetRequiredService<GetArtifactFiles>();
var serviceDirectory = provider.GetRequiredService<ManagementServiceDirectory>();
return doesInformationFileExist;
bool doesInformationFileExist(ProductName name)
{
var artifactFiles = getArtifactFiles();
var informationFile = ProductInformationFile.From(name, serviceDirectory);
return artifactFiles.Contains(informationFile.ToFileInfo());
}
}
private static void ConfigurePutProduct(IHostApplicationBuilder builder)
{
ConfigureFindProductDto(builder);
ConfigurePutProductInApim(builder);
builder.Services.TryAddSingleton(GetPutProduct);
}
private static PutProduct GetPutProduct(IServiceProvider provider)
{
var findDto = provider.GetRequiredService<FindProductDto>();
var putInApim = provider.GetRequiredService<PutProductInApim>();
var activitySource = provider.GetRequiredService<ActivitySource>();
return async (name, cancellationToken) =>
{
using var _ = activitySource.StartActivity(nameof(PutProduct))
?.AddTag("product.name", name);
var dtoOption = await findDto(name, cancellationToken);
await dtoOption.IterTask(async dto => await putInApim(name, dto, cancellationToken));
};
}
private static void ConfigureFindProductDto(IHostApplicationBuilder builder)
{
AzureModule.ConfigureManagementServiceDirectory(builder);
CommonModule.ConfigureTryGetFileContents(builder);
OverrideDtoModule.ConfigureOverrideDtoFactory(builder);
builder.Services.TryAddSingleton(GetFindProductDto);
}
private static FindProductDto GetFindProductDto(IServiceProvider provider)
{
var serviceDirectory = provider.GetRequiredService<ManagementServiceDirectory>();
var tryGetFileContents = provider.GetRequiredService<TryGetFileContents>();
var overrideFactory = provider.GetRequiredService<OverrideDtoFactory>();
var overrideDto = overrideFactory.Create<ProductName, ProductDto>();
return async (name, cancellationToken) =>
{
var informationFile = ProductInformationFile.From(name, serviceDirectory);
var informationFileInfo = informationFile.ToFileInfo();
var contentsOption = await tryGetFileContents(informationFileInfo, cancellationToken);
return from contents in contentsOption
let dto = contents.ToObjectFromJson<ProductDto>()
select overrideDto(name, dto);
};
}
private static void ConfigurePutProductInApim(IHostApplicationBuilder builder)
{
AzureModule.ConfigureManagementServiceUri(builder);
AzureModule.ConfigureHttpPipeline(builder);
builder.Services.TryAddSingleton(GetPutProductInApim);
}
private static PutProductInApim GetPutProductInApim(IServiceProvider provider)
{
var serviceUri = provider.GetRequiredService<ManagementServiceUri>();
var pipeline = provider.GetRequiredService<HttpPipeline>();
var logger = provider.GetRequiredService<ILogger>();
return async (name, dto, cancellationToken) =>
{
logger.LogInformation("Putting product {ProductName}...", name);
var productAlreadyExists = await doesProductAlreadyExist(name, cancellationToken);
await ProductUri.From(name, serviceUri)
.PutDto(dto, pipeline, cancellationToken);
// Delete automatically created resources if the product is new. This ensures that APIM is consistent with source control.
if (productAlreadyExists is false)
{
await deleteAutomaticallyCreatedProductGroups(name, cancellationToken);
await deleteAutomaticallyCreatedProductSubscriptions(name, cancellationToken);
}
};
async ValueTask<bool> doesProductAlreadyExist(ProductName name, CancellationToken cancellationToken)
{
var productUri = ProductUri.From(name, serviceUri).ToUri();
var contentOption = await pipeline.GetContentOption(productUri, cancellationToken);
return contentOption.IsSome;
}
async ValueTask deleteAutomaticallyCreatedProductGroups(ProductName productName, CancellationToken cancellationToken) =>
await ProductGroupsUri.From(productName, serviceUri)
.ListNames(pipeline, cancellationToken)
.Do(groupName => logger.LogWarning("Removing automatically added group {GroupName} from product {ProductName}...", groupName, productName))
.IterParallel(async name => await ProductGroupUri.From(name, productName, serviceUri)
.Delete(pipeline, cancellationToken),
cancellationToken);
async ValueTask deleteAutomaticallyCreatedProductSubscriptions(ProductName productName, CancellationToken cancellationToken) =>
await ProductUri.From(productName, serviceUri)
.ListSubscriptionNames(pipeline, cancellationToken)
.WhereAwait(async subscriptionName =>
{
var dto = await SubscriptionUri.From(subscriptionName, serviceUri)
.GetDto(pipeline, cancellationToken);
// Automatically created subscriptions have no display name and end with "/users/1"
return string.IsNullOrWhiteSpace(dto.Properties.DisplayName)
&& (dto.Properties.OwnerId?.EndsWith("/users/1", StringComparison.OrdinalIgnoreCase) ?? false);
})
.Do(subscriptionName => logger.LogWarning("Deleting automatically created subscription {SubscriptionName} from product {ProductName}...", subscriptionName, productName))
.IterParallel(async name => await SubscriptionUri.From(name, serviceUri)
.Delete(pipeline, cancellationToken),
cancellationToken);
}
public static void ConfigureDeleteProducts(IHostApplicationBuilder builder)
{
CommonModule.ConfigureGetPublisherFiles(builder);
ConfigureTryParseProductName(builder);
ConfigureIsProductNameInSourceControl(builder);
ConfigureDeleteProduct(builder);
builder.Services.TryAddSingleton(GetDeleteProducts);
}
private static DeleteProducts GetDeleteProducts(IServiceProvider provider)
{
var getPublisherFiles = provider.GetRequiredService<GetPublisherFiles>();
var tryParseName = provider.GetRequiredService<TryParseProductName>();
var isNameInSourceControl = provider.GetRequiredService<IsProductNameInSourceControl>();
var delete = provider.GetRequiredService<DeleteProduct>();
var activitySource = provider.GetRequiredService<ActivitySource>();
var logger = provider.GetRequiredService<ILogger>();
return async cancellationToken =>
{
using var _ = activitySource.StartActivity(nameof(DeleteProducts));
logger.LogInformation("Deleting products...");
await getPublisherFiles()
.Choose(tryParseName.Invoke)
.Where(name => isNameInSourceControl(name) is false)
.Distinct()
.IterParallel(delete.Invoke, cancellationToken);
};
}
private static void ConfigureDeleteProduct(IHostApplicationBuilder builder)
{
ConfigureDeleteProductFromApim(builder);
builder.Services.TryAddSingleton(GetDeleteProduct);
}
private static DeleteProduct GetDeleteProduct(IServiceProvider provider)
{
var deleteFromApim = provider.GetRequiredService<DeleteProductFromApim>();
var activitySource = provider.GetRequiredService<ActivitySource>();
return async (name, cancellationToken) =>
{
using var _ = activitySource.StartActivity(nameof(DeleteProduct))
?.AddTag("product.name", name);
await deleteFromApim(name, cancellationToken);
};
}
private static void ConfigureDeleteProductFromApim(IHostApplicationBuilder builder)
{
AzureModule.ConfigureManagementServiceUri(builder);
AzureModule.ConfigureHttpPipeline(builder);
builder.Services.TryAddSingleton(GetDeleteProductFromApim);
}
private static DeleteProductFromApim GetDeleteProductFromApim(IServiceProvider provider)
{
var serviceUri = provider.GetRequiredService<ManagementServiceUri>();
var pipeline = provider.GetRequiredService<HttpPipeline>();
var logger = provider.GetRequiredService<ILogger>();
return async (name, cancellationToken) =>
{
logger.LogInformation("Deleting product {ProductName}...", name);
await ProductUri.From(name, serviceUri)
.Delete(pipeline, cancellationToken);
};
}
}