Skip to content

Commit 473b53e

Browse files
Fixes resolving production APIs from APIC (#695)
1 parent c9c7f24 commit 473b53e

File tree

1 file changed

+138
-39
lines changed

1 file changed

+138
-39
lines changed

dev-proxy-plugins/RequestLogs/ApiCenterProductionVersionPlugin.cs

Lines changed: 138 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33

44
using System.Diagnostics;
55
using System.Diagnostics.Tracing;
6+
using System.Dynamic;
7+
using System.Net.Http.Json;
68
using System.Text.Json;
79
using Azure.Core;
810
using Azure.Core.Diagnostics;
@@ -12,26 +14,20 @@
1214
using Microsoft.DevProxy.Plugins.RequestLogs.ApiCenter;
1315
using Microsoft.Extensions.Configuration;
1416
using Microsoft.Extensions.Logging;
17+
using Microsoft.OpenApi.Readers;
1518

1619
internal class ApiInformation
1720
{
21+
public string Name { get; set; } = "";
1822
public ApiInformationVersion[] Versions { get; set; } = [];
19-
// deployment.properties.server.runtimeUri[]
20-
public string[] Urls { get; set; } = [];
2123
}
2224

2325
internal class ApiInformationVersion
2426
{
25-
public ApiInformationVersionInformation Version { get; set; } = new();
26-
public ApiLifecycleStage? LifecycleStage { get; set; }
27-
}
28-
29-
internal class ApiInformationVersionInformation
30-
{
31-
// properties.title
27+
public string Title { get; set; } = "";
3228
public string Name { get; set; } = "";
33-
// name
34-
public string Id { get; set; } = "";
29+
public ApiLifecycleStage? LifecycleStage { get; set; }
30+
public string[] Urls { get; set; } = [];
3531
}
3632

3733
internal class ApiCenterProductionVersionPluginConfiguration
@@ -154,44 +150,85 @@ private async Task AfterRecordingStop(object sender, RecordingArgs e)
154150

155151
Debug.Assert(_httpClient is not null);
156152

157-
var apis = await LoadApisFromApiCenter();
158-
if (apis == null || !apis.Value.Any())
153+
var apisFromApiCenter = await LoadApisFromApiCenter();
154+
if (apisFromApiCenter == null || !apisFromApiCenter.Value.Any())
159155
{
160156
_logger?.LogInformation("No APIs found in API Center");
161157
return;
162158
}
163159

164160
var apisInformation = new List<ApiInformation>();
165-
foreach (var api in apis.Value)
161+
foreach (var api in apisFromApiCenter.Value)
166162
{
167-
var apiVersions = await LoadApiVersions(api);
168-
if (apiVersions == null || !apiVersions.Value.Any())
163+
var apiVersionsFromApiCenter = await LoadApiVersionsFromApiCenter(api);
164+
if (apiVersionsFromApiCenter == null || !apiVersionsFromApiCenter.Value.Any())
169165
{
170166
_logger?.LogInformation("No versions found for {api}", api.Properties?.Title);
171167
continue;
172168
}
173169

174-
var apiInformationVersion = apiVersions.Value.Select(v => new ApiInformationVersion
170+
var versions = new List<ApiInformationVersion>();
171+
foreach (var versionFromApiCenter in apiVersionsFromApiCenter.Value)
175172
{
176-
Version = new ApiInformationVersionInformation
173+
Debug.Assert(versionFromApiCenter.Id is not null);
174+
175+
var definitionsFromApiCenter = await LoadApiDefinitionsForVersion(versionFromApiCenter.Id);
176+
if (definitionsFromApiCenter is null || !definitionsFromApiCenter.Value.Any())
177+
{
178+
_logger?.LogDebug("No definitions found for version {versionId}", versionFromApiCenter.Id);
179+
continue;
180+
}
181+
182+
var apiUrls = new HashSet<string>();
183+
foreach (var definitionFromApiCenter in definitionsFromApiCenter.Value)
184+
{
185+
Debug.Assert(definitionFromApiCenter.Id is not null);
186+
187+
await EnsureApiDefinition(definitionFromApiCenter);
188+
189+
if (definitionFromApiCenter.Definition is null)
190+
{
191+
_logger?.LogDebug("API definition not found for {definitionId}", definitionFromApiCenter.Id);
192+
continue;
193+
}
194+
195+
if (!definitionFromApiCenter.Definition.Servers.Any())
196+
{
197+
_logger?.LogDebug("No servers found for API definition {definitionId}", definitionFromApiCenter.Id);
198+
continue;
199+
}
200+
201+
foreach (var server in definitionFromApiCenter.Definition.Servers)
202+
{
203+
apiUrls.Add(server.Url);
204+
}
205+
}
206+
207+
if (!apiUrls.Any())
177208
{
178-
Name = v.Properties?.Title ?? "",
179-
Id = v.Id ?? ""
180-
},
181-
LifecycleStage = v.Properties?.LifecycleStage
182-
}).ToArray();
183-
184-
var apiDeployments = await LoadApiDeployments(api);
185-
if (apiDeployments == null || !apiDeployments.Value.Any())
209+
_logger?.LogDebug("No URLs found for version {versionId}", versionFromApiCenter.Id);
210+
continue;
211+
}
212+
213+
versions.Add(new ApiInformationVersion
214+
{
215+
Title = versionFromApiCenter.Properties?.Title ?? "",
216+
Name = versionFromApiCenter.Name ?? "",
217+
LifecycleStage = versionFromApiCenter.Properties?.LifecycleStage,
218+
Urls = apiUrls.ToArray()
219+
});
220+
}
221+
222+
if (!versions.Any())
186223
{
187-
_logger?.LogInformation("No deployments found for {api}", api.Properties?.Title);
224+
_logger?.LogInformation("No versions found for {api}", api.Properties?.Title);
188225
continue;
189226
}
190227

