tools/code/publisher/WorkspaceApi.cs (556 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.Diagnostics; using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; namespace publisher; public delegate ValueTask PutWorkspaceApis(CancellationToken cancellationToken); public delegate ValueTask DeleteWorkspaceApis(CancellationToken cancellationToken); public delegate Option<(ApiName Name, WorkspaceName WorkspaceName)> TryParseWorkspaceApiName(FileInfo file); public delegate bool IsWorkspaceApiNameInSourceControl(ApiName name, WorkspaceName workspaceName); public delegate ValueTask PutWorkspaceApi(ApiName name, WorkspaceName workspaceName, CancellationToken cancellationToken); public delegate ValueTask<Option<WorkspaceApiDto>> FindWorkspaceApiDto(ApiName name, WorkspaceName workspaceName, CancellationToken cancellationToken); public delegate ValueTask<Option<(ApiSpecification Specification, BinaryData Contents)>> FindWorkspaceApiSpecificationContents(ApiName name, WorkspaceName workspaceName, CancellationToken cancellationToken); public delegate ValueTask CorrectWorkspaceApimRevisionNumber(ApiName name, WorkspaceApiDto Dto, WorkspaceName workspaceName, CancellationToken cancellationToken); public delegate FrozenDictionary<(ApiName, WorkspaceName), Func<CancellationToken, ValueTask<Option<WorkspaceApiDto>>>> GetWorkspaceApiDtosInPreviousCommit(); public delegate ValueTask MakeWorkspaceApiRevisionCurrent(ApiName name, ApiRevisionNumber revisionNumber, WorkspaceName workspaceName, CancellationToken cancellationToken); public delegate ValueTask PutWorkspaceApiInApim(ApiName name, WorkspaceApiDto dto, Option<(ApiSpecification.GraphQl Specification, BinaryData Contents)> graphQlSpecificationContentsOption, WorkspaceName workspaceName, CancellationToken cancellationToken); public delegate ValueTask DeleteWorkspaceApi(ApiName name, WorkspaceName workspaceName, CancellationToken cancellationToken); public delegate ValueTask DeleteWorkspaceApiFromApim(ApiName name, WorkspaceName workspaceName, CancellationToken cancellationToken); internal static class WorkspaceApiModule { public static void ConfigurePutWorkspaceApis(IHostApplicationBuilder builder) { CommonModule.ConfigureGetPublisherFiles(builder); ConfigureTryParseWorkspaceApiName(builder); ConfigureIsWorkspaceApiNameInSourceControl(builder); ConfigurePutWorkspaceApi(builder); builder.Services.TryAddSingleton(GetPutWorkspaceApis); } private static PutWorkspaceApis GetPutWorkspaceApis(IServiceProvider provider) { var getPublisherFiles = provider.GetRequiredService<GetPublisherFiles>(); var tryParseName = provider.GetRequiredService<TryParseWorkspaceApiName>(); var isNameInSourceControl = provider.GetRequiredService<IsWorkspaceApiNameInSourceControl>(); var put = provider.GetRequiredService<PutWorkspaceApi>(); var activitySource = provider.GetRequiredService<ActivitySource>(); var logger = provider.GetRequiredService<ILogger>(); return async cancellationToken => { using var _ = activitySource.StartActivity(nameof(PutWorkspaceApis)); logger.LogInformation("Putting workspace APIs..."); await getPublisherFiles() .Choose(tryParseName.Invoke) .Where(resource => isNameInSourceControl(resource.Name, resource.WorkspaceName)) .Distinct() .IterParallel(put.Invoke, cancellationToken); }; } private static void ConfigureTryParseWorkspaceApiName(IHostApplicationBuilder builder) { AzureModule.ConfigureManagementServiceDirectory(builder); builder.Services.TryAddSingleton(GetTryParseWorkspaceApiName); } private static TryParseWorkspaceApiName GetTryParseWorkspaceApiName(IServiceProvider provider) { var serviceDirectory = provider.GetRequiredService<ManagementServiceDirectory>(); return file => from informationFile in WorkspaceApiInformationFile.TryParse(file, serviceDirectory) select (informationFile.Parent.Name, informationFile.Parent.Parent.Parent.Name); } private static void ConfigureIsWorkspaceApiNameInSourceControl(IHostApplicationBuilder builder) { CommonModule.ConfigureGetArtifactFiles(builder); AzureModule.ConfigureManagementServiceDirectory(builder); builder.Services.TryAddSingleton(GetIsWorkspaceApiNameInSourceControl); } private static IsWorkspaceApiNameInSourceControl GetIsWorkspaceApiNameInSourceControl(IServiceProvider provider) { var getArtifactFiles = provider.GetRequiredService<GetArtifactFiles>(); var serviceDirectory = provider.GetRequiredService<ManagementServiceDirectory>(); return (name, workspaceName) => doesInformationFileExist(name, workspaceName) || doesSpecificationFileExist(name, workspaceName); bool doesInformationFileExist(ApiName name, WorkspaceName workspaceName) { var artifactFiles = getArtifactFiles(); var informationFile = WorkspaceApiInformationFile.From(name, workspaceName, serviceDirectory); return artifactFiles.Contains(informationFile.ToFileInfo()); } bool doesSpecificationFileExist(ApiName name, WorkspaceName workspaceName) { var artifactFiles = getArtifactFiles(); var getFileInApiDirectory = WorkspaceApiDirectory.From(name, workspaceName, serviceDirectory) .ToDirectoryInfo() .GetChildFile; return Common.SpecificationFileNames .Select(getFileInApiDirectory) .Any(artifactFiles.Contains); } } private static void ConfigurePutWorkspaceApi(IHostApplicationBuilder builder) { ConfigureFindWorkspaceApiDto(builder); ConfigureFindWorkspaceApiSpecificationContents(builder); ConfigureCorrectWorkspaceApimRevisionNumber(builder); ConfigurePutWorkspaceApiInApim(builder); builder.Services.TryAddSingleton(GetPutWorkspaceApi); } private static PutWorkspaceApi GetPutWorkspaceApi(IServiceProvider provider) { var findDto = provider.GetRequiredService<FindWorkspaceApiDto>(); var findSpecificationContents = provider.GetRequiredService<FindWorkspaceApiSpecificationContents>(); var correctRevisionNumber = provider.GetRequiredService<CorrectWorkspaceApimRevisionNumber>(); var putInApim = provider.GetRequiredService<PutWorkspaceApiInApim>(); var activitySource = provider.GetRequiredService<ActivitySource>(); var taskDictionary = new ConcurrentDictionary<(ApiName, WorkspaceName), AsyncLazy<Unit>>(); return putApi; async ValueTask putApi(ApiName name, WorkspaceName workspaceName, CancellationToken cancellationToken) { using var _ = activitySource.StartActivity(nameof(PutWorkspaceApi)) ?.AddTag("workspace.name", workspaceName) ?.AddTag("workspace_api.name", name); await taskDictionary.GetOrAdd((name, workspaceName), (pair) => new AsyncLazy<Unit>(async cancellationToken => { var (name, workspaceName) = pair; await putApiInner(name, workspaceName, cancellationToken); return Unit.Default; })) .WithCancellation(cancellationToken); }; async ValueTask putApiInner(ApiName name, WorkspaceName workspaceName, CancellationToken cancellationToken) { var informationFileDtoOption = await findDto(name, workspaceName, cancellationToken); await informationFileDtoOption.IterTask(async informationFileDto => { await putCurrentRevision(name, informationFileDto, workspaceName, cancellationToken); var specificationContentsOption = await findSpecificationContents(name, workspaceName, 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, workspaceName, cancellationToken); }); } async ValueTask putCurrentRevision(ApiName name, WorkspaceApiDto dto, WorkspaceName workspaceName, CancellationToken cancellationToken) { if (ApiName.IsRevisioned(name)) { var rootName = ApiName.GetRootName(name); await putApi(rootName, workspaceName, cancellationToken); } else { await correctRevisionNumber(name, dto, workspaceName, cancellationToken); } } async ValueTask<WorkspaceApiDto> tryGetDto(ApiName name, WorkspaceApiDto 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); }); return dto; } static async ValueTask<WorkspaceApiDto> addSpecificationToDto(ApiName name, WorkspaceApiDto 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 ConfigureFindWorkspaceApiDto(IHostApplicationBuilder builder) { AzureModule.ConfigureManagementServiceDirectory(builder); CommonModule.ConfigureTryGetFileContents(builder); builder.Services.TryAddSingleton(GetFindWorkspaceApiDto); } private static FindWorkspaceApiDto GetFindWorkspaceApiDto(IServiceProvider provider) { var serviceDirectory = provider.GetRequiredService<ManagementServiceDirectory>(); var tryGetFileContents = provider.GetRequiredService<TryGetFileContents>(); return async (name, workspaceName, cancellationToken) => { var informationFile = WorkspaceApiInformationFile.From(name, workspaceName, serviceDirectory); var contentsOption = await tryGetFileContents(informationFile.ToFileInfo(), cancellationToken); return from contents in contentsOption select contents.ToObjectFromJson<WorkspaceApiDto>(); }; } private static void ConfigureFindWorkspaceApiSpecificationContents(IHostApplicationBuilder builder) { AzureModule.ConfigureManagementServiceDirectory(builder); CommonModule.ConfigureTryGetFileContents(builder); CommonModule.ConfigureGetArtifactFiles(builder); builder.Services.TryAddSingleton(GetFindWorkspaceApiSpecificationContents); } private static FindWorkspaceApiSpecificationContents GetFindWorkspaceApiSpecificationContents(IServiceProvider provider) { var serviceDirectory = provider.GetRequiredService<ManagementServiceDirectory>(); var tryGetFileContents = provider.GetRequiredService<TryGetFileContents>(); var getArtifactFiles = provider.GetRequiredService<GetArtifactFiles>(); return async (name, workspaceName, cancellationToken) => await getSpecificationFiles(name, workspaceName) .ToAsyncEnumerable() .Choose(async file => await tryGetSpecificationContentsFromFile(file, cancellationToken)) .FirstOrNone(cancellationToken); FrozenSet<FileInfo> getSpecificationFiles(ApiName name, WorkspaceName workspaceName) { var apiDirectory = WorkspaceApiDirectory.From(name, workspaceName, 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 ConfigureCorrectWorkspaceApimRevisionNumber(IHostApplicationBuilder builder) { ConfigureGetWorkspaceApiDtosInPreviousCommit(builder); ConfigurePutWorkspaceApiInApim(builder); ConfigureMakeWorkspaceApiRevisionCurrent(builder); ConfigureIsWorkspaceApiNameInSourceControl(builder); ConfigureDeleteWorkspaceApiFromApim(builder); builder.Services.TryAddSingleton(GetCorrectWorkspaceApimRevisionNumber); } private static CorrectWorkspaceApimRevisionNumber GetCorrectWorkspaceApimRevisionNumber(IServiceProvider provider) { var getPreviousCommitDtos = provider.GetRequiredService<GetWorkspaceApiDtosInPreviousCommit>(); var putApiInApim = provider.GetRequiredService<PutWorkspaceApiInApim>(); var makeApiRevisionCurrent = provider.GetRequiredService<MakeWorkspaceApiRevisionCurrent>(); var isNameInSourceControl = provider.GetRequiredService<IsWorkspaceApiNameInSourceControl>(); var deleteApiFromApim = provider.GetRequiredService<DeleteWorkspaceApiFromApim>(); var logger = provider.GetRequiredService<ILogger>(); return async (name, dto, workspaceName, cancellationToken) => { if (ApiName.IsRevisioned(name)) { return; } var previousRevisionNumberOption = await tryGetPreviousRevisionNumber(name, workspaceName, cancellationToken); await previousRevisionNumberOption.IterTask(async previousRevisionNumber => { var currentRevisionNumber = Common.GetRevisionNumber(dto); await setApimCurrentRevisionNumber(name, currentRevisionNumber, previousRevisionNumber, workspaceName, cancellationToken); }); }; async ValueTask<Option<ApiRevisionNumber>> tryGetPreviousRevisionNumber(ApiName name, WorkspaceName workspaceName, CancellationToken cancellationToken) => await getPreviousCommitDtos() .Find((name, workspaceName)) .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, WorkspaceName workspaceName, CancellationToken cancellationToken) { if (newRevisionNumber == existingRevisionNumber) { return; } logger.LogInformation("Changing current revision on {ApiName} in workspace {WorkspaceName} from {RevisionNumber} to {RevisionNumber}...", name, workspaceName, existingRevisionNumber, newRevisionNumber); await putRevision(name, newRevisionNumber, existingRevisionNumber, workspaceName, cancellationToken); await makeApiRevisionCurrent(name, newRevisionNumber, workspaceName, cancellationToken); await deleteOldRevision(name, existingRevisionNumber, workspaceName, cancellationToken); } async ValueTask putRevision(ApiName name, ApiRevisionNumber revisionNumber, ApiRevisionNumber existingRevisionNumber, WorkspaceName workspaceName, CancellationToken cancellationToken) { var dto = new WorkspaceApiDto { Properties = new WorkspaceApiDto.ApiCreateOrUpdateProperties { ApiRevision = revisionNumber.ToString(), SourceApiId = $"/apis/{ApiName.GetRevisionedName(name, existingRevisionNumber)}" } }; await putApiInApim(name, dto, Option<(ApiSpecification.GraphQl, BinaryData)>.None, workspaceName, 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, WorkspaceName workspaceName, CancellationToken cancellationToken) { var revisionedName = ApiName.GetRevisionedName(name, oldRevisionNumber); if (isNameInSourceControl(revisionedName, workspaceName)) { return; } logger.LogInformation("Deleting old revision {RevisionNumber} of {ApiName} in workspace {WorkspaceName}...", oldRevisionNumber, name, workspaceName); await deleteApiFromApim(revisionedName, workspaceName, cancellationToken); } } private static void ConfigureGetWorkspaceApiDtosInPreviousCommit(IHostApplicationBuilder builder) { CommonModule.ConfigureGetArtifactsInPreviousCommit(builder); AzureModule.ConfigureManagementServiceDirectory(builder); builder.Services.AddMemoryCache(); builder.Services.TryAddSingleton(GetWorkspaceApiDtosInPreviousCommit); } private static GetWorkspaceApiDtosInPreviousCommit GetWorkspaceApiDtosInPreviousCommit(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, WorkspaceName), Func<CancellationToken, ValueTask<Option<WorkspaceApiDto>>>> getDtos() => getArtifactsInPreviousCommit() .Choose(kvp => from apiName in tryGetNameFromInformationFile(kvp.Key) select (apiName, tryGetDto(kvp.Value))) .ToFrozenDictionary(); Option<(ApiName, WorkspaceName)> tryGetNameFromInformationFile(FileInfo file) => from informationFile in WorkspaceApiInformationFile.TryParse(file, serviceDirectory) select (informationFile.Parent.Name, informationFile.Parent.Parent.Parent.Name); static Func<CancellationToken, ValueTask<Option<WorkspaceApiDto>>> tryGetDto(Func<CancellationToken, ValueTask<Option<BinaryData>>> tryGetContents) => async cancellationToken => { var contentsOption = await tryGetContents(cancellationToken); return from contents in contentsOption select contents.ToObjectFromJson<WorkspaceApiDto>(); }; } private static void ConfigureMakeWorkspaceApiRevisionCurrent(IHostApplicationBuilder builder) { WorkspaceApiReleaseModule.ConfigurePutWorkspaceApiReleaseInApim(builder); WorkspaceApiReleaseModule.ConfigureDeleteWorkspaceApiReleaseFromApim(builder); builder.Services.TryAddSingleton(GetMakeWorkspaceApiRevisionCurrent); } private static MakeWorkspaceApiRevisionCurrent GetMakeWorkspaceApiRevisionCurrent(IServiceProvider provider) { var putRelease = provider.GetRequiredService<PutWorkspaceApiReleaseInApim>(); var deleteRelease = provider.GetRequiredService<DeleteWorkspaceApiReleaseFromApim>(); return async (name, revisionNumber, workspaceName, cancellationToken) => { var revisionedName = ApiName.GetRevisionedName(name, revisionNumber); var releaseName = WorkspaceApiReleaseName.From("apiops-set-current"); var releaseDto = new WorkspaceApiReleaseDto { Properties = new WorkspaceApiReleaseDto.ApiReleaseContract { ApiId = $"/apis/{revisionedName}", Notes = "Setting current revision for ApiOps" } }; await putRelease(releaseName, releaseDto, name, workspaceName, cancellationToken); await deleteRelease(releaseName, name, workspaceName, cancellationToken); }; } private static void ConfigurePutWorkspaceApiInApim(IHostApplicationBuilder builder) { AzureModule.ConfigureManagementServiceUri(builder); AzureModule.ConfigureHttpPipeline(builder); builder.Services.TryAddSingleton(GetPutWorkspaceApiInApim); } private static PutWorkspaceApiInApim GetPutWorkspaceApiInApim(IServiceProvider provider) { var serviceUri = provider.GetRequiredService<ManagementServiceUri>(); var pipeline = provider.GetRequiredService<HttpPipeline>(); var logger = provider.GetRequiredService<ILogger>(); return async (name, dto, graphQlSpecificationContentsOption, workspaceName, cancellationToken) => { logger.LogInformation("Adding API {ApiName} to workspace {WorkspaceName}...", name, workspaceName); var revisionNumber = Common.GetRevisionNumber(dto); var uri = getRevisionedUri(name, workspaceName, 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); }); }; WorkspaceApiUri getRevisionedUri(ApiName name, WorkspaceName workspaceName, ApiRevisionNumber revisionNumber) { var revisionedApiName = ApiName.GetRevisionedName(name, revisionNumber); return WorkspaceApiUri.From(revisionedApiName, workspaceName, serviceUri); } } public static void ConfigureDeleteWorkspaceApis(IHostApplicationBuilder builder) { CommonModule.ConfigureGetPublisherFiles(builder); ConfigureTryParseWorkspaceApiName(builder); ConfigureIsWorkspaceApiNameInSourceControl(builder); ConfigureDeleteWorkspaceApi(builder); builder.Services.TryAddSingleton(GetDeleteWorkspaceApis); } private static DeleteWorkspaceApis GetDeleteWorkspaceApis(IServiceProvider provider) { var getPublisherFiles = provider.GetRequiredService<GetPublisherFiles>(); var tryParseName = provider.GetRequiredService<TryParseWorkspaceApiName>(); var isNameInSourceControl = provider.GetRequiredService<IsWorkspaceApiNameInSourceControl>(); var delete = provider.GetRequiredService<DeleteWorkspaceApi>(); var activitySource = provider.GetRequiredService<ActivitySource>(); var logger = provider.GetRequiredService<ILogger>(); return async cancellationToken => { using var _ = activitySource.StartActivity(nameof(DeleteWorkspaceApis)); logger.LogInformation("Deleting workspace APIs..."); await getPublisherFiles() .Choose(tryParseName.Invoke) .Where(resource => isNameInSourceControl(resource.Name, resource.WorkspaceName) is false) .Distinct() .IterParallel(delete.Invoke, cancellationToken); }; } private static void ConfigureDeleteWorkspaceApi(IHostApplicationBuilder builder) { ConfigureFindWorkspaceApiDto(builder); ConfigureDeleteWorkspaceApiFromApim(builder); builder.Services.TryAddSingleton(GetDeleteWorkspaceApi); } private static DeleteWorkspaceApi GetDeleteWorkspaceApi(IServiceProvider provider) { var findDto = provider.GetRequiredService<FindWorkspaceApiDto>(); var deleteFromApim = provider.GetRequiredService<DeleteWorkspaceApiFromApim>(); var activitySource = provider.GetRequiredService<ActivitySource>(); var logger = provider.GetRequiredService<ILogger>(); var taskDictionary = new ConcurrentDictionary<(ApiName, WorkspaceName), AsyncLazy<Unit>>(); return async (name, workspaceName, cancellationToken) => { using var _ = activitySource.StartActivity(nameof(DeleteWorkspaceApi)) ?.AddTag("workspace.name", workspaceName) ?.AddTag("workspace_api.name", name); await taskDictionary.GetOrAdd((name, workspaceName), pair => new AsyncLazy<Unit>(async cancellationToken => { var (name, workspaceName) = pair; await deleteApiInner(name, workspaceName, cancellationToken); return Unit.Default; })) .WithCancellation(cancellationToken); }; async ValueTask deleteApiInner(ApiName name, WorkspaceName workspaceName, CancellationToken cancellationToken) { if (await isApiRevisionNumberCurrentInSourceControl(name, workspaceName, cancellationToken)) { logger.LogInformation("API {ApiName} in workspace {WorkspaceName} is the current revision in source control. Skipping deletion...", name, workspaceName); return; } await deleteFromApim(name, workspaceName, cancellationToken); } /// <summary> /// 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 ValueTask<bool> isApiRevisionNumberCurrentInSourceControl(ApiName name, WorkspaceName workspaceName, CancellationToken cancellationToken) => await ApiName.TryParseRevisionedName(name) .Match(async _ => await ValueTask.FromResult(false), async api => { var (rootName, revisionNumber) = api; var sourceControlRevisionNumberOption = await tryGetRevisionNumberInSourceControl(rootName, workspaceName, cancellationToken); return sourceControlRevisionNumberOption .Map(sourceControlRevisionNumber => sourceControlRevisionNumber == revisionNumber) .IfNone(false); }); async ValueTask<Option<ApiRevisionNumber>> tryGetRevisionNumberInSourceControl(ApiName name, WorkspaceName workspaceName, CancellationToken cancellationToken) { var dtoOption = await findDto(name, workspaceName, cancellationToken); return dtoOption.Map(Common.GetRevisionNumber); } } private static void ConfigureDeleteWorkspaceApiFromApim(IHostApplicationBuilder builder) { AzureModule.ConfigureManagementServiceUri(builder); AzureModule.ConfigureHttpPipeline(builder); builder.Services.TryAddSingleton(GetDeleteWorkspaceApiFromApim); } private static DeleteWorkspaceApiFromApim GetDeleteWorkspaceApiFromApim(IServiceProvider provider) { var serviceUri = provider.GetRequiredService<ManagementServiceUri>(); var pipeline = provider.GetRequiredService<HttpPipeline>(); var logger = provider.GetRequiredService<ILogger>(); return async (name, workspaceName, cancellationToken) => { logger.LogInformation("Removing API {ApiName} from workspace {WorkspaceName}...", name, workspaceName); var resourceUri = WorkspaceApiUri.From(name, workspaceName, serviceUri); await resourceUri.Delete(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(WorkspaceApiDto dto) => ApiRevisionNumber.TryFrom(dto.Properties.ApiRevision) .IfNone(() => ApiRevisionNumber.From(1)); }