diff --git a/src/ArmTemplates/Commands/Executors/ExtractorExecutor.cs b/src/ArmTemplates/Commands/Executors/ExtractorExecutor.cs index bc272e6b..261679e9 100644 --- a/src/ArmTemplates/Commands/Executors/ExtractorExecutor.cs +++ b/src/ArmTemplates/Commands/Executors/ExtractorExecutor.cs @@ -11,6 +11,7 @@ using Microsoft.Azure.Management.ApiManagement.ArmTemplates.Common.FileHandlers; using Microsoft.Azure.Management.ApiManagement.ArmTemplates.Common.Templates.Abstractions; using Microsoft.Azure.Management.ApiManagement.ArmTemplates.Common.Templates.ApiManagementService; +using Microsoft.Azure.Management.ApiManagement.ArmTemplates.Common.Templates.ApiReleases; using Microsoft.Azure.Management.ApiManagement.ArmTemplates.Common.Templates.Apis; using Microsoft.Azure.Management.ApiManagement.ArmTemplates.Common.Templates.ApiVersionSet; using Microsoft.Azure.Management.ApiManagement.ArmTemplates.Common.Templates.AuthorizationServer; @@ -70,7 +71,8 @@ public class ExtractorExecutor readonly ISchemaExtractor schemaExtractor; readonly IOpenIdConnectProviderExtractor openIdConnectProviderExtractor; readonly IPolicyFragmentsExtractor policyFragmentsExtractor; - + readonly IApiReleaseExtractor apiReleaseExtractor; + public ExtractorExecutor( ILogger logger, IApisClient apisClient, @@ -95,7 +97,8 @@ public ExtractorExecutor( IApiManagementServiceExtractor apiManagementServiceExtractor, ISchemaExtractor schemaExtractor, IOpenIdConnectProviderExtractor openIdConnectProviderExtractor, - IPolicyFragmentsExtractor policyFragmentsExtractor) + IPolicyFragmentsExtractor policyFragmentsExtractor, + IApiReleaseExtractor apiReleaseExtractor) { this.logger = logger; this.apisClient = apisClient; @@ -121,6 +124,7 @@ public ExtractorExecutor( this.schemaExtractor = schemaExtractor; this.openIdConnectProviderExtractor = openIdConnectProviderExtractor; this.policyFragmentsExtractor = policyFragmentsExtractor; + this.apiReleaseExtractor = apiReleaseExtractor; } /// @@ -151,7 +155,8 @@ public static ExtractorExecutor BuildExtractorExecutor( IApiManagementServiceExtractor apiManagementServiceExtractor = null, ISchemaExtractor schemaExtractor = null, IOpenIdConnectProviderExtractor openIdConnectProviderExtractor = null, - IPolicyFragmentsExtractor policyFragmentsExtractor = null) + IPolicyFragmentsExtractor policyFragmentsExtractor = null, + IApiReleaseExtractor apiReleaseExtractor = null) => new ExtractorExecutor( logger, apisClient, @@ -176,7 +181,8 @@ public static ExtractorExecutor BuildExtractorExecutor( apiManagementServiceExtractor, schemaExtractor, openIdConnectProviderExtractor, - policyFragmentsExtractor); + policyFragmentsExtractor, + apiReleaseExtractor); public void SetExtractorParameters(ExtractorParameters extractorParameters) { @@ -478,7 +484,8 @@ public async Task GenerateResourceParametersFiles( Template identityProviderTemplate = null, Template schemaTemplate = null, Template openIdConnectProviderTemplate = null, - Template policyFragmentsTemplate = null) + Template policyFragmentsTemplate = null, + Template apiReleaseTemplate = null) { this.RenameExistingParametersDirectory(baseFilesGenerationDirectory); @@ -498,6 +505,7 @@ public async Task GenerateResourceParametersFiles( await this.GenerateResourceParametersFile(baseFilesGenerationDirectory, this.extractorParameters.FileNames.SchemaParameters, schemaTemplate, mainParametersTemplate); await this.GenerateResourceParametersFile(baseFilesGenerationDirectory, this.extractorParameters.FileNames.OpenIdConnectProvidersParameters, openIdConnectProviderTemplate, mainParametersTemplate); await this.GenerateResourceParametersFile(baseFilesGenerationDirectory, this.extractorParameters.FileNames.PolicyFragmentsParameters, policyFragmentsTemplate, mainParametersTemplate); + await this.GenerateResourceParametersFile(baseFilesGenerationDirectory, this.extractorParameters.FileNames.ApiReleaseParameters, apiReleaseTemplate, mainParametersTemplate); } public async Task GenerateResourceParametersFile(string baseFilesGenerationDirectory, string fileName, Template resourceTemplate, Template mainParametersTemplate) where TTemplateResource : ITemplateResources, new() @@ -901,6 +909,57 @@ await FileWriter.SaveAsJsonAsync( } /// + /// Generates api release template in the desired folder + /// + /// name of base folder where to save output files + /// generated api release template + public async Task> GenerateApiReleaseTemplateAsync(string apiName, string baseFilesGenerationDirectory) + { + this.logger.LogInformation("Started generation of api release template..."); + + var apiReleaseTemplate = this.apiReleaseExtractor.GenerateSingleApiReleaseTemplate(apiName, this.extractorParameters); + + if (apiReleaseTemplate?.HasResources() == true) + { + await FileWriter.SaveAsJsonAsync( + apiReleaseTemplate, + directory: baseFilesGenerationDirectory, + fileName: this.extractorParameters.FileNames.ApiRelease); + } + + this.logger.LogInformation("Finished generation of apiRelease template..."); + return apiReleaseTemplate; + } + + /// + /// Generates api release template in the desired folder + /// + /// name of base folder where to save output files + /// generated api release template + public async Task> GenerateApiReleasesTemplateAsync(string baseFilesGenerationDirectory) + { + if (!string.IsNullOrEmpty(this.extractorParameters.SingleApiName) || !this.extractorParameters.MultipleApiNames.IsNullOrEmpty()) + { + this.logger.LogInformation("Skip generation of api releases template: not all apis are extracted"); + return null; + } + + this.logger.LogInformation("Started generation of api releases template..."); + + var apiReleaseTemplate = await this.apiReleaseExtractor.GenerateCurrentApiReleaseTemplate(this.extractorParameters); + + if (apiReleaseTemplate?.HasResources() == true) + { + await FileWriter.SaveAsJsonAsync( + apiReleaseTemplate, + directory: baseFilesGenerationDirectory, + fileName: this.extractorParameters.FileNames.ApiRelease); + } + + this.logger.LogInformation("Finished generation of apiReleases template..."); + return apiReleaseTemplate; + } + /// Generates policy fragments templates in the desired folder /// /// name of base folder where to save output files @@ -1034,14 +1093,17 @@ async Task GenerateSingleAPIWithRevisionsTemplates() this.logger.LogInformation("Extracting singleAPI {0} with revisions", this.extractorParameters.SingleApiName); string currentRevision = null; + bool generateSingleApiReleaseTemplate; List revList = new List(); await foreach (var apiRevision in this.apiRevisionExtractor.GetApiRevisionsAsync(this.extractorParameters.SingleApiName, this.extractorParameters)) { + generateSingleApiReleaseTemplate = false; var apiRevisionName = apiRevision.ApiId.Split("/")[2]; if (apiRevision.IsCurrent) { currentRevision = apiRevisionName; + generateSingleApiReleaseTemplate = true; } // creating a folder for this api revision @@ -1050,6 +1112,11 @@ async Task GenerateSingleAPIWithRevisionsTemplates() revList.Add(apiRevisionName); await this.GenerateTemplates(revFileFolder, singleApiName: apiRevisionName); + + if (generateSingleApiReleaseTemplate) + { + await this.GenerateApiReleaseTemplateAsync(apiRevisionName, revFileFolder); + } } if (currentRevision is null) @@ -1088,9 +1155,9 @@ async Task GenerateTemplates( } var apisToExtract = await this.GetApiNamesToExtract(singleApiName, multipleApiNames); - // generate different templates using extractors and write to output apiTemplate = apiTemplate ?? await this.GenerateApiTemplateAsync(singleApiName, multipleApiNames, baseFilesGenerationDirectory); + var globalServicePolicyTemplate = await this.GeneratePolicyTemplateAsync(baseFilesGenerationDirectory); var productApiTemplate = await this.GenerateProductApisTemplateAsync(singleApiName, multipleApiNames, baseFilesGenerationDirectory); var productTemplate = await this.GenerateProductsTemplateAsync(singleApiName, baseFilesGenerationDirectory, apiTemplate.TypedResources.ApiProducts); @@ -1106,9 +1173,11 @@ async Task GenerateTemplates( var openIdConnectProviderTemplate = await this.GenerateOpenIdConnectProviderTemplateAsync(baseFilesGenerationDirectory); var schemasTempate = await this.GenerateSchemasTemplateAsync(baseFilesGenerationDirectory); var policyFragmentTemplate = await this.GeneratePolicyFragmentsTemplateAsync(apiTemplate.TypedResources.GetAllPolicies(), baseFilesGenerationDirectory); + var apiReleasesTemplate = await this.GenerateApiReleasesTemplateAsync(baseFilesGenerationDirectory); await this.GenerateGatewayTemplateAsync(singleApiName, baseFilesGenerationDirectory); await this.GenerateGatewayApiTemplateAsync(singleApiName, multipleApiNames, baseFilesGenerationDirectory); await this.GenerateApiManagementServiceTemplate(baseFilesGenerationDirectory); + var parametersTemplate = await this.GenerateParametersTemplateAsync(apisToExtract, loggerTemplate.TypedResources, backendTemplate.TypedResources, namedValueTemplate.TypedResources, identityProviderTemplate.TypedResources, openIdConnectProviderTemplate.TypedResources, baseFilesGenerationDirectory); await this.GenerateResourceParametersFiles( @@ -1129,7 +1198,8 @@ await this.GenerateResourceParametersFiles( identityProviderTemplate: identityProviderTemplate, openIdConnectProviderTemplate: openIdConnectProviderTemplate, schemaTemplate: schemasTempate, - policyFragmentsTemplate: policyFragmentTemplate); + policyFragmentsTemplate: policyFragmentTemplate, + apiReleaseTemplate: apiReleasesTemplate); await this.GenerateMasterTemplateAsync( baseFilesGenerationDirectory, diff --git a/src/ArmTemplates/Common/API/Clients/Abstractions/IApisClient.cs b/src/ArmTemplates/Common/API/Clients/Abstractions/IApisClient.cs index a9a0d5de..603696d3 100644 --- a/src/ArmTemplates/Common/API/Clients/Abstractions/IApisClient.cs +++ b/src/ArmTemplates/Common/API/Clients/Abstractions/IApisClient.cs @@ -16,6 +16,8 @@ public interface IApisClient Task> GetAllAsync(ExtractorParameters extractorParameters); + Task> GetAllCurrentAsync(ExtractorParameters extractorParameters); + Task> GetAllLinkedToProductAsync(string productName, ExtractorParameters extractorParameters); Task> GetAllLinkedToGatewayAsync(string gatewayName, ExtractorParameters extractorParameters); diff --git a/src/ArmTemplates/Common/API/Clients/Apis/ApisClient.cs b/src/ArmTemplates/Common/API/Clients/Apis/ApisClient.cs index 5805144b..c65cc615 100644 --- a/src/ArmTemplates/Common/API/Clients/Apis/ApisClient.cs +++ b/src/ArmTemplates/Common/API/Clients/Apis/ApisClient.cs @@ -10,6 +10,7 @@ using Microsoft.Azure.Management.ApiManagement.ArmTemplates.Common.Constants; using Microsoft.Azure.Management.ApiManagement.ArmTemplates.Common.Templates.Apis; using Microsoft.Azure.Management.ApiManagement.ArmTemplates.Extractor.Models; +using Microsoft.Azure.Management.ApiManagement.ArmTemplates.Extractor.Utilities.DataProcessors.Absctraction; namespace Microsoft.Azure.Management.ApiManagement.ArmTemplates.Common.API.Clients.Apis { @@ -17,11 +18,15 @@ public class ApisClient : ApiClientBase, IApisClient { const string GetSingleApiRequest = "{0}/subscriptions/{1}/resourceGroups/{2}/providers/Microsoft.ApiManagement/service/{3}/apis/{4}?api-version={5}"; const string GetAllApisRequest = "{0}/subscriptions/{1}/resourceGroups/{2}/providers/Microsoft.ApiManagement/service/{3}/apis?api-version={4}"; + const string GetAllCurrentApisRequest = "{0}/subscriptions/{1}/resourceGroups/{2}/providers/Microsoft.ApiManagement/service/{3}/apis?api-version={4}&$filter=isCurrent"; const string GetAllApisLinkedToProductRequest = "{0}/subscriptions/{1}/resourceGroups/{2}/providers/Microsoft.ApiManagement/service/{3}/products/{4}/apis?api-version={5}"; const string GetApisLinkedToGatewayRequest = "{0}/subscriptions/{1}/resourceGroups/{2}/providers/Microsoft.ApiManagement/service/{3}/gateways/{4}/apis?api-version={5}"; - public ApisClient(IHttpClientFactory httpClientFactory) : base(httpClientFactory) + readonly IApiDataProcessor apiDataProcessor; + + public ApisClient(IHttpClientFactory httpClientFactory, IApiDataProcessor apiDataProcessor) : base(httpClientFactory) { + this.apiDataProcessor = apiDataProcessor; } public async Task GetSingleAsync(string apiName, ExtractorParameters extractorParameters) @@ -30,7 +35,10 @@ public async Task GetSingleAsync(string apiName, ExtractorP string requestUrl = string.Format(GetSingleApiRequest, this.BaseUrl, azSubId, extractorParameters.ResourceGroup, extractorParameters.SourceApimName, apiName, GlobalConstants.ApiVersion); - return await this.GetResponseAsync(azToken, requestUrl); + var api = await this.GetResponseAsync(azToken, requestUrl); + this.apiDataProcessor.ProcessSingleData(api); + + return api; } public async Task> GetAllAsync(ExtractorParameters extractorParameters) @@ -40,7 +48,23 @@ public async Task> GetAllAsync(ExtractorParameters ext string requestUrl = string.Format(GetAllApisRequest, this.BaseUrl, azSubId, extractorParameters.ResourceGroup, extractorParameters.SourceApimName, GlobalConstants.ApiVersion); - return await this.GetPagedResponseAsync(azToken, requestUrl); + var apis = await this.GetPagedResponseAsync(azToken, requestUrl); + this.apiDataProcessor.ProcessData(apis); + + return apis; + } + + public async Task> GetAllCurrentAsync(ExtractorParameters extractorParameters) + { + var (azToken, azSubId) = await this.Auth.GetAccessToken(); + + string requestUrl = string.Format(GetAllCurrentApisRequest, + this.BaseUrl, azSubId, extractorParameters.ResourceGroup, extractorParameters.SourceApimName, GlobalConstants.ApiVersion); + + var apis = await this.GetPagedResponseAsync(azToken, requestUrl); + this.apiDataProcessor.ProcessData(apis); + + return apis; } public async Task> GetAllLinkedToProductAsync(string productName, ExtractorParameters extractorParameters) @@ -49,7 +73,10 @@ public async Task> GetAllLinkedToProductAsync(string p string requestUrl = string.Format(GetAllApisLinkedToProductRequest, this.BaseUrl, azSubId, extractorParameters.ResourceGroup, extractorParameters.SourceApimName, productName, GlobalConstants.ApiVersion); - return await this.GetPagedResponseAsync(azToken, requestUrl); + var apis = await this.GetPagedResponseAsync(azToken, requestUrl); + this.apiDataProcessor.ProcessData(apis); + + return apis; } public async Task> GetAllLinkedToGatewayAsync(string gatewayName, ExtractorParameters extractorParameters) @@ -58,7 +85,10 @@ public async Task> GetAllLinkedToGatewayAsync(string g string requestUrl = string.Format(GetApisLinkedToGatewayRequest, this.BaseUrl, azSubId, extractorParameters.ResourceGroup, extractorParameters.SourceApimName, gatewayName, GlobalConstants.ApiVersion); - return await this.GetPagedResponseAsync(azToken, requestUrl); + var apis = await this.GetPagedResponseAsync(azToken, requestUrl); + this.apiDataProcessor.ProcessData(apis); + + return apis; } } } diff --git a/src/ArmTemplates/Common/Extensions/NamingHelper.cs b/src/ArmTemplates/Common/Extensions/NamingHelper.cs index 813a27b7..9ac8c235 100644 --- a/src/ArmTemplates/Common/Extensions/NamingHelper.cs +++ b/src/ArmTemplates/Common/Extensions/NamingHelper.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Text.RegularExpressions; +using Microsoft.Azure.Management.ApiManagement.ArmTemplates.Common.Constants; namespace Microsoft.Azure.Management.ApiManagement.ArmTemplates.Common.Extensions { @@ -13,6 +14,11 @@ public static class NamingHelper static readonly Regex ExcludeOtherFromLettersAndDigitsRegex = new Regex("[^a-zA-Z0-9]"); static readonly Regex ExcludeOtherFromAlphaNumericsAndHyphensRegex = new Regex("[^a-zA-Z0-9-]"); + // ValidDeployment name is the string following the pattern: '^[-\w\._\(\)]+$'. https://docs.microsoft.com/en-us/rest/api/resources/deployments/create-or-update?tabs=HTTP#uri-parameters + // ExcludeOtherFromValidDeploymentNameCharsRegex matches any character other than [alphanumerics + '-' + '.' + '(' + ')' + '_']; + // Example: 'template;rev=1.json' string will match the ';' and '=' chars + static readonly Regex ExcludeOtherFromValidDeploymentNameCharsRegex = new Regex(@"[^-\w\._\(\)]"); + public static string GetSubstringBetweenTwoCharacters(char left, char right, string fullString) { var regex = new Regex($"(?<={left})(.*?)(?={right})"); @@ -60,5 +66,21 @@ public static string GenerateParametrizedResourceName(string parameterName, stri { return $"[concat(parameters('{parameterName}'), '/{resourceName}')]"; } + + public static string GenerateValidDeploymentFileName(string fileName) + { + if (string.IsNullOrEmpty(fileName)) + { + return string.Empty; + } + + var resourceName = ExcludeOtherFromValidDeploymentNameCharsRegex.Replace(fileName, "-"); + return resourceName; + } + + public static string GenerateApisResourceId(string apiName) + { + return $"[resourceId('{ResourceTypeConstants.API}', parameters('{ParameterNames.ApimServiceName}'), '{apiName}')]"; + } } } diff --git a/src/ArmTemplates/Common/FileHandlers/FileNameGenerator.cs b/src/ArmTemplates/Common/FileHandlers/FileNameGenerator.cs index 71bcbfe3..a6efd710 100644 --- a/src/ArmTemplates/Common/FileHandlers/FileNameGenerator.cs +++ b/src/ArmTemplates/Common/FileHandlers/FileNameGenerator.cs @@ -3,6 +3,8 @@ // Licensed under the MIT License. // -------------------------------------------------------------------------- +using Microsoft.Azure.Management.ApiManagement.ArmTemplates.Common.Extensions; + namespace Microsoft.Azure.Management.ApiManagement.ArmTemplates.Common.FileHandlers { public static class FileNameGenerator @@ -56,6 +58,8 @@ public static FileNames GenerateFileNames(string baseFileName) SchemaParameters = "schemas.parameters.json", PolicyFragments = $@"{baseFileName}policy-fragments.template.json", PolicyFragmentsParameters = "policy-fragments.parameters.json", + ApiRelease = $@"{baseFileName}api-release.template.json", + ApiReleaseParameters = $@"api-release.parameters.json", Parameters = $@"{baseFileName}parameters.json", LinkedMaster = $@"{baseFileName}master.template.json", Apis = "/Apis", @@ -96,7 +100,7 @@ public static string GenerateExtractorAPIParametersFileName(string singleAPIName } public static string GenerateApiFileNameBase(string singleAPIName) { - return singleAPIName == null ? "apis" : $"{singleAPIName}-api"; + return singleAPIName == null ? "apis" : $"{NamingHelper.GenerateValidDeploymentFileName(singleAPIName)}-api"; } public static string GenerateOriginalAPIName(string apiName) diff --git a/src/ArmTemplates/Common/FileHandlers/FileNames.cs b/src/ArmTemplates/Common/FileHandlers/FileNames.cs index 2671ad78..64e4f882 100644 --- a/src/ArmTemplates/Common/FileHandlers/FileNames.cs +++ b/src/ArmTemplates/Common/FileHandlers/FileNames.cs @@ -77,6 +77,10 @@ public class FileNames public string PolicyFragmentsParameters { get; set; } + public string ApiRelease { get; set; } + + public string ApiReleaseParameters { get; set; } + public string Parameters { get; set; } // linked property outputs 1 master template diff --git a/src/ArmTemplates/Common/Templates/ApiReleases/ApiReleaseProperties.cs b/src/ArmTemplates/Common/Templates/ApiReleases/ApiReleaseProperties.cs new file mode 100644 index 00000000..c43db5c9 --- /dev/null +++ b/src/ArmTemplates/Common/Templates/ApiReleases/ApiReleaseProperties.cs @@ -0,0 +1,14 @@ +// -------------------------------------------------------------------------- +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +// -------------------------------------------------------------------------- + +namespace Microsoft.Azure.Management.ApiManagement.ArmTemplates.Common.Templates.ApiReleases +{ + public class ApiReleaseProperties + { + public string ApiId { get; set; } + + public string Notes { get; set; } + } +} diff --git a/src/ArmTemplates/Common/Templates/ApiReleases/ApiReleaseTemplateResource.cs b/src/ArmTemplates/Common/Templates/ApiReleases/ApiReleaseTemplateResource.cs new file mode 100644 index 00000000..28420c67 --- /dev/null +++ b/src/ArmTemplates/Common/Templates/ApiReleases/ApiReleaseTemplateResource.cs @@ -0,0 +1,14 @@ +// -------------------------------------------------------------------------- +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +// -------------------------------------------------------------------------- + +using Microsoft.Azure.Management.ApiManagement.ArmTemplates.Common.Templates.Abstractions; + +namespace Microsoft.Azure.Management.ApiManagement.ArmTemplates.Common.Templates.ApiReleases +{ + public class ApiReleaseTemplateResource : TemplateResource + { + public ApiReleaseProperties Properties { get; set; } + } +} diff --git a/src/ArmTemplates/Common/Templates/ApiReleases/ApiReleaseTemplateResources.cs b/src/ArmTemplates/Common/Templates/ApiReleases/ApiReleaseTemplateResources.cs new file mode 100644 index 00000000..eb8d0183 --- /dev/null +++ b/src/ArmTemplates/Common/Templates/ApiReleases/ApiReleaseTemplateResources.cs @@ -0,0 +1,26 @@ +// -------------------------------------------------------------------------- +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +// -------------------------------------------------------------------------- + +using System.Collections.Generic; +using Microsoft.Azure.Management.ApiManagement.ArmTemplates.Common.Extensions; +using Microsoft.Azure.Management.ApiManagement.ArmTemplates.Common.Templates.Abstractions; + +namespace Microsoft.Azure.Management.ApiManagement.ArmTemplates.Common.Templates.ApiReleases +{ + public class ApiReleaseTemplateResources : ITemplateResources + { + public List ApiReleases { get; set; } = new(); + + public TemplateResource[] BuildTemplateResources() + { + return this.ApiReleases.ToArray(); + } + + public bool HasContent() + { + return !this.ApiReleases.IsNullOrEmpty(); + } + } +} diff --git a/src/ArmTemplates/Common/Templates/Apis/ApiProperties.cs b/src/ArmTemplates/Common/Templates/Apis/ApiProperties.cs index 3df65c18..146cbaf2 100644 --- a/src/ArmTemplates/Common/Templates/Apis/ApiProperties.cs +++ b/src/ArmTemplates/Common/Templates/Apis/ApiProperties.cs @@ -3,6 +3,8 @@ // Licensed under the MIT License. // -------------------------------------------------------------------------- +using Newtonsoft.Json; + namespace Microsoft.Azure.Management.ApiManagement.ArmTemplates.Common.Templates.Apis { public class ApiProperties @@ -21,6 +23,8 @@ public class ApiProperties public bool? IsCurrent { get; set; } + public bool? IsOnline { get; set; } + public string ApiRevisionDescription { get; set; } public string ApiVersionDescription { get; set; } diff --git a/src/ArmTemplates/Common/Templates/Apis/ApiTemplateResource.cs b/src/ArmTemplates/Common/Templates/Apis/ApiTemplateResource.cs index d816b1b3..ec714a88 100644 --- a/src/ArmTemplates/Common/Templates/Apis/ApiTemplateResource.cs +++ b/src/ArmTemplates/Common/Templates/Apis/ApiTemplateResource.cs @@ -4,11 +4,18 @@ // -------------------------------------------------------------------------- using Microsoft.Azure.Management.ApiManagement.ArmTemplates.Common.Templates.Abstractions; +using Newtonsoft.Json; namespace Microsoft.Azure.Management.ApiManagement.ArmTemplates.Common.Templates.Apis { public class ApiTemplateResource : TemplateResource { + [JsonIgnore] + public string OriginalName { get; set; } + + [JsonIgnore] + public string ApiNameWithRevision { get; set; } + public ApiProperties Properties { get; set; } } } diff --git a/src/ArmTemplates/Extractor/EntityExtractors/APIExtractor.cs b/src/ArmTemplates/Extractor/EntityExtractors/APIExtractor.cs index 24b29e2d..d558283e 100644 --- a/src/ArmTemplates/Extractor/EntityExtractors/APIExtractor.cs +++ b/src/ArmTemplates/Extractor/EntityExtractors/APIExtractor.cs @@ -33,6 +33,7 @@ public class ApiExtractor : IApiExtractor readonly IProductApisExtractor productApisExtractor; readonly ITagExtractor tagExtractor; readonly IApiOperationExtractor apiOperationExtractor; + readonly IApiRevisionClient apiRevisionClient; public ApiExtractor( ILogger logger, @@ -43,7 +44,8 @@ public ApiExtractor( IPolicyExtractor policyExtractor, IProductApisExtractor productApisExtractor, ITagExtractor tagExtractor, - IApiOperationExtractor apiOperationExtractor) + IApiOperationExtractor apiOperationExtractor, + IApiRevisionClient apiRevisionClient) { this.logger = logger; this.templateBuilder = templateBuilder; @@ -56,6 +58,7 @@ public ApiExtractor( this.productApisExtractor = productApisExtractor; this.tagExtractor = tagExtractor; this.apiOperationExtractor = apiOperationExtractor; + this.apiRevisionClient = apiRevisionClient; } public async Task> GenerateApiTemplateAsync( @@ -94,7 +97,7 @@ public async Task> GenerateApiTemplateAsync( return apiTemplate; } - public async Task GenerateSingleApiTemplateResourcesAsync(string singleApiName, string baseFilesGenerationDirectory, ExtractorParameters extractorParameters) + public async Task GenerateSingleApiTemplateResourcesAsync(string singleApiName, string baseFilesGenerationDirectory, ExtractorParameters extractorParameters, string apiDepends = null) { var apiTemplateResources = await this.GetApiRelatedTemplateResourcesAsync(singleApiName, baseFilesGenerationDirectory, extractorParameters); @@ -108,7 +111,7 @@ public async Task GenerateSingleApiTemplateResourcesAsync( } this.logger.LogInformation("{0} API data found ...", singleApiName); - this.SetArmTemplateValuesToApiTemplateResource(apiResource, extractorParameters); + this.SetArmTemplateValuesToApiTemplateResource(singleApiName, apiResource, extractorParameters, apiDepends); apiTemplateResources.Apis.Add(apiResource); } catch (Exception ex) @@ -123,50 +126,55 @@ public async Task GenerateSingleApiTemplateResourcesAsync( async Task GenerateMultipleApisTemplateAsync(List multipleApiNames, string baseFilesGenerationDirectory, ExtractorParameters extractorParameters) { var generalApiTemplateResourcesStorage = new ApiTemplateResources(); + + var apiDependencies = await this.GenerateApiRevisionsDependenciesAsync(extractorParameters); + foreach (var apiName in multipleApiNames) { - var specificApiTemplateResources = await this.GenerateSingleApiTemplateResourcesAsync(apiName, baseFilesGenerationDirectory, extractorParameters); + apiDependencies.TryGetValue(apiName, out var apiDependsOn); + + var specificApiTemplateResources = await this.GenerateSingleApiTemplateResourcesAsync(apiName, baseFilesGenerationDirectory, extractorParameters, apiDependsOn); generalApiTemplateResourcesStorage.AddResourcesData(specificApiTemplateResources); } return generalApiTemplateResourcesStorage; } - void SetArmTemplateValuesToApiTemplateResource(ApiTemplateResource apiResource, ExtractorParameters extractorParameters) + void SetArmTemplateValuesToApiTemplateResource(string apiName, ApiTemplateResource apiResource, ExtractorParameters extractorParameters, string apiDepends = null) { - var originalServiceApiName = apiResource.Name; - - apiResource.Name = $"[concat(parameters('{ParameterNames.ApimServiceName}'), '/{originalServiceApiName}')]"; + apiResource.Name = $"[concat(parameters('{ParameterNames.ApimServiceName}'), '/{apiName}')]"; apiResource.ApiVersion = GlobalConstants.ApiVersion; apiResource.Scale = null; + apiResource.DependsOn = Array.Empty(); + var apiResourceDepends = new List(); + + if (!string.IsNullOrEmpty(apiDepends)) { + apiResourceDepends.Add(NamingHelper.GenerateApisResourceId(apiDepends)); + } if (extractorParameters.ParameterizeServiceUrl) { - apiResource.Properties.ServiceUrl = $"[parameters('{ParameterNames.ServiceUrl}').{NamingHelper.GenerateValidParameterName(originalServiceApiName, ParameterPrefix.Api)}]"; + apiResource.Properties.ServiceUrl = $"[parameters('{ParameterNames.ServiceUrl}').{NamingHelper.GenerateValidParameterName(apiName, ParameterPrefix.Api)}]"; } if (extractorParameters.ParametrizeApiOauth2Scope) { if (apiResource.Properties.AuthenticationSettings?.OAuth2?.Scope is not null) { - apiResource.Properties.AuthenticationSettings.OAuth2.Scope = $"[parameters('{ParameterNames.ApiOauth2ScopeSettings}').{NamingHelper.GenerateValidParameterName(originalServiceApiName, ParameterPrefix.ApiOauth2Scope)}]"; + apiResource.Properties.AuthenticationSettings.OAuth2.Scope = $"[parameters('{ParameterNames.ApiOauth2ScopeSettings}').{NamingHelper.GenerateValidParameterName(apiName, ParameterPrefix.ApiOauth2Scope)}]"; } } if (apiResource.Properties.ApiVersionSetId != null) { - apiResource.DependsOn = Array.Empty(); - string versionSetName = apiResource.Properties.ApiVersionSetId; int versionSetPosition = versionSetName.IndexOf("apiVersionSets/"); versionSetName = versionSetName.Substring(versionSetPosition, versionSetName.Length - versionSetPosition); apiResource.Properties.ApiVersionSetId = $"[concat(resourceId('Microsoft.ApiManagement/service', parameters('{ParameterNames.ApimServiceName}')), '/{versionSetName}')]"; } - else - { - apiResource.DependsOn = Array.Empty(); - } + + apiResource.DependsOn = apiResourceDepends.ToArray(); } /// @@ -232,5 +240,44 @@ public async Task GetApiRelatedTemplateResourcesAsync(stri return apiTemplateResources; } + + /// + /// Generates dictionary with api and api revisions dependency. + /// + /// + async Task> GenerateApiRevisionsDependenciesAsync(ExtractorParameters extractorParameters) + { + var apiDependency = new Dictionary(); + if (!string.IsNullOrEmpty(extractorParameters.SingleApiName) || !extractorParameters.MultipleApiNames.IsNullOrEmpty()) + { + return apiDependency; + } + + var apis = await this.apisClient.GetAllCurrentAsync(extractorParameters); + + if (apis.IsNullOrEmpty()) + { + this.logger.LogWarning($"No current apis were found for '{extractorParameters.SourceApimName}' at '{extractorParameters.ResourceGroup}'"); + return apiDependency; + } + + foreach (var api in apis) + { + string apiName = api.OriginalName; + var apiRevisions = await this.apiRevisionClient.GetApiRevisionsAsync(apiName, extractorParameters); + + foreach (var apiRevision in apiRevisions) + { + if (apiRevision.IsCurrent) + { + continue; + } + + apiDependency.Add(apiRevision.ApiId.Split("/")[2], api.Name); + } + } + + return apiDependency; + } } } diff --git a/src/ArmTemplates/Extractor/EntityExtractors/Abstractions/IApiExtractor.cs b/src/ArmTemplates/Extractor/EntityExtractors/Abstractions/IApiExtractor.cs index 957d40e0..3a711162 100644 --- a/src/ArmTemplates/Extractor/EntityExtractors/Abstractions/IApiExtractor.cs +++ b/src/ArmTemplates/Extractor/EntityExtractors/Abstractions/IApiExtractor.cs @@ -15,7 +15,7 @@ public interface IApiExtractor { Task> GenerateApiTemplateAsync(string singleApiName, List multipleApiNames, string baseFilesGenerationDirectory, ExtractorParameters extractorParameters); - Task GenerateSingleApiTemplateResourcesAsync(string singleApiName, string baseFilesGenerationDirectory, ExtractorParameters extractorParameters); + Task GenerateSingleApiTemplateResourcesAsync(string singleApiName, string baseFilesGenerationDirectory, ExtractorParameters extractorParameters, string apiDepends = null); Task GetApiRelatedTemplateResourcesAsync(string apiName, string baseFilesGenerationDirectory, ExtractorParameters extractorParameters); } diff --git a/src/ArmTemplates/Extractor/EntityExtractors/Abstractions/IApiReleaseExtractor.cs b/src/ArmTemplates/Extractor/EntityExtractors/Abstractions/IApiReleaseExtractor.cs new file mode 100644 index 00000000..05efe650 --- /dev/null +++ b/src/ArmTemplates/Extractor/EntityExtractors/Abstractions/IApiReleaseExtractor.cs @@ -0,0 +1,19 @@ +// -------------------------------------------------------------------------- +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +// -------------------------------------------------------------------------- + +using System.Threading.Tasks; +using Microsoft.Azure.Management.ApiManagement.ArmTemplates.Common.Templates.Abstractions; +using Microsoft.Azure.Management.ApiManagement.ArmTemplates.Common.Templates.ApiReleases; +using Microsoft.Azure.Management.ApiManagement.ArmTemplates.Extractor.Models; + +namespace Microsoft.Azure.Management.ApiManagement.ArmTemplates.Extractor.EntityExtractors.Abstractions +{ + public interface IApiReleaseExtractor + { + Template GenerateSingleApiReleaseTemplate(string apiName, ExtractorParameters extractorParameters); + + Task> GenerateCurrentApiReleaseTemplate(ExtractorParameters extractorParameters); + } +} \ No newline at end of file diff --git a/src/ArmTemplates/Extractor/EntityExtractors/ApiOperationExtractor.cs b/src/ArmTemplates/Extractor/EntityExtractors/ApiOperationExtractor.cs index 634b2872..277bf6e2 100644 --- a/src/ArmTemplates/Extractor/EntityExtractors/ApiOperationExtractor.cs +++ b/src/ArmTemplates/Extractor/EntityExtractors/ApiOperationExtractor.cs @@ -47,7 +47,7 @@ public async Task> GenerateApiOperationsResou apiOperation.ApiVersion = GlobalConstants.ApiVersion; apiOperation.Scale = null; - var operationDependsOn = new List() { $"[resourceId('Microsoft.ApiManagement/service/apis', parameters('{ParameterNames.ApimServiceName}'), '{apiName}')]" }; + var operationDependsOn = new List() { NamingHelper.GenerateApisResourceId(apiName) }; if (apiOperation.Properties?.Request?.Representations?.IsNullOrEmpty() == false) { foreach (var requestRepresentation in apiOperation.Properties.Request.Representations) diff --git a/src/ArmTemplates/Extractor/EntityExtractors/ApiReleaseExtractor.cs b/src/ArmTemplates/Extractor/EntityExtractors/ApiReleaseExtractor.cs new file mode 100644 index 00000000..134965dd --- /dev/null +++ b/src/ArmTemplates/Extractor/EntityExtractors/ApiReleaseExtractor.cs @@ -0,0 +1,90 @@ +// -------------------------------------------------------------------------- +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +// -------------------------------------------------------------------------- + +using System; +using System.Threading.Tasks; +using Microsoft.Azure.Management.ApiManagement.ArmTemplates.Common.API.Clients.Abstractions; +using Microsoft.Azure.Management.ApiManagement.ArmTemplates.Common.Constants; +using Microsoft.Azure.Management.ApiManagement.ArmTemplates.Common.Extensions; +using Microsoft.Azure.Management.ApiManagement.ArmTemplates.Common.Templates.Abstractions; +using Microsoft.Azure.Management.ApiManagement.ArmTemplates.Common.Templates.ApiReleases; +using Microsoft.Azure.Management.ApiManagement.ArmTemplates.Common.Templates.Builders.Abstractions; +using Microsoft.Azure.Management.ApiManagement.ArmTemplates.Extractor.EntityExtractors.Abstractions; +using Microsoft.Azure.Management.ApiManagement.ArmTemplates.Extractor.Models; +using Microsoft.Extensions.Logging; + +namespace Microsoft.Azure.Management.ApiManagement.ArmTemplates.Extractor.EntityExtractors +{ + public class ApiReleaseExtractor : IApiReleaseExtractor + { + readonly ILogger logger; + readonly ITemplateBuilder templateBuilder; + readonly IApisClient apisClient; + + public ApiReleaseExtractor( + ILogger logger, + ITemplateBuilder templateBuilder, + IApisClient apisClient + ) + { + this.logger = logger; + this.templateBuilder = templateBuilder; + this.apisClient = apisClient; + } + + public Template GenerateSingleApiReleaseTemplate(string apiName, ExtractorParameters extractorParameters) + { + var apiReleaseTemplate = this.templateBuilder + .GenerateTemplateWithApimServiceNameProperty() + .Build(); + + var apiReleaseResource = this.GenerateReleaseResource(apiName); + apiReleaseTemplate.TypedResources.ApiReleases.Add(apiReleaseResource); + + return apiReleaseTemplate; + } + + + public async Task> GenerateCurrentApiReleaseTemplate(ExtractorParameters extractorParameters) + { + var apiReleasesTemplate = this.templateBuilder + .GenerateTemplateWithApimServiceNameProperty() + .Build(); + + var apis = await this.apisClient.GetAllCurrentAsync(extractorParameters); + + if (apis.IsNullOrEmpty()) + { + this.logger.LogWarning($"No current apis were found for '{extractorParameters.SourceApimName}' at '{extractorParameters.ResourceGroup}'"); + return apiReleasesTemplate; + } + + foreach (var api in apis) + { + var apiReleaseResource = this.GenerateReleaseResource(api.ApiNameWithRevision); + apiReleasesTemplate.TypedResources.ApiReleases.Add(apiReleaseResource); + } + + return apiReleasesTemplate; + } + + ApiReleaseTemplateResource GenerateReleaseResource(string apiName) + { + var apiReleaseName = $"{Guid.NewGuid()}"; + var apiReleaseResource = new ApiReleaseTemplateResource + { + Name = $"[concat(parameters('{ParameterNames.ApimServiceName}'), '/{apiName}/{apiReleaseName}')]", + Type = ResourceTypeConstants.APIRelease, + ApiVersion = GlobalConstants.ApiVersion, + Properties = new ApiReleaseProperties + { + ApiId = $"/apis/{apiName}" + } + }; + + return apiReleaseResource; + } + } +} diff --git a/src/ArmTemplates/Extractor/EntityExtractors/ApiRevisionExtractor.cs b/src/ArmTemplates/Extractor/EntityExtractors/ApiRevisionExtractor.cs index af4834b7..9d64a37d 100644 --- a/src/ArmTemplates/Extractor/EntityExtractors/ApiRevisionExtractor.cs +++ b/src/ArmTemplates/Extractor/EntityExtractors/ApiRevisionExtractor.cs @@ -25,7 +25,6 @@ public class ApiRevisionExtractor : IApiRevisionExtractor readonly ILogger logger; readonly ITemplateBuilder templateBuilder; - readonly IApisClient apisClient; readonly IApiRevisionClient apiRevisionClient; readonly IApiExtractor apiExtractor; @@ -34,13 +33,11 @@ public ApiRevisionExtractor( ILogger logger, ITemplateBuilder templateBuilder, IApiExtractor apiExtractor, - IApisClient apisClient, IApiRevisionClient apiRevisionClient) { this.logger = logger; this.templateBuilder = templateBuilder; - this.apisClient = apisClient; this.apiRevisionClient = apiRevisionClient; this.apiExtractor = apiExtractor; @@ -84,7 +81,7 @@ public async Task> GenerateApiRevisionTemplateAsy { var apiResources = await this.apiExtractor.GenerateSingleApiTemplateResourcesAsync(curApi, baseFilesGenerationDirectory, extractorParameters); var apiResource = apiResources.Apis.FirstOrDefault(); - apiResource.DependsOn = new[] { $"[resourceId('Microsoft.ApiManagement/service/apis', parameters('{ParameterNames.ApimServiceName}'), '{extractorParameters.SingleApiName}')]" }; + apiResource.DependsOn = new[] { NamingHelper.GenerateApisResourceId(extractorParameters.SingleApiName) }; apiRevisionTemplate.TypedResources.AddResourcesData(apiResources); } @@ -92,51 +89,5 @@ public async Task> GenerateApiRevisionTemplateAsy return apiRevisionTemplate; } - - async Task GenerateCurrentRevisionApiTemplateResourcesAsync(string apiName, string baseFilesGenerationDirectory, ExtractorParameters extractorParameters) - { - var apiTemplateResources = new ApiTemplateResources(); - - // gets the current revision of this api and will remove "isCurrent" paramter - var versionedApiResource = await this.GenerateApiTemplateResourceAsVersioned(apiName, extractorParameters); - - apiTemplateResources.Apis.Add(versionedApiResource); - var relatedTemplateResources = await this.apiExtractor.GetApiRelatedTemplateResourcesAsync(apiName, baseFilesGenerationDirectory, extractorParameters); - apiTemplateResources.AddResourcesData(relatedTemplateResources); - - return apiTemplateResources; - } - - async Task GenerateApiTemplateResourceAsVersioned(string apiName, ExtractorParameters extractorParameters) - { - var apiDetails = await this.apisClient.GetSingleAsync(apiName, extractorParameters); - - apiDetails.Name = $"[concat(parameters('{ParameterNames.ApimServiceName}'), '/{apiName}')]"; - apiDetails.ApiVersion = GlobalConstants.ApiVersion; - apiDetails.Scale = null; - apiDetails.Properties.IsCurrent = null; - - if (extractorParameters.ParameterizeServiceUrl) - { - apiDetails.Properties.ServiceUrl = $"[parameters('{ParameterNames.ServiceUrl}').{NamingHelper.GenerateValidParameterName(apiName, ParameterPrefix.Api)}]"; - } - - if (apiDetails.Properties.ApiVersionSetId != null) - { - apiDetails.DependsOn = Array.Empty(); - - string versionSetName = apiDetails.Properties.ApiVersionSetId; - int versionSetPosition = versionSetName.IndexOf("apiVersionSets/"); - - versionSetName = versionSetName.Substring(versionSetPosition, versionSetName.Length - versionSetPosition); - apiDetails.Properties.ApiVersionSetId = $"[concat(resourceId('Microsoft.ApiManagement/service', parameters('{ParameterNames.ApimServiceName}')), '/{versionSetName}')]"; - } - else - { - apiDetails.DependsOn = Array.Empty(); - } - - return apiDetails; - } } } diff --git a/src/ArmTemplates/Extractor/EntityExtractors/ApiSchemaExtractor.cs b/src/ArmTemplates/Extractor/EntityExtractors/ApiSchemaExtractor.cs index 4ce245db..14d53d88 100644 --- a/src/ArmTemplates/Extractor/EntityExtractors/ApiSchemaExtractor.cs +++ b/src/ArmTemplates/Extractor/EntityExtractors/ApiSchemaExtractor.cs @@ -6,6 +6,7 @@ using System.Threading.Tasks; using Microsoft.Azure.Management.ApiManagement.ArmTemplates.Common.API.Clients.Abstractions; using Microsoft.Azure.Management.ApiManagement.ArmTemplates.Common.Constants; +using Microsoft.Azure.Management.ApiManagement.ArmTemplates.Common.Extensions; using Microsoft.Azure.Management.ApiManagement.ArmTemplates.Common.Templates.ApiSchemas; using Microsoft.Azure.Management.ApiManagement.ArmTemplates.Extractor.EntityExtractors.Abstractions; using Microsoft.Azure.Management.ApiManagement.ArmTemplates.Extractor.Models; @@ -38,7 +39,7 @@ public async Task GenerateApiSchemaResourcesAsync(st apiSchema.Name = $"[concat(parameters('{ParameterNames.ApimServiceName}'), '/{apiName}/{apiSchemaOriginalName}')]"; apiSchema.Type = ResourceTypeConstants.APISchema; apiSchema.ApiVersion = GlobalConstants.ApiVersion; - apiSchema.DependsOn = new string[] { $"[resourceId('Microsoft.ApiManagement/service/apis', parameters('{ParameterNames.ApimServiceName}'), '{apiName}')]" }; + apiSchema.DependsOn = new string[] { NamingHelper.GenerateApisResourceId(apiName) }; apiSchemaResources.ApiSchemas.Add(apiSchema); } diff --git a/src/ArmTemplates/Extractor/EntityExtractors/DiagnosticExtractor.cs b/src/ArmTemplates/Extractor/EntityExtractors/DiagnosticExtractor.cs index 018b0f04..40a013c5 100644 --- a/src/ArmTemplates/Extractor/EntityExtractors/DiagnosticExtractor.cs +++ b/src/ArmTemplates/Extractor/EntityExtractors/DiagnosticExtractor.cs @@ -48,7 +48,7 @@ public async Task> GetApiDiagnosticsResourcesAs apiDiagnostic.Type = ResourceTypeConstants.APIDiagnostic; apiDiagnostic.ApiVersion = GlobalConstants.ApiVersion; apiDiagnostic.Scale = null; - apiDiagnostic.DependsOn = new string[] { $"[resourceId('Microsoft.ApiManagement/service/apis', parameters('{ParameterNames.ApimServiceName}'), '{apiName}')]" }; + apiDiagnostic.DependsOn = new string[] { NamingHelper.GenerateApisResourceId(apiName) }; if (extractorParameters.ParameterizeApiLoggerId) { diff --git a/src/ArmTemplates/Extractor/EntityExtractors/PolicyExtractor.cs b/src/ArmTemplates/Extractor/EntityExtractors/PolicyExtractor.cs index 68d6bb3b..7e2e852e 100644 --- a/src/ArmTemplates/Extractor/EntityExtractors/PolicyExtractor.cs +++ b/src/ArmTemplates/Extractor/EntityExtractors/PolicyExtractor.cs @@ -139,7 +139,7 @@ public async Task GenerateApiPolicyResourceAsync( apiPolicy.ApiVersion = GlobalConstants.ApiVersion; apiPolicy.Name = $"[concat(parameters('{ParameterNames.ApimServiceName}'), '/{apiName}/{apiPolicyOriginalName}')]"; - apiPolicy.DependsOn = new string[] { $"[resourceId('Microsoft.ApiManagement/service/apis', parameters('{ParameterNames.ApimServiceName}'), '{apiName}')]" }; + apiPolicy.DependsOn = new string[] { NamingHelper.GenerateApisResourceId(apiName) }; // write policy xml content to file and point to it if policyXMLBaseUrl is provided if (extractorParameters.PolicyXMLBaseUrl is not null) diff --git a/src/ArmTemplates/Extractor/EntityExtractors/ProductApisExtractor.cs b/src/ArmTemplates/Extractor/EntityExtractors/ProductApisExtractor.cs index e4734b33..c449db40 100644 --- a/src/ArmTemplates/Extractor/EntityExtractors/ProductApisExtractor.cs +++ b/src/ArmTemplates/Extractor/EntityExtractors/ProductApisExtractor.cs @@ -83,7 +83,7 @@ public async Task> GenerateSingleApiTemplateAsy this.logger.LogInformation("{0} Product API found ...", singleApiName); var dependsOnParameter = addDependsOnParameter - ? new string[] { $"[resourceId('Microsoft.ApiManagement/service/apis', parameters('{ParameterNames.ApimServiceName}'), '{singleApiName}')]" } + ? new string[] { NamingHelper.GenerateApisResourceId(singleApiName) } : Array.Empty(); return await this.GenerateProductApiTemplateResourcesAsync(singleApiName, extractorParameters, dependsOnParameter); diff --git a/src/ArmTemplates/Extractor/EntityExtractors/TagExtractor.cs b/src/ArmTemplates/Extractor/EntityExtractors/TagExtractor.cs index a3c0b0cf..9e17d158 100644 --- a/src/ArmTemplates/Extractor/EntityExtractors/TagExtractor.cs +++ b/src/ArmTemplates/Extractor/EntityExtractors/TagExtractor.cs @@ -80,7 +80,7 @@ public async Task> GenerateTagResourcesLinkedToApiAsyn apiTag.Name = $"[concat(parameters('{ParameterNames.ApimServiceName}'), '/{apiName}/{apiTagOriginalName}')]"; apiTag.ApiVersion = GlobalConstants.ApiVersion; apiTag.Scale = null; - apiTag.DependsOn = new string[] { $"[resourceId('Microsoft.ApiManagement/service/apis', parameters('{ParameterNames.ApimServiceName}'), '{apiName}')]" }; + apiTag.DependsOn = new string[] { NamingHelper.GenerateApisResourceId(apiName) }; templateResources.Add(apiTag); } diff --git a/src/ArmTemplates/Extractor/Models/ExtractorParameters.cs b/src/ArmTemplates/Extractor/Models/ExtractorParameters.cs index 9b2fdd78..76196c98 100644 --- a/src/ArmTemplates/Extractor/Models/ExtractorParameters.cs +++ b/src/ArmTemplates/Extractor/Models/ExtractorParameters.cs @@ -107,7 +107,6 @@ public ExtractorParameters(ExtractorConsoleAppConfiguration extractorConfig) this.PolicyXMLBaseUrl = extractorConfig.PolicyXMLBaseUrl; this.PolicyXMLSasToken = extractorConfig.PolicyXMLSasToken; this.ApiVersionSetName = extractorConfig.ApiVersionSetName; - this.IncludeAllRevisions = extractorConfig.IncludeAllRevisions != null && extractorConfig.IncludeAllRevisions.Equals("true", StringComparison.OrdinalIgnoreCase); this.ParameterizeNamedValue = extractorConfig.ParamNamedValue != null && extractorConfig.ParamNamedValue.Equals("true", StringComparison.OrdinalIgnoreCase); this.ParameterizeApiLoggerId = extractorConfig.ParamApiLoggerId != null && extractorConfig.ParamApiLoggerId.Equals("true", StringComparison.OrdinalIgnoreCase); this.ParameterizeLogResourceId = extractorConfig.ParamLogResourceId != null && extractorConfig.ParamLogResourceId.Equals("true", StringComparison.OrdinalIgnoreCase); diff --git a/src/ArmTemplates/Extractor/Utilities/DataProcessors/Absctraction/IApiDataProcessor.cs b/src/ArmTemplates/Extractor/Utilities/DataProcessors/Absctraction/IApiDataProcessor.cs new file mode 100644 index 00000000..b70abc66 --- /dev/null +++ b/src/ArmTemplates/Extractor/Utilities/DataProcessors/Absctraction/IApiDataProcessor.cs @@ -0,0 +1,17 @@ +// -------------------------------------------------------------------------- +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +// -------------------------------------------------------------------------- + +using System.Collections.Generic; +using Microsoft.Azure.Management.ApiManagement.ArmTemplates.Common.Templates.Apis; +using Microsoft.Azure.Management.ApiManagement.ArmTemplates.Extractor.Models; + +namespace Microsoft.Azure.Management.ApiManagement.ArmTemplates.Extractor.Utilities.DataProcessors.Absctraction +{ + public interface IApiDataProcessor + { + void ProcessData(List apis); + void ProcessSingleData(ApiTemplateResource api); + } +} diff --git a/src/ArmTemplates/Extractor/Utilities/DataProcessors/ApiDataProcessor.cs b/src/ArmTemplates/Extractor/Utilities/DataProcessors/ApiDataProcessor.cs new file mode 100644 index 00000000..d08ba9e8 --- /dev/null +++ b/src/ArmTemplates/Extractor/Utilities/DataProcessors/ApiDataProcessor.cs @@ -0,0 +1,44 @@ +// -------------------------------------------------------------------------- +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +// -------------------------------------------------------------------------- + +using System.Collections.Generic; +using Microsoft.Azure.Management.ApiManagement.ArmTemplates.Common.Extensions; +using Microsoft.Azure.Management.ApiManagement.ArmTemplates.Common.Templates.Apis; +using Microsoft.Azure.Management.ApiManagement.ArmTemplates.Extractor.Utilities.DataProcessors.Absctraction; + +namespace Microsoft.Azure.Management.ApiManagement.ArmTemplates.Extractor.Utilities.DataProcessors +{ + public class ApiDataProcessor : IApiDataProcessor + { + public void ProcessData(List apis) + { + if (apis.IsNullOrEmpty()) + { + return; + } + + foreach (var api in apis) + { + this.ProcessSingleData(api); + } + } + + public void ProcessSingleData(ApiTemplateResource api) + { + api.OriginalName = api.Name; + var isCurrent = api.Properties.IsCurrent; + api.Properties.IsCurrent = null; + + if (isCurrent == true && !string.IsNullOrEmpty(api.Properties.ApiRevision)) + { + api.ApiNameWithRevision = $"{api.Name};rev={api.Properties.ApiRevision}"; + if (!api.Name.Contains($";rev={api.Properties.ApiRevision}")) + { + api.Name = $"{api.Name};rev={api.Properties.ApiRevision}"; + } + } + } + } +} diff --git a/src/ArmTemplates/ServiceExtensions.cs b/src/ArmTemplates/ServiceExtensions.cs index 2cb45014..0f6b7b04 100644 --- a/src/ArmTemplates/ServiceExtensions.cs +++ b/src/ArmTemplates/ServiceExtensions.cs @@ -77,6 +77,7 @@ static void SetupDataProcessors(IServiceCollection services) services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); } static void SetupCommands(IServiceCollection services) @@ -144,6 +145,7 @@ static void SetupExtractors(IServiceCollection services) services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); } static void SetupApiClients(IServiceCollection services) diff --git a/tests/ArmTemplates.Tests/Extractor/Scenarios/ApiExtractorTests.cs b/tests/ArmTemplates.Tests/Extractor/Scenarios/ApiExtractorTests.cs index 0e88a1b1..550ede74 100644 --- a/tests/ArmTemplates.Tests/Extractor/Scenarios/ApiExtractorTests.cs +++ b/tests/ArmTemplates.Tests/Extractor/Scenarios/ApiExtractorTests.cs @@ -66,6 +66,7 @@ public async Task GenerateApiTemplates_ProperlyLaysTheInformation() var mockedTagClient = MockTagClient.GetMockedApiClientWithDefaultValues(); var mockedApiOperationClient = MockApiOperationClient.GetMockedApiClientWithDefaultValues(); var mockedDiagnosticClient = MockDiagnosticClient.GetMockedClientWithApiDependentValues(); + var mockedRevisionClient = MockApisRevisionsClient.GetMockedApiRevisionClientWithDefaultValues(); // mocked extractors var mockedDiagnosticExtractor = new DiagnosticExtractor(this.GetTestLogger(), mockedDiagnosticClient); @@ -84,7 +85,9 @@ public async Task GenerateApiTemplates_ProperlyLaysTheInformation() mockedPolicyExtractor, mockedProductApisExtractor, mockedTagExtractor, - mockedApiOperationExtractor); + mockedApiOperationExtractor, + mockedRevisionClient + ); var extractorExecutor = ExtractorExecutor.BuildExtractorExecutor( this.GetTestLogger(), @@ -184,6 +187,7 @@ public async Task GenerateGraphQLApiTemplates() var mockedTagClient = MockTagClient.GetMockedApiClientWithDefaultValues(); var mockedApiOperationClient = MockApiOperationClient.GetMockedApiClientWithDefaultValues(); var mockedDiagnosticClient = MockDiagnosticClient.GetMockedClientWithApiDependentValues(); + var mockedRevisionClient = MockApisRevisionsClient.GetMockedApiRevisionClientWithDefaultValues(); // mocked extractors var mockedDiagnosticExtractor = new DiagnosticExtractor(this.GetTestLogger(), mockedDiagnosticClient); @@ -202,7 +206,8 @@ public async Task GenerateGraphQLApiTemplates() mockedPolicyExtractor, mockedProductApisExtractor, mockedTagExtractor, - mockedApiOperationExtractor); + mockedApiOperationExtractor, + mockedRevisionClient); var extractorExecutor = ExtractorExecutor.BuildExtractorExecutor( this.GetTestLogger(), diff --git a/tests/ArmTemplates.Tests/Extractor/Scenarios/ApiReleaseExtractorTests.cs b/tests/ArmTemplates.Tests/Extractor/Scenarios/ApiReleaseExtractorTests.cs new file mode 100644 index 00000000..233c5988 --- /dev/null +++ b/tests/ArmTemplates.Tests/Extractor/Scenarios/ApiReleaseExtractorTests.cs @@ -0,0 +1,97 @@ +// -------------------------------------------------------------------------- +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +// -------------------------------------------------------------------------- + +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using FluentAssertions; +using Microsoft.Azure.Management.ApiManagement.ArmTemplates.Commands.Executors; +using Microsoft.Azure.Management.ApiManagement.ArmTemplates.Common.Constants; +using Microsoft.Azure.Management.ApiManagement.ArmTemplates.Common.Templates.Builders; +using Microsoft.Azure.Management.ApiManagement.ArmTemplates.Extractor.EntityExtractors; +using Microsoft.Azure.Management.ApiManagement.ArmTemplates.Extractor.Models; +using Microsoft.Azure.Management.ApiManagement.ArmTemplates.Tests.Extractor.Abstractions; +using Microsoft.Azure.Management.ApiManagement.ArmTemplates.Tests.Moqs.ApiClients; +using Xunit; + +namespace Microsoft.Azure.Management.ApiManagement.ArmTemplates.Tests.Extractor.Scenarios +{ + [Trait("Category", "Api Release Extraction")] + public class ApiReleaseExtractorTests : ExtractorMockerWithOutputTestsBase + { + public ApiReleaseExtractorTests() : base("api-release-tests") + { + } + + [Fact] + public async Task GenerateSingleApiReleaseTemplateAsync_ProperlyCreatesTemplate() + { + // arrange + var currentTestDirectory = Path.Combine(this.OutputDirectory, nameof(GenerateSingleApiReleaseTemplateAsync_ProperlyCreatesTemplate)); + + var extractorConfig = this.GetDefaultExtractorConsoleAppConfiguration(); + var extractorParameters = new ExtractorParameters(extractorConfig); + + // mocked extractors + var apiReleaseExtractor = new ApiReleaseExtractor(this.GetTestLogger(), new TemplateBuilder(), null); + + var extractorExecutor = ExtractorExecutor.BuildExtractorExecutor( + this.GetTestLogger(), + apiReleaseExtractor: apiReleaseExtractor); + extractorExecutor.SetExtractorParameters(extractorParameters); + + var apiId = "api1;rev=2"; + + // act + var apiReleaseTemplate = await extractorExecutor.GenerateApiReleaseTemplateAsync(apiId, currentTestDirectory); + + // assert + File.Exists(Path.Combine(currentTestDirectory, extractorParameters.FileNames.ApiRelease)).Should().BeTrue(); + + apiReleaseTemplate.TypedResources.ApiReleases.Count().Should().Be(1); + var apiRelease = apiReleaseTemplate.TypedResources.ApiReleases[0]; + + apiRelease.Properties.Should().NotBeNull(); + apiRelease.Properties.ApiId.Should().Be($"/apis/{apiId}"); + } + + [Fact] + public async Task GenerateAllCurrentApiReleaseTemplateAsync_ProperlyCreatesTemplate() + { + // arrange + var responseFileLocation = Path.Combine(MockClientUtils.ApiClientJsonResponsesPath, "ApiManagementListApis_success_response.json"); + var currentTestDirectory = Path.Combine(this.OutputDirectory, nameof(GenerateAllCurrentApiReleaseTemplateAsync_ProperlyCreatesTemplate)); + + var mockedApiClientAllCurrent = await MockApisClient.GetMockedHttpApiClient(responseFileLocation); + + var extractorConfig = this.GetDefaultExtractorConsoleAppConfiguration( + apiName: string.Empty); + var extractorParameters = new ExtractorParameters(extractorConfig); + + // mocked extractors + var apiReleaseExtractor = new ApiReleaseExtractor(this.GetTestLogger(), new TemplateBuilder(), mockedApiClientAllCurrent); + + var extractorExecutor = ExtractorExecutor.BuildExtractorExecutor( + this.GetTestLogger(), + apiReleaseExtractor: apiReleaseExtractor); + extractorExecutor.SetExtractorParameters(extractorParameters); + + // act + var apiReleasesTemplate = await extractorExecutor.GenerateApiReleasesTemplateAsync(currentTestDirectory); + + // assert + File.Exists(Path.Combine(currentTestDirectory, extractorParameters.FileNames.ApiRelease)).Should().BeTrue(); + + apiReleasesTemplate.TypedResources.ApiReleases.Count().Should().Be(4); + apiReleasesTemplate.TypedResources.ApiReleases.All(x => x.Type.Equals(ResourceTypeConstants.APIRelease)).Should().BeTrue(); + + apiReleasesTemplate.TypedResources.ApiReleases.Any(x => x.Properties.ApiId.Contains($"/apis/a1;rev=1")).Should().BeTrue(); + apiReleasesTemplate.TypedResources.ApiReleases.Any(x => x.Properties.ApiId.Contains($"/apis/echo-api;rev=1")).Should().BeTrue(); + apiReleasesTemplate.TypedResources.ApiReleases.Any(x => x.Properties.ApiId.Contains($"/apis/5a7390baa5816a110435aee0;rev=1")).Should().BeTrue(); + apiReleasesTemplate.TypedResources.ApiReleases.Any(x => x.Properties.ApiId.Contains($"/apis/5a73933b8f27f7cc82a2d533;rev=1")).Should().BeTrue(); + + } + } +} diff --git a/tests/ArmTemplates.Tests/Extractor/Utilities/ApiDataProcessorTest.cs b/tests/ArmTemplates.Tests/Extractor/Utilities/ApiDataProcessorTest.cs new file mode 100644 index 00000000..1620acc1 --- /dev/null +++ b/tests/ArmTemplates.Tests/Extractor/Utilities/ApiDataProcessorTest.cs @@ -0,0 +1,64 @@ +// -------------------------------------------------------------------------- +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +// -------------------------------------------------------------------------- + +using System.Collections.Generic; +using System.Linq; +using FluentAssertions; +using Microsoft.Azure.Management.ApiManagement.ArmTemplates.Common.Constants; +using Microsoft.Azure.Management.ApiManagement.ArmTemplates.Common.Templates.Apis; +using Microsoft.Azure.Management.ApiManagement.ArmTemplates.Extractor.Utilities.DataProcessors; +using Microsoft.Azure.Management.ApiManagement.ArmTemplates.Tests.Extractor.Abstractions; +using Xunit; + +namespace Microsoft.Azure.Management.ApiManagement.ArmTemplates.Tests.Extractor.Utilities +{ + public class ApiDataProcessorTest : ExtractorMockerTestsBase + { + public List GetMockedApiTemplates() { + return new List + { + new ApiTemplateResource + { + Name = "api1", + Type = ResourceTypeConstants.API, + Properties = new ApiProperties + { + DisplayName = "Unlimited", + Description = "unlimited description", + ApiRevision = "1", + IsCurrent = true, + } + }, + + new ApiTemplateResource + { + Name = "api1;rev=2", + Type = ResourceTypeConstants.API, + Properties = new ApiProperties + { + DisplayName = "Unlimited", + Description = "unlimited description", + ApiRevision = "2" + } + }, + }; + } + + [Fact] + public void TestApiDataProcess() + { + var apiTemplates = this.GetMockedApiTemplates(); + + var apiDataProcessor = new ApiDataProcessor(); + + apiDataProcessor.ProcessData(apiTemplates); + + apiTemplates.ElementAt(0).Name.Should().BeEquivalentTo("api1;rev=1"); + apiTemplates.ElementAt(0).ApiNameWithRevision.Should().BeEquivalentTo("api1;rev=1"); + + apiTemplates.ElementAt(1).Name.Should().BeEquivalentTo("api1;rev=2"); + } + } +} diff --git a/tests/ArmTemplates.Tests/Moqs/ApiClients/MockApisClient.cs b/tests/ArmTemplates.Tests/Moqs/ApiClients/MockApisClient.cs index c377d397..ec29f64b 100644 --- a/tests/ArmTemplates.Tests/Moqs/ApiClients/MockApisClient.cs +++ b/tests/ArmTemplates.Tests/Moqs/ApiClients/MockApisClient.cs @@ -4,9 +4,12 @@ // -------------------------------------------------------------------------- using System.Collections.Generic; +using System.Threading.Tasks; using Microsoft.Azure.Management.ApiManagement.ArmTemplates.Common.API.Clients.Abstractions; +using Microsoft.Azure.Management.ApiManagement.ArmTemplates.Common.API.Clients.Apis; using Microsoft.Azure.Management.ApiManagement.ArmTemplates.Common.Templates.Apis; using Microsoft.Azure.Management.ApiManagement.ArmTemplates.Extractor.Models; +using Microsoft.Azure.Management.ApiManagement.ArmTemplates.Extractor.Utilities.DataProcessors; using Moq; namespace Microsoft.Azure.Management.ApiManagement.ArmTemplates.Tests.Moqs.ApiClients @@ -58,12 +61,12 @@ public static ApiProperties GetMockedServiceApiProperties1() public static IApisClient GetMockedApiClientWithDefaultValues() { - var mockServiceApiProductsApiClient = new Mock(MockBehavior.Strict); + var mockedApisClient = new Mock(MockBehavior.Strict); var serviceProperties1 = GetMockedServiceApiProperties1(); var serviceProperties2 = GetMockedServiceApiProperties2(); - mockServiceApiProductsApiClient + mockedApisClient .Setup(x => x.GetAllAsync(It.IsAny())) .ReturnsAsync((ExtractorParameters _) => new List { @@ -82,7 +85,13 @@ public static IApisClient GetMockedApiClientWithDefaultValues() }, }); - mockServiceApiProductsApiClient + mockedApisClient + .Setup(x => x.GetAllCurrentAsync(It.IsAny())) + .ReturnsAsync((ExtractorParameters _) => new List + { + }); + + mockedApisClient .Setup(x => x.GetAllLinkedToGatewayAsync(It.IsAny(), It.IsAny())) .ReturnsAsync((string gatewayName, ExtractorParameters _) => new List { @@ -101,7 +110,7 @@ public static IApisClient GetMockedApiClientWithDefaultValues() }, }); - mockServiceApiProductsApiClient + mockedApisClient .Setup(x => x.GetSingleAsync(It.Is((o => o.Equals(ServiceApiName1))), It.IsAny())) .ReturnsAsync((string _, ExtractorParameters _) => new ApiTemplateResource { @@ -110,7 +119,7 @@ public static IApisClient GetMockedApiClientWithDefaultValues() Properties = serviceProperties1 }); - mockServiceApiProductsApiClient + mockedApisClient .Setup(x => x.GetSingleAsync(It.Is((o => o.Equals(ServiceApiName2))), It.IsAny())) .ReturnsAsync((string _, ExtractorParameters _) => new ApiTemplateResource { @@ -119,7 +128,7 @@ public static IApisClient GetMockedApiClientWithDefaultValues() Properties = serviceProperties2 }); - mockServiceApiProductsApiClient + mockedApisClient .Setup(x => x.GetAllLinkedToProductAsync(It.IsAny(), It.IsAny())) .ReturnsAsync((string productName, ExtractorParameters _) => new List { @@ -138,7 +147,16 @@ public static IApisClient GetMockedApiClientWithDefaultValues() } }); - return mockServiceApiProductsApiClient.Object; + return mockedApisClient.Object; + } + + public static async Task GetMockedHttpApiClient(string responseFileLocation) + { + var apiDataProcessor = new ApiDataProcessor(); + var mockedClient = new Mock(MockBehavior.Strict, await MockClientUtils.GenerateMockedIHttpClientFactoryWithResponse(responseFileLocation), apiDataProcessor); + MockClientUtils.MockAuthOfApiClient(mockedClient); + + return mockedClient.Object; } } } diff --git a/tests/ArmTemplates.Tests/Moqs/ApiClients/MockApisRevisionsClient.cs b/tests/ArmTemplates.Tests/Moqs/ApiClients/MockApisRevisionsClient.cs new file mode 100644 index 00000000..e8eef926 --- /dev/null +++ b/tests/ArmTemplates.Tests/Moqs/ApiClients/MockApisRevisionsClient.cs @@ -0,0 +1,31 @@ +// -------------------------------------------------------------------------- +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +// -------------------------------------------------------------------------- + +using System.Collections.Generic; +using Microsoft.Azure.Management.ApiManagement.ArmTemplates.Common.API.Clients.Abstractions; +using Microsoft.Azure.Management.ApiManagement.ArmTemplates.Common.Templates.ApiRevisions; +using Microsoft.Azure.Management.ApiManagement.ArmTemplates.Extractor.Models; +using Moq; + +namespace Microsoft.Azure.Management.ApiManagement.ArmTemplates.Tests.Moqs.ApiClients +{ + public class MockApisRevisionsClient + { + public static IApiRevisionClient GetMockedApiRevisionClientWithDefaultValues() + { + var mockApiRevisionClient = new Mock(MockBehavior.Loose); + + mockApiRevisionClient + .Setup(x => x.GetApiRevisionsAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(new List + { + new ApiRevisionTemplateResource(), + new ApiRevisionTemplateResource() + }); + + return mockApiRevisionClient.Object; + } + } +} diff --git a/tests/ArmTemplates.Tests/Resources/ApiClientJsonResponses/ApiManagementListApis_success_response.json b/tests/ArmTemplates.Tests/Resources/ApiClientJsonResponses/ApiManagementListApis_success_response.json new file mode 100644 index 00000000..aa1172bb --- /dev/null +++ b/tests/ArmTemplates.Tests/Resources/ApiClientJsonResponses/ApiManagementListApis_success_response.json @@ -0,0 +1,66 @@ +{ + "value": [ + { + "id": "/subscriptions/subid/resourceGroups/rg1/providers/Microsoft.ApiManagement/service/apimService1/apis/a1", + "type": "Microsoft.ApiManagement/service/apis", + "name": "a1", + "properties": { + "displayName": "api1", + "apiRevision": "1", + "serviceUrl": "http://echoapi.cloudapp.net/api", + "path": "api1", + "protocols": [ + "https" + ], + "isCurrent": true, + } + }, + { + "id": "/subscriptions/subid/resourceGroups/rg1/providers/Microsoft.ApiManagement/service/apimService1/apis/5a73933b8f27f7cc82a2d533", + "type": "Microsoft.ApiManagement/service/apis", + "name": "5a73933b8f27f7cc82a2d533", + "properties": { + "displayName": "api1", + "apiRevision": "1", + "serviceUrl": "http://echoapi.cloudapp.net/api", + "path": "api1", + "protocols": [ + "https" + ], + "isCurrent": true, + } + }, + { + "id": "/subscriptions/subid/resourceGroups/rg1/providers/Microsoft.ApiManagement/service/apimService1/apis/echo-api", + "type": "Microsoft.ApiManagement/service/apis", + "name": "echo-api", + "properties": { + "displayName": "Echo API", + "apiRevision": "1", + "serviceUrl": "http://echoapi.cloudapp.net/api", + "path": "echo", + "protocols": [ + "https" + ], + "isCurrent": true + } + }, + { + "id": "/subscriptions/subid/resourceGroups/rg1/providers/Microsoft.ApiManagement/service/apimService1/apis/5a7390baa5816a110435aee0", + "type": "Microsoft.ApiManagement/service/apis", + "name": "5a7390baa5816a110435aee0", + "properties": { + "displayName": "Swagger Petstore Extensive", + "apiRevision": "1", + "description": "A sample API that uses a petstore as an example to demonstrate features in the swagger-2.0 specification", + "serviceUrl": "http://petstore.swagger.wordnik.com/api", + "path": "vvv", + "protocols": [ + "https" + ], + "isCurrent": true + } + } + ], + "count": 4, +} \ No newline at end of file