191228
apisInformation.Add(new ApiInformation
192229
{
193-
Versions = apiInformationVersion,
194-
Urls = apiDeployments.Value.SelectMany(d => d.Properties?.Server?.RuntimeUri ?? Array.Empty<string>()).ToArray()
230+
Name = api.Properties?.Title ?? "",
231+
Versions = versions.ToArray()
195232
});
196233
}
197234

@@ -218,25 +255,87 @@ private async Task AfterRecordingStop(object sender, RecordingArgs e)
218255
{
219256
var productionVersions = apiInformation.Versions
220257
.Where(v => v.LifecycleStage == ApiLifecycleStage.Production)
221-
.Select(v => v.Version.Name);
258+
.Select(v => v.Title);
222259

223260
if (productionVersions.Any())
224261
{
225-
_logger?.LogWarning("Request {request} uses API version {version} which is defined as {lifecycleStage}. Upgrade to a production version of the API. Recommended versions: {versions}", urlAndMethodString, apiInformation.Versions.First(v => v.LifecycleStage == lifecycleStage).Version.Name, lifecycleStage, string.Join(", ", productionVersions));
262+
_logger?.LogWarning("Request {request} uses API version {version} which is defined as {lifecycleStage}. Upgrade to a production version of the API. Recommended versions: {versions}", urlAndMethodString, apiInformation.Versions.First(v => v.LifecycleStage == lifecycleStage).Title, lifecycleStage, string.Join(", ", productionVersions));
226263
}
227264
else
228265
{
229-
_logger?.LogWarning("Request {request} uses API version {version} which is defined as {lifecycleStage}.", urlAndMethodString, apiInformation.Versions.First(v => v.LifecycleStage == lifecycleStage).Version.Name, lifecycleStage);
266+
_logger?.LogWarning("Request {request} uses API version {version} which is defined as {lifecycleStage}.", urlAndMethodString, apiInformation.Versions.First(v => v.LifecycleStage == lifecycleStage).Title, lifecycleStage);
230267
}
231268
}
232269
}
233270

234271
_logger?.LogInformation("DONE");
235272
}
236273

