tools/code/publisher/Api.cs (564 lines of code) (raw):
using Azure.Core.Pipeline;
using common;
using DotNext.Threading;
using LanguageExt;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.OpenApi;
using Microsoft.OpenApi.Exceptions;
using Microsoft.OpenApi.Extensions;
using Microsoft.OpenApi.Models;
using Microsoft.OpenApi.Readers;
using System;
using System.Collections.Concurrent;
using System.Collections.Frozen;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace publisher;
public delegate ValueTask PutApis(CancellationToken cancellationToken);
public delegate Option<ApiName> TryParseApiName(FileInfo file);
public delegate bool IsApiNameInSourceControl(ApiName name);
public delegate ValueTask PutApi(ApiName name, CancellationToken cancellationToken);
public delegate ValueTask<Option<ApiDto>> FindApiInformationFileDto(ApiName name, CancellationToken cancellationToken);
public delegate ValueTask<Option<(ApiSpecification Specification, BinaryData Contents)>> FindApiSpecificationContents(ApiName name, CancellationToken cancellationToken);
public delegate ValueTask CorrectApimRevisionNumber(ApiName name, ApiDto Dto, CancellationToken cancellationToken);
public delegate FrozenDictionary<ApiName, Func<CancellationToken, ValueTask<Option<ApiDto>>>> GetApiDtosInPreviousCommit();
public delegate ValueTask MakeApiRevisionCurrent(ApiName name, ApiRevisionNumber revisionNumber, CancellationToken cancellationToken);
public delegate ValueTask PutApiInApim(ApiName name, ApiDto dto, Option<(ApiSpecification.GraphQl Specification, BinaryData Contents)> graphQlSpecificationContentsOption, CancellationToken cancellationToken);
public delegate ValueTask DeleteApis(CancellationToken cancellationToken);
public delegate ValueTask DeleteApi(ApiName name, CancellationToken cancellationToken);
public delegate ValueTask DeleteApiFromApim(ApiName name, CancellationToken cancellationToken);
internal static class ApiModule
{
public static void ConfigurePutApis(IHostApplicationBuilder builder)
{
CommonModule.ConfigureGetPublisherFiles(builder);
ConfigureTryParseApiName(builder);
ConfigureIsApiNameInSourceControl(builder);
ConfigurePutApi(builder);
builder.Services.TryAddSingleton(GetPutApis);
}
private static PutApis GetPutApis(IServiceProvider provider)
{
var getPublisherFiles = provider.GetRequiredService<GetPublisherFiles>();
var tryParseName = provider.GetRequiredService<TryParseApiName>();
var isNameInSourceControl = provider.GetRequiredService<IsApiNameInSourceControl>();
var put = provider.GetRequiredService<PutApi>();
var activitySource = provider.GetRequiredService<ActivitySource>();
var logger = provider.GetRequiredService<ILogger>();
return async cancellationToken =>
{
using var _ = activitySource.StartActivity(nameof(PutApis));
logger.LogInformation("Putting APIs...");
await getPublisherFiles()
.Choose(tryParseName.Invoke)
.Where(isNameInSourceControl.Invoke)
.Distinct()
.IterParallel(put.Invoke, cancellationToken);
};
}
private static void ConfigureTryParseApiName(IHostApplicationBuilder builder)
{
AzureModule.ConfigureManagementServiceDirectory(builder);
builder.Services.TryAddSingleton(GetTryParseApiName);
}
private static TryParseApiName GetTryParseApiName(IServiceProvider provider)
{
var serviceDirectory = provider.GetRequiredService<ManagementServiceDirectory>();
return file => tryParseNameFromInformationFile(file) | tryParseNameFromSpecificationFile(file);
Option<ApiName> tryParseNameFromInformationFile(FileInfo file) =>
from informationFile in ApiInformationFile.TryParse(file, serviceDirectory)
select informationFile.Parent.Name;
Option<ApiName> tryParseNameFromSpecificationFile(FileInfo file) =>
from apiDirectory in ApiDirectory.TryParse(file.Directory, serviceDirectory)
where Common.SpecificationFileNames.Contains(file.Name)
select apiDirectory.Name;
}
private static void ConfigureIsApiNameInSourceControl(IHostApplicationBuilder builder)
{
CommonModule.ConfigureGetArtifactFiles(builder);
AzureModule.ConfigureManagementServiceDirectory(builder);
builder.Services.TryAddSingleton(GetIsApiNameInSourceControl);
}
private static IsApiNameInSourceControl GetIsApiNameInSourceControl(IServiceProvider provider)
{
var getArtifactFiles = provider.GetRequiredService<GetArtifactFiles>();
var serviceDirectory = provider.GetRequiredService<ManagementServiceDirectory>();
return name => doesInformationFileExist(name) || doesSpecificationFileExist(name);
bool doesInformationFileExist(ApiName name)
{
var artifactFiles = getArtifactFiles();
var informationFile = ApiInformationFile.From(name, serviceDirectory);
return artifactFiles.Contains(informationFile.ToFileInfo());
}
bool doesSpecificationFileExist(ApiName name)
{
var artifactFiles = getArtifactFiles();
var getFileInApiDirectory = ApiDirectory.From(name, serviceDirectory)
.ToDirectoryInfo()
.GetChildFile;
return Common.SpecificationFileNames
.Select(getFileInApiDirectory)
.Any(artifactFiles.Contains);
}
}
private static void ConfigurePutApi(IHostApplicationBuilder builder)
{
ConfigureFindApiInformationFileDto(builder);
ConfigureFindApiSpecificationContents(builder);
OverrideDtoModule.ConfigureOverrideDtoFactory(builder);
ConfigureCorrectApimRevisionNumber(builder);
ConfigurePutApiInApim(builder);
builder.Services.TryAddSingleton(GetPutApi);
}
private static PutApi GetPutApi(IServiceProvider provider)
{
var findInformationFileDto = provider.GetRequiredService<FindApiInformationFileDto>();
var findSpecificationContents = provider.GetRequiredService<FindApiSpecificationContents>();
var overrideDtoFactory = provider.GetRequiredService<OverrideDtoFactory>();
var correctRevisionNumber = provider.GetRequiredService<CorrectApimRevisionNumber>();
var putInApim = provider.GetRequiredService<PutApiInApim>();
var activitySource = provider.GetRequiredService<ActivitySource>();
var overrideDto = overrideDtoFactory.Create<ApiName, ApiDto>();
var taskDictionary = new ConcurrentDictionary<ApiName, AsyncLazy<Unit>>();
return putApi;
async ValueTask putApi(ApiName name, CancellationToken cancellationToken)
{
using var _ = activitySource.StartActivity(nameof(PutApi))
?.AddTag("api.name", name);
await taskDictionary.GetOrAdd(name,
name => new AsyncLazy<Unit>(async cancellationToken =>
{
await putApiInner(name, cancellationToken);
return Unit.Default;
}))
.WithCancellation(cancellationToken);
};
async ValueTask putApiInner(ApiName name, CancellationToken cancellationToken)
{
var informationFileDtoOption = await findInformationFileDto(name, cancellationToken);
await informationFileDtoOption.IterTask(async informationFileDto =>
{
await putCurrentRevision(name, informationFileDto, cancellationToken);
var specificationContentsOption = await findSpecificationContents(name, cancellationToken);
var dto = await tryGetDto(name, informationFileDto, specificationContentsOption, cancellationToken);
var graphQlSpecificationContentsOption = specificationContentsOption.Bind(specificationContents =>
{
var (specification, contents) = specificationContents;
return specification is ApiSpecification.GraphQl graphQl
? (graphQl, contents)
: Option<(ApiSpecification.GraphQl, BinaryData)>.None;
});
await putInApim(name, dto, graphQlSpecificationContentsOption, cancellationToken);
});
}
async ValueTask putCurrentRevision(ApiName name, ApiDto dto, CancellationToken cancellationToken)
{
if (ApiName.IsRevisioned(name))
{
var rootName = ApiName.GetRootName(name);
await putApi(rootName, cancellationToken);
}
else
{
await correctRevisionNumber(name, dto, cancellationToken);
}
}
async ValueTask<ApiDto> tryGetDto(ApiName name,
ApiDto informationFileDto,
Option<(ApiSpecification, BinaryData)> specificationContentsOption,
CancellationToken cancellationToken)
{
var dto = informationFileDto;
await specificationContentsOption.IterTask(async specificationContents =>
{
var (specification, contents) = specificationContents;
dto = await addSpecificationToDto(name, dto, specification, contents, cancellationToken);
});
dto = overrideDto(name, dto);
return dto;
}
static async ValueTask<ApiDto> addSpecificationToDto(ApiName name, ApiDto dto, ApiSpecification specification, BinaryData contents, CancellationToken cancellationToken) =>
dto with
{
Properties = dto.Properties with
{
Format = specification switch
{
ApiSpecification.Wsdl => "wsdl",
ApiSpecification.Wadl => "wadl-xml",
ApiSpecification.OpenApi openApi => (openApi.Format, openApi.Version) switch
{
(common.OpenApiFormat.Json, OpenApiVersion.V2) => "swagger-json",
(common.OpenApiFormat.Json, OpenApiVersion.V3) => "openapi+json",
(common.OpenApiFormat.Yaml, OpenApiVersion.V2) => "openapi",
(common.OpenApiFormat.Yaml, OpenApiVersion.V3) => "openapi",
_ => throw new InvalidOperationException($"Unsupported OpenAPI format '{openApi.Format}' and version '{openApi.Version}'.")
},
_ => dto.Properties.Format
},
// APIM does not support OpenAPI V2 YAML. Convert to V3 YAML if needed.
Value = specification switch
{
ApiSpecification.GraphQl => null,
ApiSpecification.OpenApi { Format: common.OpenApiFormat.Yaml, Version: OpenApiVersion.V2 } =>
await convertStreamToOpenApiV3Yaml(contents, $"Could not convert specification for API {name} to OpenAPIV3.", cancellationToken),
_ => contents.ToString()
}
}
};
static async ValueTask<string> convertStreamToOpenApiV3Yaml(BinaryData contents, string errorMessage, CancellationToken cancellationToken)
{
using var stream = contents.ToStream();
var readResult = await new OpenApiStreamReader().ReadAsync(stream, cancellationToken);
return readResult.OpenApiDiagnostic.Errors switch
{
[] => readResult.OpenApiDocument.Serialize(OpenApiSpecVersion.OpenApi3_0, Microsoft.OpenApi.OpenApiFormat.Yaml),
var errors => throw openApiErrorsToException(errorMessage, errors)
};
}
static OpenApiException openApiErrorsToException(string message, IEnumerable<OpenApiError> errors) =>
new($"{message}. Errors are: {Environment.NewLine}{string.Join(Environment.NewLine, errors)}");
}
private static void ConfigureFindApiSpecificationContents(IHostApplicationBuilder builder)
{
AzureModule.ConfigureManagementServiceDirectory(builder);
CommonModule.ConfigureTryGetFileContents(builder);
CommonModule.ConfigureGetArtifactFiles(builder);
builder.Services.TryAddSingleton(GetFindApiSpecificationContents);
}
private static FindApiSpecificationContents GetFindApiSpecificationContents(IServiceProvider provider)
{
var serviceDirectory = provider.GetRequiredService<ManagementServiceDirectory>();
var tryGetFileContents = provider.GetRequiredService<TryGetFileContents>();
var getArtifactFiles = provider.GetRequiredService<GetArtifactFiles>();
return async (name, cancellationToken) =>
await getSpecificationFiles(name)
.ToAsyncEnumerable()
.Choose(async file => await tryGetSpecificationContentsFromFile(file, cancellationToken))
.FirstOrNone(cancellationToken);
FrozenSet<FileInfo> getSpecificationFiles(ApiName name)
{
var apiDirectory = ApiDirectory.From(name, serviceDirectory);
var artifactFiles = getArtifactFiles();
return Common.SpecificationFileNames
.Select(apiDirectory.ToDirectoryInfo().GetChildFile)
.Where(artifactFiles.Contains)
.ToFrozenSet();
}
async ValueTask<Option<(ApiSpecification, BinaryData)>> tryGetSpecificationContentsFromFile(FileInfo file, CancellationToken cancellationToken)
{
var contentsOption = await tryGetFileContents(file, cancellationToken);
return await contentsOption.BindTask(async contents =>
{
var specificationFileOption = await tryParseSpecificationFile(file, contents, cancellationToken);
return from specificationFile in specificationFileOption
select (specificationFile.Specification, contents);
});
}
async ValueTask<Option<ApiSpecificationFile>> tryParseSpecificationFile(FileInfo file, BinaryData contents, CancellationToken cancellationToken) =>
await ApiSpecificationFile.TryParse(file,
getFileContents: _ => ValueTask.FromResult(contents),
serviceDirectory,
cancellationToken);
}
private static void ConfigureFindApiInformationFileDto(IHostApplicationBuilder builder)
{
AzureModule.ConfigureManagementServiceDirectory(builder);
CommonModule.ConfigureTryGetFileContents(builder);
builder.Services.TryAddSingleton(GetFindApiInformationFileDto);
}
private static FindApiInformationFileDto GetFindApiInformationFileDto(IServiceProvider provider)
{
var serviceDirectory = provider.GetRequiredService<ManagementServiceDirectory>();
var tryGetFileContents = provider.GetRequiredService<TryGetFileContents>();
return async (name, cancellationToken) =>
{
var informationFile = ApiInformationFile.From(name, serviceDirectory);
var contentsOption = await tryGetFileContents(informationFile.ToFileInfo(), cancellationToken);
return from contents in contentsOption
select contents.ToObjectFromJson<ApiDto>();
};
}
private static void ConfigureCorrectApimRevisionNumber(IHostApplicationBuilder builder)
{
ConfigureGetApiDtosInPreviousCommit(builder);
ConfigurePutApiInApim(builder);
ConfigureMakeApiRevisionCurrent(builder);
ConfigureIsApiNameInSourceControl(builder);
ConfigureDeleteApiFromApim(builder);
builder.Services.TryAddSingleton(GetCorrectApimRevisionNumber);
}
private static CorrectApimRevisionNumber GetCorrectApimRevisionNumber(IServiceProvider provider)
{
var getPreviousCommitDtos = provider.GetRequiredService<GetApiDtosInPreviousCommit>();
var putApiInApim = provider.GetRequiredService<PutApiInApim>();
var makeApiRevisionCurrent = provider.GetRequiredService<MakeApiRevisionCurrent>();
var isNameInSourceControl = provider.GetRequiredService<IsApiNameInSourceControl>();
var deleteApiFromApim = provider.GetRequiredService<DeleteApiFromApim>();
var logger = provider.GetRequiredService<ILogger>();
return async (name, dto, cancellationToken) =>
{
if (ApiName.IsRevisioned(name))
{
return;
}
var previousRevisionNumberOption = await tryGetPreviousRevisionNumber(name, cancellationToken);
await previousRevisionNumberOption.IterTask(async previousRevisionNumber =>
{
var currentRevisionNumber = Common.GetRevisionNumber(dto);
await setApimCurrentRevisionNumber(name, currentRevisionNumber, previousRevisionNumber, cancellationToken);
});
};
async ValueTask<Option<ApiRevisionNumber>> tryGetPreviousRevisionNumber(ApiName name, CancellationToken cancellationToken) =>
await getPreviousCommitDtos()
.Find(name)
.BindTask(async getDto =>
{
var dtoOption = await getDto(cancellationToken);
return from dto in dtoOption
select Common.GetRevisionNumber(dto);
});
async ValueTask setApimCurrentRevisionNumber(ApiName name, ApiRevisionNumber newRevisionNumber, ApiRevisionNumber existingRevisionNumber, CancellationToken cancellationToken)
{
if (newRevisionNumber == existingRevisionNumber)
{
return;
}
logger.LogInformation("Changing current revision on {ApiName} from {RevisionNumber} to {RevisionNumber}...", name, existingRevisionNumber, newRevisionNumber);
await putRevision(name, newRevisionNumber, existingRevisionNumber, cancellationToken);
await makeApiRevisionCurrent(name, newRevisionNumber, cancellationToken);
await deleteOldRevision(name, existingRevisionNumber, cancellationToken);
}
async ValueTask putRevision(ApiName name, ApiRevisionNumber revisionNumber, ApiRevisionNumber existingRevisionNumber, CancellationToken cancellationToken)
{
var dto = new ApiDto
{
Properties = new ApiDto.ApiCreateOrUpdateProperties
{
ApiRevision = revisionNumber.ToString(),
SourceApiId = $"/apis/{ApiName.GetRevisionedName(name, existingRevisionNumber)}"
}
};
await putApiInApim(name, dto, Option<(ApiSpecification.GraphQl, BinaryData)>.None, cancellationToken);
}
/// <summary>
/// If the old revision is no longer in source control, delete it from APIM. Handles this scenario:
/// 1. Dev and prod APIM both have apiA with current revision 1. Artifacts folder has /apis/apiA/apiInformation.json with revision 1.
/// 2. User changes the current revision in dev APIM from 1 to 2.
/// 3. User deletes revision 1 from dev APIM, as it's no longer needed.
/// 4. User runs extractor for dev APIM. Artifacts folder has /apis/apiA/apiInformation.json with revision 2.
/// 5. User runs publisher to prod APIM. The only changed artifact will be an update in apiInformation.json to revision 2, so we will create revision 2 in prod and make it current.
///
/// If we do nothing else, dev and prod will be inconsistent as prod will still have the revision 1 API. There was nothing in Git that told the publisher to delete revision 1.
/// </summary>
async ValueTask deleteOldRevision(ApiName name, ApiRevisionNumber oldRevisionNumber, CancellationToken cancellationToken)
{
var revisionedName = ApiName.GetRevisionedName(name, oldRevisionNumber);
if (isNameInSourceControl(revisionedName))
{
return;
}
logger.LogInformation("Deleting old revision {RevisionNumber} of {ApiName}...", oldRevisionNumber, name);
await deleteApiFromApim(revisionedName, cancellationToken);
}
}
private static void ConfigureGetApiDtosInPreviousCommit(IHostApplicationBuilder builder)
{
CommonModule.ConfigureGetArtifactsInPreviousCommit(builder);
AzureModule.ConfigureManagementServiceDirectory(builder);
builder.Services.AddMemoryCache();
builder.Services.TryAddSingleton(GetApiDtosInPreviousCommit);
}
private static GetApiDtosInPreviousCommit GetApiDtosInPreviousCommit(IServiceProvider provider)
{
var getArtifactsInPreviousCommit = provider.GetRequiredService<GetArtifactsInPreviousCommit>();
var serviceDirectory = provider.GetRequiredService<ManagementServiceDirectory>();
var cache = provider.GetRequiredService<IMemoryCache>();
var cacheKey = Guid.NewGuid().ToString();
return () =>
cache.GetOrCreate(cacheKey, _ => getDtos())!;
FrozenDictionary<ApiName, Func<CancellationToken, ValueTask<Option<ApiDto>>>> getDtos() =>
getArtifactsInPreviousCommit()
.Choose(kvp => from apiName in tryGetNameFromInformationFile(kvp.Key)
select (apiName, tryGetDto(kvp.Value)))
.ToFrozenDictionary();
Option<ApiName> tryGetNameFromInformationFile(FileInfo file) =>
from informationFile in ApiInformationFile.TryParse(file, serviceDirectory)
select informationFile.Parent.Name;
static Func<CancellationToken, ValueTask<Option<ApiDto>>> tryGetDto(Func<CancellationToken, ValueTask<Option<BinaryData>>> tryGetContents) =>
async cancellationToken =>
{
var contentsOption = await tryGetContents(cancellationToken);
return from contents in contentsOption
select contents.ToObjectFromJson<ApiDto>();
};
}
private static void ConfigureMakeApiRevisionCurrent(IHostApplicationBuilder builder)
{
ApiReleaseModule.ConfigurePutApiReleaseInApim(builder);
ApiReleaseModule.ConfigureDeleteApiReleaseFromApim(builder);
builder.Services.TryAddSingleton(GetMakeApiRevisionCurrent);
}
private static MakeApiRevisionCurrent GetMakeApiRevisionCurrent(IServiceProvider provider)
{
var putRelease = provider.GetRequiredService<PutApiReleaseInApim>();
var deleteRelease = provider.GetRequiredService<DeleteApiReleaseFromApim>();
return async (name, revisionNumber, cancellationToken) =>
{
var revisionedName = ApiName.GetRevisionedName(name, revisionNumber);
var releaseName = ApiReleaseName.From("apiops-set-current");
var releaseDto = new ApiReleaseDto
{
Properties = new ApiReleaseDto.ApiReleaseContract
{
ApiId = $"/apis/{revisionedName}",
Notes = "Setting current revision for ApiOps"
}
};
await putRelease(releaseName, releaseDto, name, cancellationToken);
await deleteRelease(releaseName, name, cancellationToken);
};
}
private static void ConfigurePutApiInApim(IHostApplicationBuilder builder)
{
AzureModule.ConfigureManagementServiceUri(builder);
AzureModule.ConfigureHttpPipeline(builder);
builder.Services.TryAddSingleton(GetPutApiInApim);
}
private static PutApiInApim GetPutApiInApim(IServiceProvider provider)
{
var serviceUri = provider.GetRequiredService<ManagementServiceUri>();
var pipeline = provider.GetRequiredService<HttpPipeline>();
var logger = provider.GetRequiredService<ILogger>();
return async (name, dto, graphQlSpecificationContentsOption, cancellationToken) =>
{
logger.LogInformation("Putting API {ApiName}...", name);
var revisionNumber = Common.GetRevisionNumber(dto);
var uri = getRevisionedUri(name, revisionNumber);
// APIM sometimes fails revisions if isCurrent is set to true.
var dtoWithoutIsCurrent = dto with { Properties = dto.Properties with { IsCurrent = null } };
await uri.PutDto(dtoWithoutIsCurrent, pipeline, cancellationToken);
// Put GraphQl schema
await graphQlSpecificationContentsOption.IterTask(async graphQlSpecificationContents =>
{
var (_, contents) = graphQlSpecificationContents;
await uri.PutGraphQlSchema(contents, pipeline, cancellationToken);
});
};
ApiUri getRevisionedUri(ApiName name, ApiRevisionNumber revisionNumber)
{
var revisionedApiName = ApiName.GetRevisionedName(name, revisionNumber);
return ApiUri.From(revisionedApiName, serviceUri);
}
}
public static void ConfigureDeleteApis(IHostApplicationBuilder builder)
{
CommonModule.ConfigureGetPublisherFiles(builder);
ConfigureTryParseApiName(builder);
ConfigureIsApiNameInSourceControl(builder);
ConfigureDeleteApi(builder);
builder.Services.TryAddSingleton(GetDeleteApis);
}
private static DeleteApis GetDeleteApis(IServiceProvider provider)
{
var getPublisherFiles = provider.GetRequiredService<GetPublisherFiles>();
var tryParseName = provider.GetRequiredService<TryParseApiName>();
var isNameInSourceControl = provider.GetRequiredService<IsApiNameInSourceControl>();
var delete = provider.GetRequiredService<DeleteApi>();
var activitySource = provider.GetRequiredService<ActivitySource>();
var logger = provider.GetRequiredService<ILogger>();
return async cancellationToken =>
{
using var _ = activitySource.StartActivity(nameof(DeleteApis));
logger.LogInformation("Deleting APIs...");
await getPublisherFiles()
.Choose(tryParseName.Invoke)
.Where(name => isNameInSourceControl(name) is false)
.Distinct()
.IterParallel(delete.Invoke, cancellationToken);
};
}
private static void ConfigureDeleteApi(IHostApplicationBuilder builder)
{
ConfigureFindApiInformationFileDto(builder);
ConfigureDeleteApiFromApim(builder);
builder.Services.TryAddSingleton(GetDeleteApi);
}
private static DeleteApi GetDeleteApi(IServiceProvider provider)
{
var findDto = provider.GetRequiredService<FindApiInformationFileDto>();
var deleteFromApim = provider.GetRequiredService<DeleteApiFromApim>();
var activitySource = provider.GetRequiredService<ActivitySource>();
var logger = provider.GetRequiredService<ILogger>();
var taskDictionary = new ConcurrentDictionary<ApiName, AsyncLazy<Unit>>();
return deleteApi;
async ValueTask deleteApi(ApiName name, CancellationToken cancellationToken)
{
using var _ = activitySource.StartActivity(nameof(DeleteApi))
?.AddTag("api.name", name);
await taskDictionary.GetOrAdd(name,
name => new AsyncLazy<Unit>(async cancellationToken =>
{
await deleteApiInner(name, cancellationToken);
return Unit.Default;
}))
.WithCancellation(cancellationToken);
};
async ValueTask deleteApiInner(ApiName name, CancellationToken cancellationToken) =>
await ApiName.TryParseRevisionedName(name)
.Map(async api => await processRevisionedApi(api.RootName, api.RevisionNumber, cancellationToken))
.IfLeft(async _ => await processRootApi(name, cancellationToken));
async ValueTask processRootApi(ApiName name, CancellationToken cancellationToken) =>
await deleteFromApim(name, cancellationToken);
async ValueTask processRevisionedApi(ApiName name, ApiRevisionNumber revisionNumber, CancellationToken cancellationToken)
{
var rootName = ApiName.GetRootName(name);
var currentRevisionNumberOption = await tryGetRevisionNumberInSourceControl(rootName, cancellationToken);
await currentRevisionNumberOption.Match(// If the current revision in source control has a different revision number, delete this revision.
// We don't want to delete a revision if it was just made current. For instance:
// 1. Dev has apiA with revision 1 (current) and revision 2. Artifacts folder has:
// - /apis/apiA/apiInformation.json with revision 1 as current
// - /apis/apiA;rev=2/apiInformation.json
// 2. User makes revision 2 current in dev APIM.
// 3. User runs extractor for dev APIM. Artifacts folder has:
// - /apis/apiA/apiInformation.json with revision 2 as current
// - /apis/apiA;rev=1/apiInformation.json
// - /apis/apiA;rev=2 folder gets deleted.
// 4. User runs publisher to prod APIM. We don't want to handle the deletion of folder /apis/apiA;rev=2, as it's the current revision.
async currentRevisionNumber =>
{
if (currentRevisionNumber != revisionNumber)
{
await deleteFromApim(name, cancellationToken);
}
},
// If there is no current revision in source control, process the root API deletion
async () => await deleteApi(rootName, cancellationToken));
}
async ValueTask<Option<ApiRevisionNumber>> tryGetRevisionNumberInSourceControl(ApiName name, CancellationToken cancellationToken)
{
var dtoOption = await findDto(name, cancellationToken);
return dtoOption.Map(Common.GetRevisionNumber);
}
}
private static void ConfigureDeleteApiFromApim(IHostApplicationBuilder builder)
{
AzureModule.ConfigureManagementServiceUri(builder);
AzureModule.ConfigureHttpPipeline(builder);
builder.Services.TryAddSingleton(GetDeleteApiFromApim);
}
private static DeleteApiFromApim GetDeleteApiFromApim(IServiceProvider provider)
{
var serviceUri = provider.GetRequiredService<ManagementServiceUri>();
var pipeline = provider.GetRequiredService<HttpPipeline>();
var logger = provider.GetRequiredService<ILogger>();
return async (name, cancellationToken) =>
{
logger.LogInformation("Deleting API {ApiName}...", name);
var apiUri = ApiUri.From(name, serviceUri);
await (ApiName.IsRevisioned(name)
? apiUri.Delete(pipeline, cancellationToken)
: apiUri.DeleteAllRevisions(pipeline, cancellationToken));
};
}
}
file static class Common
{
public static FrozenSet<string> SpecificationFileNames { get; } =
new[]
{
WadlSpecificationFile.Name,
WsdlSpecificationFile.Name,
GraphQlSpecificationFile.Name,
JsonOpenApiSpecificationFile.Name,
YamlOpenApiSpecificationFile.Name
}.ToFrozenSet();
public static ApiRevisionNumber GetRevisionNumber(ApiDto dto) =>
ApiRevisionNumber.TryFrom(dto.Properties.ApiRevision)
.IfNone(() => ApiRevisionNumber.From(1));
}