3
3
4
4
using System . Diagnostics ;
5
5
using System . Diagnostics . Tracing ;
6
+ using System . Dynamic ;
7
+ using System . Net . Http . Json ;
6
8
using System . Text . Json ;
7
9
using Azure . Core ;
8
10
using Azure . Core . Diagnostics ;
12
14
using Microsoft . DevProxy . Plugins . RequestLogs . ApiCenter ;
13
15
using Microsoft . Extensions . Configuration ;
14
16
using Microsoft . Extensions . Logging ;
17
+ using Microsoft . OpenApi . Readers ;
15
18
16
19
internal class ApiInformation
17
20
{
21
+ public string Name { get ; set ; } = "" ;
18
22
public ApiInformationVersion [ ] Versions { get ; set ; } = [ ] ;
19
- // deployment.properties.server.runtimeUri[]
20
- public string [ ] Urls { get ; set ; } = [ ] ;
21
23
}
22
24
23
25
internal class ApiInformationVersion
24
26
{
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 ; } = "" ;
32
28
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 ; } = [ ] ;
35
31
}
36
32
37
33
internal class ApiCenterProductionVersionPluginConfiguration
@@ -154,44 +150,85 @@ private async Task AfterRecordingStop(object sender, RecordingArgs e)
154
150
155
151
Debug . Assert ( _httpClient is not null ) ;
156
152
157
- var apis = await LoadApisFromApiCenter ( ) ;
158
- if ( apis == null || ! apis . Value . Any ( ) )
153
+ var apisFromApiCenter = await LoadApisFromApiCenter ( ) ;
154
+ if ( apisFromApiCenter == null || ! apisFromApiCenter . Value . Any ( ) )
159
155
{
160
156
_logger ? . LogInformation ( "No APIs found in API Center" ) ;
161
157
return ;
162
158
}
163
159
164
160
var apisInformation = new List < ApiInformation > ( ) ;
165
- foreach ( var api in apis . Value )
161
+ foreach ( var api in apisFromApiCenter . Value )
166
162
{
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 ( ) )
169
165
{
170
166
_logger ? . LogInformation ( "No versions found for {api}" , api . Properties ? . Title ) ;
171
167
continue ;
172
168
}
173
169
174
- var apiInformationVersion = apiVersions . Value . Select ( v => new ApiInformationVersion
170
+ var versions = new List < ApiInformationVersion > ( ) ;
171
+ foreach ( var versionFromApiCenter in apiVersionsFromApiCenter . Value )
175
172
{
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 ( ) )
177
208
{
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 ( ) )
186
223
{
187
- _logger ? . LogInformation ( "No deployments found for {api}" , api . Properties ? . Title ) ;
224
+ _logger ? . LogInformation ( "No versions found for {api}" , api . Properties ? . Title ) ;
188
225
continue ;
189
226
}
190
227
191
228
apisInformation . Add ( new ApiInformation
192
229
{
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 ( )
195
232
} ) ;
196
233
}
197
234
@@ -218,25 +255,87 @@ private async Task AfterRecordingStop(object sender, RecordingArgs e)
218
255
{
219
256
var productionVersions = apiInformation . Versions
220
257
. Where ( v => v . LifecycleStage == ApiLifecycleStage . Production )
221
- . Select ( v => v . Version . Name ) ;
258
+ . Select ( v => v . Title ) ;
222
259
223
260
if ( productionVersions . Any ( ) )
224
261
{
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 ) ) ;
226
263
}
227
264
else
228
265
{
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 ) ;
230
267
}
231
268
}
232
269
}
233
270
234
271
_logger ? . LogInformation ( "DONE" ) ;
235
272
}
236
273
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
+
237
336
private ApiInformation ? FindMatchingApiInformation ( string requestUrl , List < ApiInformation > ? apisInformation )
238
337
{
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 ) ) ) ) ;
240
339
if ( apiInformation is null )
241
340
{
242
341
_logger ? . LogDebug ( "No matching API found for {request}" , requestUrl ) ;
@@ -258,22 +357,22 @@ private async Task AfterRecordingStop(object sender, RecordingArgs e)
258
357
foreach ( var apiVersion in apiInformation . Versions )
259
358
{
260
359
// check URL
261
- if ( requestUrl . Contains ( apiVersion . Version . Id ) || requestUrl . Contains ( apiVersion . Version . Name ) )
360
+ if ( requestUrl . Contains ( apiVersion . Name ) || requestUrl . Contains ( apiVersion . Title ) )
262
361
{
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 ) ;
264
363
version = apiVersion ;
265
364
break ;
266
365
}
267
366
268
367
// check headers
269
368
Debug . Assert ( request . Context is not null ) ;
270
369
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 )
273
372
) ;
274
373
if ( header is not null )
275
374
{
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 ) ;
277
376
version = apiVersion ;
278
377
break ;
279
378
}
@@ -288,7 +387,7 @@ private async Task AfterRecordingStop(object sender, RecordingArgs e)
288
387
return version . LifecycleStage ;
289
388
}
290
389
291
- private async Task < Collection < ApiVersion > ? > LoadApiVersions ( Api api )
390
+ private async Task < Collection < ApiVersion > ? > LoadApiVersionsFromApiCenter ( Api api )
292
391
{
293
392
Debug . Assert ( _httpClient is not null ) ;
294
393
0 commit comments