274+
private async Task<Collection<ApiDefinition>?> LoadApiDefinitionsForVersion(string versionId)
275+
{
276+
Debug.Assert(_httpClient is not null);
277+
278+
_logger?.LogDebug("Loading API definitions for version {id}...", versionId);
279+
280+
var res = await _httpClient.GetStringAsync($"https://management.azure.com{versionId}/definitions?api-version=2024-03-01");
281+
return JsonSerializer.Deserialize<Collection<ApiDefinition>>(res, _jsonSerializerOptions);
282+
}
283+
284+
async Task EnsureApiDefinition(ApiDefinition apiDefinition)
285+
{
286+
Debug.Assert(_httpClient is not null);
287+
288+
if (apiDefinition.Definition is not null)
289+
{
290+
_logger?.LogDebug("API definition already loaded for {apiDefinitionId}", apiDefinition.Id);
291+
return;
292+
}
293+
294+
_logger?.LogDebug("Loading API definition for {apiDefinitionId}...", apiDefinition.Id);
295+
296+
var res = await _httpClient.GetStringAsync($"https://management.azure.com{apiDefinition.Id}?api-version=2024-03-01");
297+
var definition = JsonSerializer.Deserialize<ApiDefinition>(res, _jsonSerializerOptions);
298+
if (definition is null)
299+
{
300+
_logger?.LogError("Failed to deserialize API definition for {apiDefinitionId}", apiDefinition.Id);
301+
return;
302+
}
303+
304+
apiDefinition.Properties = definition.Properties;
305+
if (apiDefinition.Properties?.Specification?.Name != "openapi")
306+
{
307+
_logger?.LogDebug("API definition is not OpenAPI for {apiDefinitionId}", apiDefinition.Id);
308+
return;
309+
}
310+
311+
var definitionRes = await _httpClient.PostAsync($"https://management.azure.com{apiDefinition.Id}/exportSpecification?api-version=2024-03-01", null);
312+
var exportResult = await definitionRes.Content.ReadFromJsonAsync<ApiSpecExportResult>();
313+
if (exportResult is null)
314+
{
315+
_logger?.LogError("Failed to deserialize exported API definition for {apiDefinitionId}", apiDefinition.Id);
316+
return;
317+
}
318+
319+
if (exportResult.Format != ApiSpecExportResultFormat.Inline)
320+
{
321+
_logger?.LogDebug("API definition is not inline for {apiDefinitionId}", apiDefinition.Id);
322+
return;
323+
}
324+
325+
try
326+
{
327+
apiDefinition.Definition = new OpenApiStringReader().Read(exportResult.Value, out _);
328+
}
329+
catch (Exception ex)
330+
{
331+
_logger?.LogError(ex, "Failed to parse OpenAPI document for {apiDefinitionId}", apiDefinition.Id);
332+
return;
333+
}
334+
}
335+
237336
private ApiInformation? FindMatchingApiInformation(string requestUrl, List<ApiInformation>? apisInformation)
238337
{
239-
var apiInformation = apisInformation?.FirstOrDefault(a => a.Urls.Any(u => requestUrl.StartsWith(u)));
338+
var apiInformation = apisInformation?.FirstOrDefault(a => a.Versions.Any(v => v.Urls.Any(u => requestUrl.StartsWith(u))));
240339
if (apiInformation is null)
241340
{
242341
_logger?.LogDebug("No matching API found for {request}", requestUrl);
@@ -258,22 +357,22 @@ private async Task AfterRecordingStop(object sender, RecordingArgs e)
258357
foreach (var apiVersion in apiInformation.Versions)
259358
{
260359
// check URL
261-
if (requestUrl.Contains(apiVersion.Version.Id) || requestUrl.Contains(apiVersion.Version.Name))
360+
if (requestUrl.Contains(apiVersion.Name) || requestUrl.Contains(apiVersion.Title))
262361
{
263-
_logger?.LogDebug("Version {version} found in URL {url}", $"{apiVersion.Version.Id}/{apiVersion.Version.Name}", requestUrl);
362+
_logger?.LogDebug("Version {version} found in URL {url}", $"{apiVersion.Name}/{apiVersion.Title}", requestUrl);
264363
version = apiVersion;
265364
break;
266365
}
267366

268367
// check headers
269368
Debug.Assert(request.Context is not null);
270369
var header = request.Context.Session.HttpClient.Request.Headers.FirstOrDefault(
271-
h => h.Value.Contains(apiVersion.Version.Id) ||
272-
h.Value.Contains(apiVersion.Version.Name)
370+
h => h.Value.Contains(apiVersion.Name) ||
371+
h.Value.Contains(apiVersion.Title)
273372
);
274373
if (header is not null)
275374
{
276-
_logger?.LogDebug("Version {version} found in header {header}", $"{apiVersion.Version.Id}/{apiVersion.Version.Name}", header.Name);
375+
_logger?.LogDebug("Version {version} found in header {header}", $"{apiVersion.Name}/{apiVersion.Title}", header.Name);
277376
version = apiVersion;
278377
break;
279378
}
@@ -288,7 +387,7 @@ private async Task AfterRecordingStop(object sender, RecordingArgs e)
288387
return version.LifecycleStage;
289388
}
290389

291-
private async Task<Collection<ApiVersion>?> LoadApiVersions(Api api)
390+
private async Task<Collection<ApiVersion>?> LoadApiVersionsFromApiCenter(Api api)
292391
{
293392
Debug.Assert(_httpClient is not null);
294393

0 commit comments

Comments
 (0)