1
- namespace Azure . Sdk . Tools . PipelineWitness
1
+ using System . Diagnostics ;
2
+
3
+ namespace Azure . Sdk . Tools . PipelineWitness
2
4
{
3
5
using System ;
4
6
using System . Collections . Generic ;
5
7
using System . Collections . Immutable ;
6
8
using System . IO ;
9
+ using System . IO . Compression ;
7
10
using System . Linq ;
8
11
using System . Net ;
9
12
using System . Text ;
@@ -32,6 +35,7 @@ public class BlobUploadProcessor
32
35
private const string BuildTimelineRecordsContainerName = "buildtimelinerecords" ;
33
36
private const string BuildDefinitionsContainerName = "builddefinitions" ;
34
37
private const string BuildFailuresContainerName = "buildfailures" ;
38
+ private const string PipelineOwnersContainerName = "pipelineowners" ;
35
39
private const string TestRunsContainerName = "testruns" ;
36
40
37
41
private const string TimeFormat = @"yyyy-MM-dd\THH:mm:ss.fffffff\Z" ;
@@ -52,6 +56,7 @@ public class BlobUploadProcessor
52
56
private readonly BlobContainerClient testRunsContainerClient ;
53
57
private readonly BlobContainerClient buildDefinitionsContainerClient ;
54
58
private readonly BlobContainerClient buildFailuresContainerClient ;
59
+ private readonly BlobContainerClient pipelineOwnersContainerClient ;
55
60
private readonly IOptions < PipelineWitnessSettings > options ;
56
61
private readonly Dictionary < string , int ? > cachedDefinitionRevisions = new ( ) ;
57
62
private readonly IFailureAnalyzer failureAnalyzer ;
@@ -82,6 +87,7 @@ public BlobUploadProcessor(
82
87
this . buildFailuresContainerClient = blobServiceClient . GetBlobContainerClient ( BuildFailuresContainerName ) ;
83
88
this . testRunsContainerClient = blobServiceClient . GetBlobContainerClient ( TestRunsContainerName ) ;
84
89
this . buildDefinitionsContainerClient = blobServiceClient . GetBlobContainerClient ( BuildDefinitionsContainerName ) ;
90
+ this . pipelineOwnersContainerClient = blobServiceClient . GetBlobContainerClient ( PipelineOwnersContainerName ) ;
85
91
this . failureAnalyzer = failureAnalyzer ;
86
92
}
87
93
@@ -176,6 +182,119 @@ public async Task UploadBuildBlobsAsync(string account, Guid projectId, int buil
176
182
{
177
183
await UploadLogLinesBlobAsync ( account , build , log ) ;
178
184
}
185
+
186
+ if ( build . Definition . Id == options . Value . PipelineOwnersDefinitionId )
187
+ {
188
+ await UploadPipelineOwnersBlobAsync ( account , build , timeline ) ;
189
+ }
190
+ }
191
+
192
+ private async Task UploadPipelineOwnersBlobAsync ( string account , Build build , Timeline timeline )
193
+ {
194
+ try
195
+ {
196
+ var blobPath = $ "{ build . Project . Name } /{ build . FinishTime : yyyy/MM/dd} /{ build . Id } -{ timeline . ChangeId } .jsonl";
197
+ var blobClient = this . pipelineOwnersContainerClient . GetBlobClient ( blobPath ) ;
198
+
199
+ if ( await blobClient . ExistsAsync ( ) )
200
+ {
201
+ this . logger . LogInformation ( "Skipping existing build failure blob for build {BuildId}" , build . Id ) ;
202
+ return ;
203
+ }
204
+
205
+ var owners = await GetOwnersFromBuildArtifactAsync ( build ) ;
206
+
207
+ if ( owners == null )
208
+ {
209
+ // no need to log anything here. GetOwnersFromBuildArtifactAsync logs a warning before returning null;
210
+ return ;
211
+ }
212
+
213
+ this . logger . LogInformation ( "Creating owners blob for build {DefinitionId} change {ChangeId}" , build . Id , timeline . ChangeId ) ;
214
+
215
+ var stringBuilder = new StringBuilder ( ) ;
216
+
217
+ foreach ( var owner in owners )
218
+ {
219
+ var contentLine = JsonConvert . SerializeObject ( new
220
+ {
221
+ OrganizationName = account ,
222
+ BuildDefinitionId = owner . Key ,
223
+ Owners = owner . Value ,
224
+ Timestamp = new DateTimeOffset ( build . FinishTime . Value ) . ToUniversalTime ( ) ,
225
+ EtlIngestDate = DateTimeOffset . UtcNow
226
+ } , jsonSettings ) ;
227
+
228
+ stringBuilder . AppendLine ( contentLine ) ;
229
+ }
230
+
231
+ await blobClient . UploadAsync ( new BinaryData ( stringBuilder . ToString ( ) ) ) ;
232
+ }
233
+ catch ( RequestFailedException ex ) when ( ex . Status == ( int ) HttpStatusCode . Conflict )
234
+ {
235
+ this . logger . LogInformation ( "Ignoring exception from existing owners blob for build {BuildId}" , build . Id ) ;
236
+ }
237
+ catch ( Exception ex )
238
+ {
239
+ this . logger . LogError ( ex , "Error processing owners artifact from build {BuildId}" , build . Id ) ;
240
+ throw ;
241
+ }
242
+ }
243
+
244
+ private async Task < Dictionary < int , string [ ] > > GetOwnersFromBuildArtifactAsync ( Build build )
245
+ {
246
+ var artifactName = this . options . Value . PipelineOwnersArtifactName ;
247
+ var filePath = this . options . Value . PipelineOwnersFilePath ;
248
+
249
+ try
250
+ {
251
+ await using var artifactStream = await this . buildClient . GetArtifactContentZipAsync ( build . Project . Id , build . Id , artifactName ) ;
252
+ using var zip = new ZipArchive ( artifactStream ) ;
253
+
254
+ var fileEntry = zip . GetEntry ( filePath ) ;
255
+
256
+ if ( fileEntry == null )
257
+ {
258
+ this . logger . LogWarning ( "Artifact {ArtifactName} in build {BuildId} didn't contain the expected file {FilePath}" , artifactName , build . Id , filePath ) ;
259
+ return null ;
260
+ }
261
+
262
+ await using var contentStream = fileEntry . Open ( ) ;
263
+ using var contentReader = new StreamReader ( contentStream ) ;
264
+ var content = await contentReader . ReadToEndAsync ( ) ;
265
+
266
+ if ( string . IsNullOrEmpty ( content ) )
267
+ {
268
+ this . logger . LogWarning ( "The file {filePath} in artifact {ArtifactName} in build {BuildId} contained no content" , filePath , artifactName , build . Id ) ;
269
+ return null ;
270
+ }
271
+
272
+ var ownersDictionary = JsonConvert . DeserializeObject < Dictionary < int , string [ ] > > ( content ) ;
273
+
274
+ if ( ownersDictionary == null )
275
+ {
276
+ this . logger . LogWarning ( "The file {filePath} in artifact {ArtifactName} in build {BuildId} contained a null json object" , filePath , artifactName , build . Id ) ;
277
+ }
278
+
279
+ return ownersDictionary ;
280
+ }
281
+ catch ( ArtifactNotFoundException ex )
282
+ {
283
+ this . logger . LogWarning ( ex , "Build {BuildId} did not contain the expected artifact {ArtifactName}" , build . Id , artifactName ) ;
284
+ }
285
+ catch ( InvalidDataException ex )
286
+ {
287
+ this . logger . LogWarning ( ex , "Unable to read ZIP contents from artifact {ArtifactName} in build {BuildId}" , artifactName , build . Id ) ;
288
+
289
+ // rethrow the exception so the queue message will be retried.
290
+ throw ;
291
+ }
292
+ catch ( JsonSerializationException ex )
293
+ {
294
+ this . logger . LogWarning ( ex , "Problem deserializing JSON from artifact {ArtifactName} in build {BuildId}" , artifactName , build . Id ) ;
295
+ }
296
+
297
+ return null ;
179
298
}
180
299
181
300
private async Task UploadBuildFailureBlobAsync ( string account , Build build , Timeline timeline )
@@ -217,7 +336,8 @@ private async Task UploadBuildFailureBlobAsync(string account, Build build, Time
217
336
RecordId = failure . Record . Id ,
218
337
BuildTimelineId = timeline . Id ,
219
338
ErrorClassification = failure . Classification ,
220
- } , jsonSettings ) ;
339
+ EtlIngestDate = DateTimeOffset . UtcNow
340
+ } , jsonSettings ) ;
221
341
stringBuilder . AppendLine ( contentLine ) ;
222
342
}
223
343
@@ -241,16 +361,16 @@ public async Task UploadBuildDefinitionBlobsAsync(string account, string project
241
361
foreach ( var definition in definitions )
242
362
{
243
363
var cacheKey = $ "{ definition . Project . Id } :{ definition . Id } ";
244
-
364
+
245
365
if ( ! this . cachedDefinitionRevisions . TryGetValue ( cacheKey , out var cachedRevision ) || cachedRevision != definition . Revision )
246
- {
366
+ {
247
367
await UploadBuildDefinitionBlobAsync ( account , definition ) ;
248
368
}
249
369
250
370
this . cachedDefinitionRevisions [ cacheKey ] = definition . Revision ;
251
371
}
252
372
}
253
-
373
+
254
374
private async Task UploadBuildDefinitionBlobAsync ( string account , BuildDefinition definition )
255
375
{
256
376
var blobPath = $ "{ definition . Project . Name } /{ definition . Id } -{ definition . Revision } .jsonl";
0 commit comments